Linux进程编程介绍01

更新时间:2023-06-08 00:23:01 阅读量: 实用文档 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

Linux进程编程

Linux进程编程

第一章 进程的定义

摘要:本节将介绍进程的定义。进程作为构成系统的基本细胞,不仅是系统内部独立运行的实体,而且是独立竞争资源的基本实体。了解进程的本质,对于理解、描述和设计操作系统有着极为重要的意义。了解进程的活动、状态,也有利于编制复杂程序。

1.进程的基本概念

首先我们先看看进程的定义,进程是一个具有独立功能的程序关于某个数据集合的一次可以并发执行的运行活动,是处于活动状态的计算机程序。进程作为构成系统的基本细胞,不仅是系统内部独立运行的实体,而且是独立竞争资源的基本实体。了解进程的本质,对于理解、描述和设计操作系统有着极为重要的意义。了解进程的活动、状态,也有利于编制复杂程序。

1.1 进程状态和状态转换

现在我们来看看,进程在生存周期中的各种状态及状态的转换。下面是LINUX系统的进程状态模型的各种状态:

1) 用户状态:进程在用户状态下运行的状态。

2) 内核状态①:进程在内核状态下运行的状态。

3) 内存中就绪:进程没有执行,但处于就绪状态,只要内核调度它,就可以执行。

4) 内存中睡眠:进程正在睡眠并且进程存储在内存中,没有被交换到SWAP设备。

5) 就绪且换出:进程处于就绪状态,但是必须把它换入内存,内核才能再次调度它进行运行。

6) 睡眠且换出:进程正在睡眠,且被换出内存。

7) 被抢先:进程从内核状态返回用户状态时,内核抢先于它,做了上下文切换,调度了另一个进程。原先这个进程就处于被抢先状态。

8) 创建状态:进程刚被创建。该进程存在,但既不是就绪状态,也不是睡眠状态。这个状态是除了进程0以外的所有进程的最初状态。

9) 僵死状态(zombie):进程调用exit结束,进程不再存在,但在进程表项中仍有纪录,该纪录可由父进程收集。

现在我们从进程的创建到退出来看看进程的状态转化。需要说明的是,进程在它的生命周期里并不一定要经历所有的状态。

首先父进程通过系统调用fork来创建子进程,调用fork时,子进程首先处于创建态,fork调用为子进程配置好内核数据结构和子进程私有数据结构后,子进程就要进入就绪态3或5,即在内存中就绪,或者因为内存不够,而导致在SWAP设备中就绪。

假设进程在内存中就绪,这时子进程就可以被内核调度程序调度上CPU运行。内核调度该进程进入内核状态②,再由内核状态返回用户状态执行。该进程在用户状态运行一定时间后,又会被调度程序所调①

② 关于内核态与用户态,请参见附录I。 内核对外的接口是系统调用,内核外的程序都必须经由系统调用才能获得操作系统的服务。进程调度程序属系统调用,因此无论进程获得或让出CPU都要执行进程调度程序,从而在这段时间内运行于内核态。

1东东整理

Linux进程编程

② 同上页②。关于进程调度的详细介绍请参见本章第3节或附录II。 关于进程上下文,请参见

③ 该过程类似于对当前进程上下文进行备份,一旦进程出现了问题,则从备份点重新运行。

2东东整度而进入内核状态,由此转入就绪态①。有时进程在用户状态运行时,也会因为需要内核服务,使用系统调用而进入内核状态,服务完毕,会由内核状态转回用户状态。要注意的是,进程在从内核状态向用户状态返回时可能被抢占,进入状态7,这是由于有优先级更高的进程急需使用 CPU,不能等到下一次调度时机,从而造成抢占。 进程还会因为请求的资源不能得到满足,进入睡眠状态,直到它请求的资源被释放,才会被内核唤醒而进入就绪态。如果进程在内存中睡眠时,内存不足,当进程睡眠时间达到一个阀值,进程会被SWAP出内存,使得进程在SWAP设备上睡眠。这种状况同样可能发生在就绪的进程上。 进程调用exit系统调用,将使得进程进入内核状态,执行exit调用,进入僵死状态而结束。以上就是进程状态转换的简单描述。 进程的上下文②是由用户级上下文、寄存器上下文以及系统级上下文组成。主要内容是该进程用户空间内容、寄存器内容以及与该进程有关的内核数据结构。当系统收到一个中断、执行系统调用或内核做上下文切换时,就会保存进程的上下文。一个进程是在它的上下文中运行的,若要调度进程,就要进行上下文切换。内核在四种情况下允许发生上下文切换: 1) 当进程自己进入睡眠时; 2) 当进程执行完系统调用要返回用户状态,但发现该进程不是最有资格运行的进程时; 3) 当内核完成中断处理后要返回用户状态,但发现该进程不是最有资格运行的进程时; 4) 当进程退出(执行系统调用exit后)时。 有时内核要求必须终止当前进程的执行,立即从先前保存的上下文处执行。这可由setjmp和longjmp实现,setjmp将保存的上下文存入进程自身的数据空间(u区)中,并继续在当前的上下文中执行,一旦碰到了longjmp,内核就从该进程的u区,取出先前保存的上下文,并恢复该进程的上下文为原先保存的。这时内核将使得进程从setjmp处执行,并给setjmp返回1③。 进程因等待资源或其他原因,进入睡眠态是通过内核的sleep算法。该算法与本章后面要讲到的sleep函数是两个概念。算法sleep记录进程原先的处理机优先级,置进程为睡眠态,将进程放入睡眠队列,记录睡眠的原因,给该进程进行上下文切换。内核通过算法wakeup来唤醒进程,如某资源被释放,则唤醒所有因等待该资源而进入睡眠的进程。如果进程睡眠在一个可以接收软中断信号(signal)的级别上,则进程的睡眠可由软中断信号的到来而被唤醒。 1.2 进程控制 现在我们开始讲述一下进程的控制,主要介绍内核对fork、exec、wait、exit的处理过程,为下一节学习这些调用打下概念上的基础,并介绍系统启动(boot)的过程以及进程init的作用。 在Linux系统中,用户创建一个进程的唯一方法就是使用系统调用fork。内核为完成系统调用fork要进行几步操作:

Linux进程编程

3东东 关于索引结点表,请参见附录III。

应该是指进程收到了信号,但并没有设置屏蔽(如用sigprocmask)或注册相应的处理程序(如用sigaction)。

③ SIGCHLD是POSIX标准中定义的,SIGCLD是SysV的事实标准,两个信号想表达的意思一样,只是出处不同,而今大多数UNIX都把两个信号define成同一个值。推荐用SIGCHLD

④ 就是引导块(BootBlock)。 整第一步,为新进程在进程表中分配一个表项。系统对一个用户可以同时运行的进程数是有限制的,对超级用户没有该限制,但也不能超过进程表的最大表项的数目。 第二步,给子进程一个唯一的进程标识号(PID)。该进程标识号其实就是该表项在进程表中的索引号。 第三步,复制一个父进程的进程表项的副本给子进程。内核初始化子进程的进程表项时,是从父进程处拷贝的。所以子进程拥有与父进程一样的uid、euid、gid、用于计算优先权的nice值、当前目录、当前根、用户文件描述符表等。 第四步,把与父进程相连的文件表和索引节点表①的引用数加1。这些文件自动地与该子进程相连。 第五步,内核为子进程创建用户级上下文。内核为子进程的u区及辅助页表分配内存,并复制父进程的u区内容。这样生成的是进程的静态部分。 第六步,生成进程的动态部分,内核复制父进程的上下文的第一层,即寄存器上下文和内核栈,内核再为子进程虚设一个上下文层,这是为了子进程能“恢复”它的上下文。这时,该调用会对父进程返回子进程的pid,对子进程返回0。 Linux系统的系统调用exit,是进程用来终止执行时调用的。进程发出该调用,内核就会释放该进程所占的资源,释放进程上下文所占的内存空间,保留进程表项,将进程表项中纪录进程状态的字段设为僵死状态。内核在进程收到不可捕捉②的信号时,会从内核内部调用exit,使得进程退出。父进程通过 wait得到其子进程的进程表项中记录的计时数据,并释放进程表项。最后,内核使得进程1(init进程)接收终止执行的进程的所有子进程。如果有子进程僵死,就向init进程发出一个SIGCHLD的软中断信号. 一个进程通过调用wait来与它的子进程同步,如果发出调用的进程没有子进程则返回一个错误,如果找到一个僵死的子进程就取子进程的PID及退出时提供给父进程的参数。如果有子进程,但没有僵死的子进程,发出调用的进程就睡眠在一个可中断的级别上,直到收到一个子进程僵死(SIGCLD③)的信号或其他信号。 进程控制的另一个主要内容就是对其他程序引用。该功能是通过系统调用exec来实现的,该调用将一个可执行的程序文件读入,代替发出调用的进程执行。内核读入程序文件的正文,清除原先进程的数据区,清除原先用户软中断信号处理函数的地址,当exec调用返回时,进程执行新的正文。 一个系统启动的过程,也称作是自举的过程。该过程因机器的不同而有所差异。但该过程的目的对所有机器都相同:将操作系统装入内存并开始执行。计算机先由硬件将引导块的内容读到内存并执行,自举块④的程序将内核从文件系统中装入内存,并将控制转入内核的入口,内核开始运行。内核首先初始化它的数据结构,并将根文件系统安装到根“/”,为进程0形成执行环境。设置好进程0的环境后,内核便作为进程0开始执行,并调用系统调用fork。因为这时进程0运行在内核状态,所以新的进程也运行在内核状态。新的进程(进程1)创建自己的用户级上下文,设置并保存好用户寄存器上下文。这时,进程1就从内核状态返回用户状态执行从内核拷贝的代码(exec),并调用exec执行/sbin/init程序。进程1通常称为初始化进程,它负责初始化新的进程。 进程init除了产生新的进程外,还负责一些使用户在系统上注册的进程。例如,进程init一般要产生一些getty的子进程来监视终端。如果一个终端被打开,getty子进程就要求在这个终端上执行一个注册的过程,当成功注册后,执行一个shell程序,来使得用户与系统交互。同时,进程init 执行系统调用wait理

Linux进程编程

② 关于Linux系统启动的详细介绍,请参见。 进程的优先权值(优先数)越小,优先权越高。

4 东东整来监视子进程的死亡,以及由于父进程的退出而产生的孤儿进程的移交。以上是系统启动和进程init的一个粗略的模型①。 1.3 进程调度的概念 Linux系统是一个分时系统,内核给每个进程分一个时间片,该进程的时间片用完就会调度另一个进程执行。LINUX系统上的调度程序属于多级反馈循环调度。该调度方法是,给一个进程分一个时间片,抢先一个运行超过时间片的进程,并把进程反馈到若干优先级队列中的一个队列。进程在执行完之前,要经过这样多次反馈循环。 进程调度分成两个部分,一个是调度的时机,即什么时候调度;一个是调度的算法,即如何调度和调度哪个进程。我们先来看看调度的算法,假设目前内核要求进行调度,调度程序从“在内存中就绪”和“被抢先”状态的进程中选择一个优先权最高的进程,如果有若干优先权一样高的进程,则在其中选择等待时间最长的进程。切换进程上下文,继续执行该进程。如果没有选择到进程,则不做操作,等待下一次调度时机的到来。 每一个进程都有一个用于调度的优先权域。进程的优先权由低到高粗略地分为用户优先权和内核优先权。每种优先权有若干优先权值②(优先数)与其对应。每个优先权都有一个逻辑上与其相连的进程队列。进程从内核状态返回用户状态时被抢先,从而得到用户优先权。进程在内核算法sleep中得到内核优先权。内核优先权高于用户优先权,即内核优先权和用户优先权之间存在一个阀值,所有用户优先权低于该阀值,而内核优先权高于该阀值。内核优先权中又划分为可中断和不可中断,即进程在收到一个软中断信号时,低内核优先权的进程可被唤醒,而有高内核优先权的进程继续睡眠。 计算一个进程优先权的时机是:内核将一个优先权值赋给一个将进入睡眠的进程,这个优先权值是固定的,且与睡眠原因相联系;另一个时机是,时钟处理程序每隔一定时间(如每隔1秒)调整用户状态下的所有进程的优先权,并使内核运行调度算法。时钟处理程序还根据一个衰减函数,每秒一次的调整每个进程的最近CPU使用时间。例如可按如下公式调整: decay(CPU) = CPU/2; 再根据公式重新计算在“就绪”和“被抢先”状态下的每个进程的优先权值。 Priority = (“recent CPU usage”/constant) + (base priority) + (nice value); 其中constant是个系统常量(一般取值为“2”)。base priority值也是系统的一个常量,一般base priority取值为60。最后,nice的值是由进程发出nice调用时给出的值,这样就可以使得用户通过降低优先权而让出一些执行时间。只有超级用户才能指定提高优先权的nice值。 理

Linux进程编程

第二章 进程的基本操作

摘要:本节先介绍一些关于进程的基本操作,通过本节,我们将了解如何产生子进程,进程如何改变它的执行映像,父子进程的同步等操作。由此也了解到一些并行程序的基本概念与如何编制简单的并行程序。

2. 进程的一般操作

上一节介绍了一些有关进程的基本概念,从这一节开始要结合一些例子来阐述一些有关进程的系统调用。本节先介绍一些关于进程的基本操作,通过本节,我们将了解如何产生子进程,进程如何改变它的执行映像,父子进程的同步等操作。由此也了解到一些并行程序的基本概念与如何编制简单的并行程序。

2.1 fork 系统调用

系统调用fork是用来创建一个子进程。创建的过程前面一节已经介绍过。现在,再介绍一个系统调用vfork,这个调用的产生是因为认识到创建子进程时对父进程的所有页不进行拷贝能带来性能上的改善。该调用假定进行vfork调用后,将立即调用exec,这样就不需要拷贝父进程的所有页表。因为它不拷贝页表,

有些系统的fork也采用了其他方法来提高性能,比较典型的一种是增加“写时拷贝”。所以比fork调用快①。

这种fork调用,产生子进程时,并不拷贝父进程的所有页面,而是置父进程所有页面的写时拷贝位,子进程共享父进程的所有页面。直到父进程或子进程写某个页面时,就会发生一个保护性错误,并拷贝该页面。这样不仅提高了内核的性能,而且改善了内存的利用。

系统调用fork和vfork的声明格式如下:

pid_t fork(void);

pid_t vfork(void);

在使用该系统调用的程序中要加入以下头文件:

#include <unistd.h>

当调用执行成功时,该调用对父进程返回子进程的PID,对子进程返回0。调用失败时,给父进程返回-1,没有子进程创建。

下面是发生错误时,可能设置的错误代码errno:

EAGAIN:系统调用fork不能得到足够的内存来拷贝父进程页表。或用户是超级用户但进程表满,或者用户不是超级用户但达到单个用户能执行的最大进程数。

ENOMEM:对创建新进程来说没有足够的空间,该错误是指没有足够的空间分配给必要的内核结构。

① 事实上,fork与vfork的基本区别在于,当使用vfork创建新进程时,父进程将被暂时阻塞,而子进程则可以借用父进程的地址空间。这个奇特状态将持续直到子进程要么退出,要么调用exec,至此父进程才继续执行。这意味著一个有vfork创建的子进程必须小心以免出乎意料的改变父进程的变量,也一定不要从包含vfork调用的函数中返回或调用exit。 5东东整理

Linux进程编程

6东东下面我们看一个fork调用的简单的例子。该例子产生一个子进程,父进程打印出自己和子进程的PID,子进程打印出自己的PID和父进程的PID。 注意:父进程打开了一个文件。父子进程都可以对该文件操作,该程序父子进程都向文件中写入了一行。 #include <sys/types.h> #include <sys/stat.h> #include <sys/uio.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <sys/wait.h> extern int errno; int main() { char buf[100]; pid_t cld_pid; int fd; int status; if ((fd=open("temp",O_CREAT|O_TRUNC|O_RDWR,S_IRWXU)) == -1) { printf("open error %d",errno); exit(1); } strcpy(buf,"This is parent process write"); if ((cld_pid=fork()) == 0) { /* 这里是子进程执行的代码 */ strcpy(buf,"This is child process write"); printf("This is child process"); printf("My PID(child) is %d",getpid()); /*打印出本进程的ID*/ printf("My parent PID is %d",getppid()); /*打印出父进程的ID*/ write(fd,buf,strlen(buf)); close(fd); exit(0); } else

{ /* 这里是父进程执行的代码 */

printf("This is parent process");

printf("My PID(parent) is %d",getpid()); /*打印出本进程的ID */

printf("My child PID is %d",cld_pid); /*打印出子进程的ID*/ 整理

Linux进程编程

7东东 下面我们看一下,程序运行的结果,假设源文件命名为fork.c: [root@wapgw /root]# gcc -o fork fork.c [root@wapgw /root]# ./fork This is parent process This is child process My PID(child) is 5258 My parent PID is 5257 My PID(parent) is 5257 My child PID is 5258 [root@wapgw /root]# 从上面的运行结果可以看出进程的调度,父进程打印出第一行后,CPU调度子进程,打印出后续的三行,子进程结束,调度父进程执行(其中可能还有其他的进程被调度),父进程执行完,将控制返还给shell程序,最后一行是shell程序输出的提示符。 看看temp文件里有什么内容 [root@wapgw /root]# more temp This is child process write This is parent process write [root@wapgw /root]# 现在我们将程序稍作修改。将wait调用注释掉,我们看看会有什么样的结果。因为调度的原因,多执行几次,你会看到如下的结果: [root@wapgw /root]#vi fork.c //将wait调用注释掉 [root@wapgw /root]# gcc -o fork fork.c [root@wapgw /root]# ./fork This is parent process This is child process My PID(parent) is 5282 My child PID is 5283

[root@wapgw /root]# My PID(child) is 5283

My parent PID is 1

[root@wapgw /root]#

write(fd,buf,strlen(buf)); close(fd); } wait(&status); /* 如果此处没有这一句会如何?*/ return 0; } 整理

Linux进程编程

8东东 第一行是父进程的输出,第二行是子进程的输出,第三、四行是父进程的输出,这时父进程由于没有wait调用,不等待子进程而结束。下面一行中的 “[root@wapgw /root]#”是父进程结束,将控制返回给shell时,shell输出的提示符。然后CPU调用子进程,输出子进程的PID是5283。注意,下面子进程输出其父进程的ID是1,因为它的父进程结束了,内核将它交给了进程1(进程init)来管理,这个过程见前面一节。这里要提一下的是,输出结果的顺序和进程调度的顺序有关,自己试验的结果与例子中的顺序很可能不同,请自行分析。(从我的系统给出的结果来看,加不加wait 都一样,都先执行完子进程,后执行父进程,不过用管道从父进程向子进程传消息,子进程也可以正常收到,看来现在内核调度比较智能,具体调度顺序有待于进一步研究) 2.2 exec 系统调用 系统调用exec是用来执行一个可执行文件来代替当前进程的执行映像。需要注意的是,该调用并没有生成新的进程,而是在原有进程的基础上,替换原有进程的正文,调用前后是同一个进程,进程号PID不变。但执行的程序变了(执行的指令序列改变了)。它有六种调用的形式,随着系统的不同并不完全与以下介绍的相同。它们的声明格式如下: int execl( const char *path, const char *arg, ...); int execlp( const char *file, const char *arg, ...); int execle( const char *path, const char *arg , ..., char* const envp[]); int execv( const char *path, char *const argv[]); int execve( const char *path, char *const argv [], char *const envp[]); int execvp( const char *file, char *const argv[]); 在使用这些系统调用的程序中要加入以下头文件和外部变量: #include <unistd.h> extern char **environ; 下面我们先详细讲述其中的一个,然后再给出它们之间的区别。在系统调用execve中,参数path是将要执行的文件,参数argv是要传递给文件的参数,参数envp是要传递给文件的环境变量。当参数path所指的文件替换原进程的执行映像后,文件path开始执行,参数argv和envp便传递给进程。下面我们举一个简单的例子。 在讲述系统调用execve的例子之前,我们先来看看环境变量。为了使用户方便和灵活地使用Shell,LINUX引入了环境的概念。环境是一些数据,用户可以改变这些数据,增加新的数据或删除一些数据。这些数据称为环境变量。因为它们定义了用户的工作环境,同时又可以被修改。每个用户都可以有自己不同的环境变量,用户可以用env命令(不带参数)浏览环境变量,输出的格式和变量名随着 Shell的不同和系统配置的不同而不同。下面这个例子打印出传递给该进程的所有参数和环境变量: #include <stdio.h>

#include <unistd.h>

extern char **environ;

int main(int argc,char* argv[])

{ 整理

Linux进程编程

9东东 int i; printf("Argument:\n"); for (i=0;i<=argc;i++) printf("Arg%d is: %s\n",i,argv[i]); printf("Environment:\n"); for (i=0;environ[i]!=NULL;i++) printf("%s\n",environ[i]); } 下面是执行时的屏幕拷贝: [root@wapgw /root]# gcc -o example example.c [root@wapgw /root]# ./example test Argument: Arg0 is ./example Arg1 is test Environment: PWD=/root REMOTEHOST=cjm HOSTNAME=wapgw HOME=/root …… SSH_ASKPASS=/usr/libexec/ssh/gnome-ssh-askpass PATH=/usr/local/sbin:/usr/sbin:/sbin:/usr/local/sbin:/usr/local/bin: /sbin:/bin:/usr/sbin:/usr/bin:/usr/X11R6/bin:/root/bin [root@wapgw /root]# 其中Environment后的都是环境变量及其取值。下面我们来看看execve的一个简单的例子: #include <unistd.h> #include <stdio.h> extern char **environ; int main(int argc,char* argv[]) { printf("Will replace by another image"); execve("example",argv,environ); /* 用上面的例子example替换进程执行映像 */ printf("process never go to here"); /* 进程永远不会执行到这里 */ }

该程序用自己的参数argv和环境变量传递给新的执行映像。执行结果的屏幕拷贝如下:

[root@wapgw /root]# gcc -o execve execve.c

[root@wapgw /root]# ./execve these args will dend to example 整理

Linux进程编程

① 标志参数为FD_CLOEXEC。很多现有的涉及文件描述符标志的程序并不使用常量FD_CLOEXEC,而是将此标志设置为0(系统默认,在exec时不关闭)或1(在exec时关闭)。

10东东 Will replace by another image Argument: Arg0 is ./execve Arg1 is these Arg2 is args Arg3 is will Arg4 is dend Arg5 is to Arg6 is example Environment: PWD=/root REMOTEHOST=cjm HOSTNAME=wapgw HOME=/root …… SSH_ASKPASS=/usr/libexec/ssh/gnome-ssh-askpass PATH=/usr/local/sbin:/usr/sbin:/sbin:/usr/local/sbin:/usr/local/bin: /sbin:/bin:/usr/sbin:/usr/bin:/usr/X11R6/bin:/root/bin [root@wapgw /root]# 这里要注意的是,如果你用execve some args > screen时,会发现输出重定向到一个文件后,丢失了数据(即少了输出的第一行Will replace …)。这是因为将输出重定向到一个文件后,进程的第一行是输出到文件,所以被缓冲还没有真正写入文件,进程的第二行替换进程的执行映像,也覆盖了文件的缓冲。这个问题可以通过fflush(stdout)刷新stdout的缓冲区或者用setbuf(stdout,NULL)设置stdout的缓冲为空来解决。 如果对某个文件描述符fd设置了close-on-exec①标志,那么在exec调用后,该文件描述符被关闭。下面我们看一个简单的例子: 这里有一个程序pp.c: #include <stdio.h> int main() { printf("test"); } 它是用来替换进程执行图像的。再来看看下面的程序: #include <unistd.h> #include <fcntl.h>

#include <stdio.h>

extern char **environ; 整理

Linux进程编程

② 关于fctnl系统调用的详细介绍,请参见。 stdin, stdout和stderr的文件描述符分别是0,1,和2

11东东 int main(int argc,char* argv[]) { printf("close-on-exec is %d",fcntl(1,F_GETFD)); fcntl(1,F_SETFD,16); printf("close-on-exec is %d",fcntl(1,F_GETFD)); execve("pp",argv,environ); printf("AH!!!!!"); } 该程序的执行结果为: [root@wapgw /root]# ./fcntl close-on-exec is 0 close-on-exec is 0 test [root@wapgw /root]# 这是没有设置close-on-exec标志的结果,将fcntl①语句改为 fcntl(1,F_SETFD,25); 对于最后一个参数,只要保证该参数的最低位(二进制)是1就可以。这时的执行结果为: [root@wapgw /root]# ./fcntl close-on-exec is 0 close-on-exec is 1 [root@wapgw /root]# 这时,系统调用execve用pp替换原进程的执行图像,但由于文件描述符1(stdout②)被关闭,所以执行完execve调用后无输出。 系统调用execve可以执行二进制的可执行文件(如a.out)。也可以执行shell程序,该shell程序必须以下面所示的格式开头,即第一行为:#! interpreter [arg]。其中interpreter可以是shell或其他解释器,例如:#!/bin/bsh或#!/usr/bin/perl。其中的arg是传递给解释器的参数。 该系统调用成功时,不会返回任何值(因为进程的执行映像已经被替换,没有接收返回值的地方了)。如果有任何返回值(一般是-1),就代表有错误发生,内核将设置相应的错误代码errno,下面是一些可能设置的错误代码: EACCES:指向的文件或脚本文件不是普通文件;指向的文件或脚本文件没有设置可执行位;文件系统被安装成noexec;指向的文件或脚本文件所处的路径中有目录不能搜索(没有execute权)。

E2BIG:传递的参数清单太大。 整理

Linux进程编程

① exit()其实调用的也是_exit()。_exit()立即中止当前工作,exit()则要做一些结束工作然后再调用_exit()回到系统内核。‘exit()’与‘_exit()’的基本区别在于前一个函数实施与调用库里用户状态结构有关的清除工作,而且调用用户自定义的清除程序;后一个函数只为进程实施内核清除工作。在由‘fork()’创建的子进程分支里,正常情况下使用‘exit()’是不正确的,这是因为使用它会导致标准输入输出的缓冲区被清空两次,而且临时文件被出乎意料的删除。

12东东 ENOEXEC:指定的文件确实有执行权限位,但是为即不可识别的执行文件格式。 ETXTBUSY:指定文件被一个或多个进程以可写入的方式打开。 EIO:从文件系统读入时发生I/O错误。 现在我们来看看这一族系统调用。在系统调用execl、execlp、execle中,参数是以arg0、arg1、arg2、…的方式传递的。按照惯例,arg0应该是要执行的程序名。在调用execl、execlp中环境变量的值是自动传递的,即不用象调用execve、execle那样在调用中指定参数envp。在调用execve、execv、execvp中参数是以数组的方式传递的。另一个区别是,调用execlp、execvp可以在环境变量PATH定义的路径中查找执行程序。例如,PATH定义为“/bin:/usr/bin:/usr/sbin”,如果调用指定执行文件名为test,那么这两个调用会在PATH定义的三个目录中查找名为test的可执行文件。 系统调用exec和fork经常结合使用,父进程fork一个子进程,在子进程中调用exec来替换子进程的执行映像,并发的执行一些操作。 2.3 exit系统调用 系统调用exit的功能是终止发出调用的进程。它的声明格式如下: void _exit(int status); 在使用这个系统调用的程序中要加入以下头文件: #include <unistd.h> 系统调用_exit①立即终止发出调用的进程。所有属于该进程的文件描述符都关闭。该进程的所有子进程由进程1(进程init)接收,并对该进程的父进程发出一个SIGCHLD(子进程僵死)的信号。参数status作为退出的状态值返回父进程,该值可以通过系统调用wait来收集。返回状态码status只有最低一个字节有效。如果进程是一个控制终端进程,则SIGHUP信号将被送往该控制终端的前台进程。系统调用_exit从不返回任何值给发出调用的进程;也不刷新I/O缓冲,如果要自动完成刷新,可以用函数调用exit。 2.4 wait 系统调用 系统调用wait的功能是发出调用的进程只要有子进程,就睡眠直到它们中的一个终止为止。该调用声明的格式如下: pid_t wait(int *status) pid_t waitpid(pid_t pid, int *status, int options); 在使用这些系统调用的程序中要加入以下头文件:

#include <sys/types.h> 整理

Linux进程编程

13东东 #include <sys/wait.h> 发出wait调用的进程进入睡眠直到它的一个子进程退出时或收到一个不能被忽略的信号时被唤醒。如果调用发出时,已经有退出的子进程(这时子进程的状态是僵死状态),该调用立即返回。其中调用返回时参数status中包含子进程退出时的状态信息。 调用waitpid与调用wait的区别是waitpid等待由参数pid指定的子进程退出。其中参数pid的含义与取值方法如下: 1) 参数pid < -1时,当退出的子进程满足下面条件时结束等待:该子进程的进程组ID(process group)等于绝对值的pid这个条件。 2) 参数pid = 0时,等待任何满足下面条件的子进程退出:该子进程的进程组ID等于发出调用进程的进程组ID。 3) 参数pid > 0时,等待进程ID等于参数pid的子进程退出。 4) 参数pid = -1时,等待任何子进程退出,相当于调用wait。 对于调用waitpid中的参数options的取值及其含义如下: WNOHANG:该选项要求如果没有子进程退出就立即返回。 WUNTRACED:对已经停止但未报告状态的子进程,该调用也从等待中返回和报告状态。 如果status不是空,调用将使status指向该信息。下面的宏可以用来检查子进程的返回状态。前面三个用来判断退出的原因,后面三个是对应不同的原因返回状态值: 1) WIFEXITED(status):如果进程通过系统调用_exit或函数调用exit正常退出,该宏的值为真。 2) WIFSIGNALED(status):如果子进程由于得到的信号(signal)没有被捕捉而导致退出时,该宏的值为真。 3) WIFSTOPPED(status):如果子进程没有终止,但停止了并可以重新执行时,该宏返回真。这种情况仅出现在waitpid调用中使用了WUNTRACED选项。 4) WEXITSTATUS(status):如果WIFEXITED(status)返回真,该宏返回由子进程调用_exit(status)或exit(status)时设置的调用参数status值。 5) WTERMSIG(status):如果WIFSIGNALED(status)返回为真,该宏返回导致子进程退出的信号(signal)的值。 6) WSTOPSIG(status):如果WIFSTOPPED(status)返回真,该宏返回导致子进程停止的信号(signal)值。 该调用返回退出的子进程的PID;或者发生错误时返回-1;或者设置了WNOHANG选项没有子进程退出就返回0;发生错误时,可能设置的错误代码如下: ECHILD:该调用指定的子进程pid不存在,或者不是发出调用进程的子进程。 EINVAL:参数options无效。

ERESTARTSYS:WNOHANG没有设置并且捕获到SIGCHLD或其它未屏蔽信号。

关于wait调用的例子,前面在介绍fork调用时,就有了简单的应用。此处不再举例。

整理

Linux进程编程

注意:子进程退出(SIGCHLD)信号设置不同的处理方法,会导致该调用不同的行为,详细情况见Linux信号处理机制。

2.5 sleep 函数调用

函数调用sleep①可以用来使进程挂起指定的秒数。该函数调用的声明格式如下:

unsigned int sleep(unsigned int seconds)

在使用这个函数调用的程序中加上以下的头文件:

#include <unistd.h>

该函数调用使得进程挂起一个指定的时间,直到指定时间用完或者收到信号。系统的活动对指定的时间有一定的影响。Linux系统是用SIGALRM实现的,在Linux系统里,sleep函数不能和alarm()调用混用。

如果指定挂起的时间到了,该调用返回0;如果该函数调用被信号所打断,则返回剩余挂起的时间数(指定的时间减去已经挂起的时间)。

① 更精确的休眠时间使用usleep,单位是微秒。

14东东整理

Linux进程编程

第三章 进程的特殊操作

摘要:本节要介绍一些有关进程的特殊操作。有了这些操作,就使得进程的编程更加完善,能编制更为实用的程序。主要的内容有得到关于进程的各种ID、对进程的设置用户ID、改变进程的工作目录、改变进程的根、改变进程的优先权值等操作。

3.进程的特殊操作

上一节介绍了有关进程的一些基本操作,如进程的产生(fork)、进程的终止(exit)、进程执行映像的改变(exec)、等待子进程终止(wait)等。本节要介绍一些有关进程的特殊操作。有了这些操作,就使得进程的编程更加完善,能编制更为实用的程序。

主要的内容有得到关于进程的各种ID、对进程的设置用户ID、改变进程的工作目录、改变进程的根、改变进程的优先权值等操作。

3.1 获得进程相关的ID

与进程相关的ID有:

1) 真正用户标识号(UID):该标识号负责标识运行进程的用户。

2) 有效用户标识号(EUID):该标识号负责标识以什么用户身份来给新创建的进程赋所有权、检查文件的存取权限和检查通过系统调用kill向进程发送软中断信号的许可权限。

3) 真正用户组标识号(GID):负责标识运行进程的用户所属的组ID。

4) 有效用户组标识号(EGID):用来标识目前进程所属的用户组。可能因为执行文件设置set-gid位而与gid不同。

5) 进程标识号(PID):用来标识进程。

6) 进程组标识号(process group ID):一个进程可以属于某个进程组。可以发送信号给一组进程。注意,它不同与gid。前面的系统调用wait中指定参数pid时,就用到了进程组的概念。

如果要获得进程的用户标识号,用getuid调用。调用geteuid是用来获得进程的有效用户标识号。有效用户ID与真正用户ID的不同是由于执行文件设置set-uid位引起的。这两个调用的格式如下:

uid_t getuid(void);

uid_t geteuid(void);

在使用这两个调用的程序中加入下列头文件:

#include <unistd.h>

#include <sys/types.h>

如果要获得运行进程的用户组ID,使用getgid调用来获得真正的用户组ID,用getegid获得有效的用户组ID。标识gid与egid的不同是由于执行文件设置set-gid位引起的。这两个调用的格式如下:

15东东整理

Linux进程编程

16东东 gid_t getgid(void); gid_t getegid(void); 在使用这两个调用的程序中加入下列头文件: #include <unistd.h> #include <sys/types.h> 如果要获得进程的ID,使用getpid调用;要获得进程的父进程的ID,使用getppid调用。这两个调用的格式如下: pid_t getpid(void); pid_t getppid(void); 在使用这两个调用的程序中加入下列头文件: #include <unistd.h> 如果要获得进程所属组的ID,使用getpgrp调用;若要获得指定PID进程所属组的ID用getpgid调用。这两个调用的格式如下: pid_t getpgrp(void); pid_t getpgid(pid_t pid); 在使用这两个调用的程序中加入下列头文件: #include <unistd.h> 注意一下gid和pgrp的区别,一般执行该进程的用户的组ID就是该进程的gid,如果该执行文件设置了set_gid位,则文件所属的组ID就是该进程的gid。对于进程组ID,一般来说,一个进程在shell下执行,shell程序就将该进程的PID赋给该进程的进程组ID,从该进程派生的子进程都拥有父进程所属的进程组ID,除非父进程将子进程的所属组ID设置成与该子进程的PID一样。由于这几个调用使用很简单,这里就不再举例。 3.2 setuid 和 setgid 系统调用 前面讲述了如何得到uid和gid,现在来看看如何设置它们。在讲述这两个调用以前我们先来看看对文件设置set_uid位会有什么作用。我们先编了一个小程序来做试验。这个程序的作用是,打印出进程的uid和euid,然后打开一个名为tty.c的文件。如果打不开,就显示错误代码;如果打开了,就显示打开成功。假设该程序名叫uid_test.c:

/* uid_test.c */

#include <sys/types.h>

#include <sys/stat.h> 整理

Linux进程编程

17东东

#include <fcntl.h> #include <unistd.h> #include <sys/types.h> #include <errno.h> extern int errno; int main() { int fd; printf("This process's uid = %d, euid = %d ",getuid(),geteuid()); if ((fd = open("tty.c",O_RDONLY))==-1) { printf("Open error, errno is %d ",errno); exit(1); } else { printf("Open success "); } } 下面列出这几个文件的目录,可以看到文件tty.c的存取许可权仅为属主root可读写。 [wap@wapgw /tmp]$ ls -l total 3 -rw------- 1 root root 0 May 31 16:15 tty.c -rwxr-xr-x 1 root root 14121 May 31 16:15 uid_test -rw-r--r-- 1 root root 390 May 31 16:15 uid_test.c [wap@wapgw /tmp]$ 在该系统的用户中有个用户wap(500),以root用户身份执行程序: [root@wapgw /tmp]# ./uid_test This process's uid = 0, euid = 0 Open success [root@wapgw /tmp]# 下面使用su命令,转到用户wap下,执行程序 [root@wapgw /tmp]#su wap [wap@wapgw /tmp]$ ./uid_test This process's uid = 500, euid = 500 Open error, errno is 13 [wap@wapgw /tmp]$ 整理

Linux进程编程

② 使可执行文件具有与被操作文件属主同样的权力。关于更改文件权限的命令chmod的详细介绍,请参见。 u区(u Area),用于存放进程表项的一些扩充信息。每一个进程都有一个私用的u区,其中含有:进程表项指针、真正用户标识符u-ruid(real user ID)、有效用户标识符u-euid(effective user ID)、用户文件描述符表、计时器、内部I/O参数、限制字段、差错字段、返回值、信号处理数组。

18东东 这是由于进程的uid是500(wap),对文件tty.c没有存取权,所以出错。 给程序文件设置set-uid位 [root@wapgw /tmp]# chmod 4755① uid_test 再转到用户wap下,执行程序uid_test。 [wap@wapgw /tmp]$ ./uid_test This process's uid = 500, euid = 0 Open success [wap@wapgw /tmp]$ 从上面我们看到,进程打印出的euid是0(root),而运行该进程的用户是500(wap)。由于进程的euid是root,所以成功打开了文件tty.c。 上面的例子说明了两个事实:第一,内核对进程存取文件的许可权的检查,是通过检查进程的有效用户ID来实现的;第二,执行一个设置set_uid位的程序时,内核将进程表项中和u区②中的有效用户ID设置为文件属主的ID。为了区别进程表项中的euid和u区中的euid,我们将进程表项中的euid 域称为保存用户标识号(saved user ID)。 下面我们来看看这两个调用。调用的声明格式如下: int setuid(uid_t uid); int setgid(gid_t gid); 在使用这两个调用的程序中加入下面的头文件: #include <unistd.h> 调用setuid为当前发出调用的进程设置真正和有效用户ID。参数uid是新的用户标识号(该标识号应该在/etc/passwd文件中存在)。如果发出调用的进程的有效用户ID(u区中的)是超级用户,内核将进程表中和u区中的真正用户标识号和有效用户标识号置为参数uid。如果发出调用的进程的有效用户 ID不是超级用户,那么内核将根据指定的参数uid来执行,如果这时指定的参数uid的值是真正用户标识号或者是保存用户标识号(saved user ID),则内核将u区中的有效用户标识号改为参数uid,否则,该调用返回错误。该调用成功时,返回值为0;发生错误时,返回-1,并设置相应的错误代码 errno,下面是经常可能发生的错误代码: EPERM:用户不是超级用户,并且指定的参数uid与发出调用的进程的真正用户ID或保存用户ID不匹配。 整理

Linux进程编程

19东东 调用setgid设置当前发出调用的进程的真正、有效用户组ID。该调用允许进程指定进程的用户组ID为参数gid,如果进程的有效用户ID不是超级用户,该参数gid必须等于真正用户组ID、有效用户组ID中的一个。如果进程的有效用户ID是超级用户,可以指定任何存在的用户组ID(在 /etc/group文件中存在)。 注意: 对于setuid程序尤其要小心,当进程的euid是超级用户时,如果将进程setuid到其他用户,就无法再得到超级用户的权力。我们可能这样用这个调用,某个程序,开始需要root权力才能完成开始的工作,但后续的工作不需要root的权力,所以,我们将程序的执行文件设置set_uid位,并使得执行文件的属主是root,这样进程开始时,就具有了root的权限,在不再需要root权限的地方,用setuid(getuid)恢复进程的uid、 euid。对于可执行文件设置set_uid位,一定要注意,尤其是对那些属主是root的更要注意。因为LINUX系统中root拥有任何权力。使用不当,会对系统安全有极大的损害。 3.3 setpgrp和setpgid 系统调用 这两个调用是用来设置进程组ID的,其声明格式如下: int setpgrp(void); int setpgid(pid_t pid, pid_t pgid); 在使用这两个调用的程序中加入下面的头文件: #include <unistd.h> 调用setpgrp用来将发出调用的进程的进程组ID设置成与该进程的PID相等。注意,以后由这个进程派生的子进程都拥有该进程组ID(除非修改子进程的进程组ID)。 调用setpgid用来将进程号为参数pid的进程的进程组ID设定为参数pgid。如果参数pid为0,则修改发出调用进程的进程组ID。如果参数pgid为0,将进程号为pid的进程改为与发出调用的进程同组。如果不是超级用户发出的调用,那么被指定的进程必须与发出调用的进程有相同的EUID,或者被指定的进程是发出调用进程的子进程。 进程组可用于信号的发送,或者终端输入的仲裁(与终端控制进程有相同的进程组ID且在前台可以读取终端,其他进程在企图读的时候被阻塞并发送信号给该进程)。 该调用成功时,返回值为0;如果请求失败,返回-1,并设置全局变量errno为对应的值。下面是可能遇到的错误代码: ESRCH:参数pid指定的进程不存在。 EINVAL:参数pgid小于0。

EPERM:指定进程的EUID与发出调用进程的euid不同,且指定进程不是发出调用进程的子进程。

3.4 chdir 系统调用

整理

Linux进程编程

20东东 chdir是用来将进程的当前工作目录改为由参数指定的目录。该调用的声明格式如下: int chdir(const char *path); 在使用该调用的程序中加入下面的头文件: #include <unistd.h> 使用该调用时要注意,发出该调用的进程必须对参数path指定的目录有搜索(execute)的权力。调用成功时,返回值为0;错误时,返回-1,并设置相应的错误代码。 3.5 chroot 系统调用 系统调用chroot用来改变发出调用进程的根(“/”)目录。该调用声明的格式如下: int chroot(const char *path); 在使用该调用的程序中加入下面的头文件: #include <unistd.h> 调用chroot将进程的根目录改到由参数path所指定的地方。以后该进程中以“/”(根)开始的路径,都从指定目录处开始查找。发出调用进程的子进程都继承这个根目录的位置。该调用只能由超级用户(root)发出。注意,该调用并不改变当前工作目录,所以有可能当前工作目录“.”在根目录“/” 之外。调用成功时,返回值为0;错误时,返回-1,并设置相应的错误代码。 注意: 如果用chroot调用改变根后,不能由调用chroot(“/”)来返回真正的根,因为调用中的参数“/”该调用一般可以用在 login程序中,或者现在国内常见的BBS系统等应用程序中,会被理解成新设置的根。用户登录后执行系统的一个程序,该程序将根改变成用户登录的目录(例如 /home/bbs)。这样使用的好处是,利于调试和安装;也利于安全。 3.6、nice 系统调用 系统调用nice用来改变进程的优先权。该调用的声明格式如下: int nice(int inc); 在使用该调用的程序中加入下面的头文件: #include <unistd.h>

调用nice将发出调用进程的优先权值增加inc大小。只有超级用户才有权指定一个负的增加量inc。发出调用的进程的子进程都继承该优先权。注意,进程的优先权值越低,优先权越高,即优先权值越低,调度上CPU的机会越大。所以只有root才能指定负值,一般用户只能指定正值,该值降低了进程的优先权,整理

本文来源:https://www.bwwdw.com/article/v7h1.html

Top