进程控制开发之:Linux进程控制编程
扫描二维码
随时随地手机看文章
在Linux中创建一个新进程的惟一方法是使用fork()函数。fork()函数是Linux中一个非常重要的函数,和读者以往遇到的函数有一些区别,因为它看起来执行一次却返回两个值。难道一个函数真的能返回两个值吗?希望读者能认真地学习这一部分的内容。
(1)fork()函数说明。
fork()函数用于从已存在的进程中创建一个新进程。新进程称为子进程,而原进程称为父进程。使用fork()函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间,包括进程上下文、代码段、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端等,而子进程所独有的只有它的进程号、资源使用和计时器等。
因为子进程几乎是父进程的完全复制,所以父子两个进程会运行同一个程序。因此需要用一种方式来区分它们,并使它们照此运行,否则,这两个进程不可能做不同的事。
实际上是在父进程中执行fork()函数时,父进程会复制出一个子进程,而且父子进程的代码从fork()函数的返回开始分别在两个地址空间中同时运行。从而两个进程分别获得其所属fork()的返回值,其中在父进程中的返回值是子进程的进程号,而在子进程中返回0。因此,可以通过返回值来判定该进程是父进程还是子进程。
同时可以看出,使用fork()函数的代价是很大的,它复制了父进程中的代码段、数据段和堆栈段里的大部分内容,使得fork()函数的系统开销比较大,而且执行速度也不是很快。
(2)fork()函数语法。
表7.2列出了fork()函数的语法要点。
表7.2 fork()函数语法要点
所需头文件
#include<sys/types.h>//提供类型pid_t的定义
#include<unistd.h>
函数原型
pid_tfork(void)
函数返回值
0:子进程
子进程ID(大于0的整数):父进程
-1:出错
(3)fork()函数使用实例。
/*fork.c*/
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
intmain(void)
{
pid_tresult;
/*调用fork()函数*/
result=fork();
/*通过result的值来判断fork()函数的返回情况,首先进行出错处理*/
if(result==-1)
{
printf("Forkerrorn");
}
elseif(result==0)/*返回值为0代表子进程*/
{
printf("Thereturnedvalueis%dn
Inchildprocess!!nMyPIDis%dn",result,getpid());
}
else/*返回值大于0代表父进程*/
{
printf("Thereturnedvalueis%dn
Infatherprocess!!nMyPIDis%dn",result,getpid());
}
returnresult;
}
将可执行程序下载到目标板上,运行结果如下所示:
$arm-linux-gccfork.c–ofork(或者修改Makefile)
$./fork
Thereturnedvalueis76/*在父进程中打印的信息*/
Infatherprocess!!
MyPIDis75
Thereturnedvalueis:0/*在子进程中打印的信息*/
Inchildprocess!!
MyPIDis76
从该实例中可以看出,使用fork()函数新建了一个子进程,其中的父进程返回子进程的PID,而子进程的返回值为0。
(4)函数使用注意点。
fork()函数使用一次就创建一个进程,所以若把fork()函数放在了ifelse判断语句中则要小心,不能多次使用fork()函数。
小知识
由于fork()完整地复制了父进程的整个地址空间,因此执行速度是比较慢的。为了加快fork()的执行速度,有些UNIX系统设计者创建了vfork()。vfork()也能创建新进程,但它不产生父进程的副本。它是通过允许父子进程可访问相同物理内存从而伪装了对进程地址空间的真实拷贝,当子进程需要改变内存中数据时才复制父进程。这就是著名的“写操作时复制”(copy-on-write)技术。
现在很多嵌入式Linux系统的fork()函数调用都采用vfork()函数的实现方式,实际上uClinux所有的多进程管理都通过vfork()来实现。
2.exec函数族(1)exec函数族说明。
fork()函数是用于创建一个子进程,该子进程几乎复制了父进程的全部内容,但是,这个新创建的进程如何执行呢?这个exec函数族就提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新的进程替换了。另外,这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行的脚本文件。
在Linux中使用exec函数族主要有两种情况。
n 当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用exec函数族中的任意一个函数让自己重生。
n 如果一个进程想执行另一个程序,那么它就可以调用fork()函数新建一个进程,然后调用exec函数族中的任意一个函数,这样看起来就像通过执行应用程序而产生了一个新进程(这种情况非常普遍)。
(2)exec函数族语法。
实际上,在Linux中并没有exec()函数,而是有6个以exec开头的函数,它们之间语法有细微差别,本书在下面会详细讲解。
下表7.3列举了exec函数族的6个成员函数的语法。
表7.3 exec函数族成员函数语法
所需头文件
#include<unistd.h>
函数原型
intexecl(constchar*path,constchar*arg,...)
intexecv(constchar*path,char*constargv[])
intexecle(constchar*path,constchar*arg,...,char*constenvp[])
intexecve(constchar*path,char*constargv[],char*constenvp[])
intexeclp(constchar*file,constchar*arg,...)
intexecvp(constchar*file,char*constargv[])
函数返回值
-1:出错
这6个函数在函数名和使用语法的规则上都有细微的区别,下面就可执行文件查找方式、参数表传递方式及环境变量这几个方面进行比较。
n 查找方式。
读者可以注意到,表7.3中的前4个函数的查找方式都是完整的文件目录路径,而最后2个函数(也就是以p结尾的两个函数)可以只给出文件名,系统就会自动按照环境变量“$PATH”所指定的路径进行查找。
n 参数传递方式。
exec函数族的参数传递有两种方式:一种是逐个列举的方式,而另一种则是将所有参数整体构造指针数组传递。
在这里是以函数名的第5位字母来区分的,字母为“l”(list)的表示逐个列举参数的方式,其语法为char*arg;字母为“v”(vertor)的表示将所有参数整体构造指针数组传递,其语法为*constargv[]。读者可以观察execl()、execle()、execlp()的语法与execv()、execve()、execvp()的区别。它们具体的用法在后面的实例讲解中会具体说明。
这里的参数实际上就是用户在使用这个可执行文件时所需的全部命令选项字符串(包括该可执行程序命令本身)。要注意的是,这些参数必须以NULL表示结束,如果使用逐个列举方式,那么要把它强制转化成一个字符指针,否则exec将会把它解释为一个整型参数,如果一个整型数的长度char*的长度不同,那么exec函数就会报错。
n 环境变量。
exec函数族可以默认系统的环境变量,也可以传入指定的环境变量。这里以“e”(environment)结尾的两个函数execle()和execve()就可以在envp[]中指定当前进程所使用的环境变量。
表7.4是对这4个函数中函数名和对应语法的小结,主要指出了函数名中每一位所表明的含义,希望读者结合此表加以记忆。
表7.4 exec函数名对应含义
前4位
统一为:exec
第5位
l:参数传递为逐个列举方式
execl、execle、execlp
v:参数传递为构造指针数组方式
execv、execve、execvp
第6位
e:可传递新进程环境变量
execle、execve
p:可执行文件查找方式为文件名
execlp、execvp
(3)exec使用实例。
下面的第一个示例说明了如何使用文件名的方式来查找可执行文件,同时使用参数列表的方式。这里用的函数是execlp()。
/*execlp.c*/
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
intmain()
{
if(fork()==0)
{
/*调用execlp()函数,这里相当于调用了"ps-ef"命令*/
if((ret=execlp("ps","ps","-ef",NULL))<0)
{
printf("Execlperrorn");
}
}
}
在该程序中,首先使用fork()函数创建一个子进程,然后在子进程里使用execlp()函数。读者可以看到,这里的参数列表列出了在shell中使用的命令名和选项。并且当使用文件名进行查找时,系统会在默认的环境变量PATH中寻找该可执行文件。读者可将编译后的结果下载到目标板上,运行结果如下所示:
$./execlp
PIDTTYUidSizeStateCommand
1root1832Sinit
2root0S[keventd]
3root0S[ksoftirqd_CPU0]
4root0S[kswapd]
5root0S[bdflush]
6root0S[kupdated]
7root0S[mtdblockd]
8root0S[khubd]
35root2104S/bin/bash/usr/etc/rc.local
36root2324S/bin/bash
41root1364S/sbin/inetd
53root14260S/Qtopia/qtopia-free-1.7.0/bin/qpe-qws
54root11672Squicklauncher
65root0S[usb-storage-0]
66root0S[scsi_eh_0]
83root2020Rps-ef
$env
……
PATH=/Qtopia/qtopia-free-1.7.0/bin:/usr/bin:/bin:/usr/sbin:/sbin
……
此程序的运行结果与在shell中直接键入命令“ps-ef”是一样的,当然,在不同系统的不同时刻都可能会有不同的结果。
接下来的示例使用完整的文件目录来查找对应的可执行文件。注意目录必须以“/”开头,否则将其视为文件名。
/*execl.c*/
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
intmain()
{
if(fork()==0)
{
/*调用execl()函数,注意这里要给出ps程序所在的完整路径*/
if(execl("/bin/ps","ps","-ef",NULL)<0)
{
printf("Execlerrorn");
}
}
}
同样下载到目标板上运行,运行结果同上例。
下面的示例利用函数execle(),将环境变量添加到新建的子进程中,这里的“env”是查看当前进程环境变量的命令,如下所示:
/*execle.c*/
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
intmain()
{
/*命令参数列表,必须以NULL结尾*/
char*envp[]={"PATH=/tmp","USER=david",NULL};
if(fork()==0)
{
/*调用execle()函数,注意这里也要指出env的完整路径*/
if(execle("/usr/bin/env","env",NULL,envp)<0)
{
printf("Execleerrorn");
}
}
}
下载到目标板后的运行结果如下所示:
$./execle
PATH=/tmp
USER=sunq
最后一个示例使用execve()函数,通过构造指针数组的方式来传递参数,注意参数列表一定要以NULL作为结尾标识符。其代码和运行结果如下所示:
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
intmain()
{
/*命令参数列表,必须以NULL结尾*/
char*arg[]={"env",NULL};
char*envp[]={"PATH=/tmp","USER=david",NULL};
if(fork()==0)
{
if(execve("/usr/bin/env",arg,envp)<0)
{
printf("Execveerrorn");
}
}
}
下载到目标板后的运行结果如下所示:
$./execve
PATH=/tmp
USER=david
(4)exec函数族使用注意点。
在使用exec函数族时,一定要加上错误判断语句。exec很容易执行失败,其中最常见的原因有:
n 找不到文件或路径,此时errno被设置为ENOENT;
n 数组argv和envp忘记用NULL结束,此时errno被设置为EFAULT;
n 没有对应可执行文件的运行权限,此时errno被设置为EACCES。
小知识
事实上,这6个函数中真正的系统调用只有execve(),其他5个都是库函数,它们最终都会调用execve()这个系统调用。
3.exit()和_exit()(1)exit()和_exit()函数说明。
exit()和_exit()函数都是用来终止进程的。当程序执行到exit()或_exit()时,进程会无条件地停止剩下的所有操作,清除包括PCB在内的各种数据结构,并终止本进程的运行。但是,这两个函数还是有区别的,这两个函数的调用过程如图7.4所示。
图7.4exit和_exit函数流程图
从图中可以看出,_exit()函数的作用是:直接使进程停止运行,清除其使用的内存空间,并清除其在内核中的各种数据结构;exit()函数则在这些基础上做了一些包装,在执行退出之前加了若干道工序。exit()函数与_exit()函数最大的区别就在于exit()函数在调用exit系统之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是图中的“清理I/O缓冲”一项。
由于在Linux的标准函数库中,有一种被称作“缓冲I/O(bufferedI/O)”操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区中读取;同样,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(如达到一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。
这种技术大大增加了文件读写的速度,但也为编程带来了一些麻烦。比如有些数据,认为已经被写入文件中,实际上因为没有满足特定的条件,它们还只是被保存在缓冲区内,这时用_exit()函数直接将进程关闭,缓冲区中的数据就会丢失。因此,若想保证数据的完整性,就一定要使用exit()函数。
(2)exit()和_exit()函数语法。
表7.5列出了exit()和_exit()函数的语法规范。
表7.5 exit()和_exit()函数族语法
所需头文件
exit:#include<stdlib.h>
_exit:#include<unistd.h>
函数原型
exit:voidexit(intstatus)
_exit:void_exit(intstatus)
函数传入值
status是一个整型的参数,可以利用这个参数传递进程结束时的状态。一般来说,0表示正常结束;其他的数值表示出现了错误,进程非正常结束。
在实际编程时,可以用wait()系统调用接收子进程的返回值,从而针对不同的情况进行不同的处理
(3)exit()和_exit()使用实例。
这两个示例比较了exit()和_exit()两个函数的区别。由于printf()函数使用的是缓冲I/O方式,该函数在遇到“n”换行符时自动从缓冲区中将记录读出。示例中就是利用这个性质来进行比较的。以下是示例1的代码:
/*exit.c*/
#include<stdio.h>
#include<stdlib.h>
intmain()
{
printf("Usingexit...n");
printf("Thisisthecontentinbuffer");
exit(0);
}
$./exit
Usingexit...
Thisisthecontentinbuffer$
读者从输出的结果中可以看到,调用exit()函数时,缓冲区中的记录也能正常输出。
以下是示例2的代码:
/*_exit.c*/
#include<stdio.h>
#include<unistd.h>
intmain()
{
printf("Using_exit...n");
printf("Thisisthecontentinbuffer");/*加上回车符之后结果又如何*/
_exit(0);
}
$./_exit
Using_exit...
$
读者从最后的结果中可以看到,调用_exit()函数无法输出缓冲区中的记录。
小知识
在一个进程调用了exit()之后,该进程并不会立刻完全消失,而是留下一个称为僵尸进程(Zombie)的数据结构。僵尸进程是一种非常特殊的进程,它已经放弃了几乎所有的内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。
4.wait()和waitpid()(1)wait()和waitpid()函数说明。
wait()函数是用于使父进程(也就是调用wait()的进程)阻塞,直到一个子进程结束或者该进程接到了一个指定的信号为止。如果该父进程没有子进程或者他的子进程已经结束,则wait()就会立即返回。
waitpid()的作用和wait()一样,但它并不一定要等待第一个终止的子进程,它还有若干选项,如可提供一个非阻塞版本的wait()功能,也能支持作业控制。实际上wait()函数只是waitpid()函数的一个特例,在Linux内部实现wait()函数时直接调用的就是waitpid()函数。
(2)wait()和waitpid()函数格式说明。
表7.6列出了wait()函数的语法规范。
表7.6 wait()函数族语法
所需头文件
#include<sys/types.h>
#include<sys/wait.h>
函数原型
pid_twait(int*status)
函数传入值
这里的status是一个整型指针,是该子进程退出时的状态
·status若不为空,则通过它可以获得子进程的结束状态
另外,子进程的结束状态可由Linux中一些特定的宏来测定
函数返回值
成功:已结束运行的子进程的进程号
失败:-1
表7.7列出了waitpid()函数的语法规范。
表7.7 waitpid()函数语法
所需头文件
#include<sys/types.h>
#include<sys/wait.h>
函数原型
pid_twaitpid(pid_tpid,int*status,intoptions)
续表
函数传入值
Pid
pid>0:只等待进程ID等于pid的子进程,不管已经有其他子进程运行结束退出了,只要指定的子进程还没有结束,waitpid()就会一直等下去
pid=-1:等待任何一个子进程退出,此时和wait()作用一样
pid=0:等待其组ID等于调用进程的组ID的任一子进程
pid<-1:等待其组ID等于pid的绝对值的任一子进程
status
同wait()
options
WNOHANG:若由pid指定的子进程不立即可用,则waitpid()不阻塞,此时返回值为0
WUNTRACED:若实现某支持作业控制,则由pid指定的任一子进程状态已暂停,且其状态自暂停以来还未报告过,则返回其状态
0:同wait(),阻塞父进程,等待子进程退出
函数返回值
正常:已经结束运行的子进程的进程号
使用选项WNOHANG且没有子进程退出:0
调用出错:-1
3)waitpid()使用实例。
由于wait()函数的使用较为简单,在此仅以waitpid()为例进行讲解。本例中首先使用fork()创建一个子进程,然后让其子进程暂停5s(使用了sleep()函数)。接下来对原有的父进程使用waitpid()函数,并使用参数WNOHANG使该父进程不会阻塞。若有子进程退出,则waitpid()返回子进程号;若没有子进程退出,则waitpid()返回0,并且父进程每隔一秒循环判断一次。该程序的流程图如图7.5所示。
图7.5waitpid示例程序流
该程序源代码如下所示:
/*waitpid.c*/
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
intmain()
{
pid_tpc,pr;
pc=fork();
if(pc<0)
{
printf("Errorforkn");
}
elseif(pc==0)/*子进程*/
{
/*子进程暂停5s*/
sleep(5);
/*子进程正常退出*/
exit(0);
}
else/*父进程*/
{
/*循环测试子进程是否退出*/
do
{
/*调用waitpid,且父进程不阻塞*/
pr=waitpid(pc,NULL,WNOHANG);
/*若子进程还未退出,则父进程暂停1s*/
if(pr==0)
{
printf("Thechildprocesshasnotexitedn");
sleep(1);
}
}while(pr==0);
/*若发现子进程退出,打印出相应情况*/
if(pr==pc)
{
printf("Getchildexitcode:%dn",pr);
}
else
{
printf("Someerroroccured.n");
}
}
}
将该程序交叉编译,下载到目标板后的运行结果如下所示:
$./waitpid
Thechildprocesshasnotexited
Thechildprocesshasnotexited
Thechildprocesshasnotexited
Thechildprocesshasnotexited
Thechildprocesshasnotexited
Getchildexitcode:75
可见,该程序在经过5次循环之后,捕获到了子进程的退出信号,具体的子进程号在不同的系统上会有所区别。
读者还可以尝试把“pr=waitpid(pc,NULL,WNOHANG);”这句改为“pr=waitpid(pc,NULL,0);”或者“pr=wait(NULL);”,运行的结果为:
$./waitpid
Getchildexitcode:76
可见,在上述两种情况下,父进程在调用waitpid()或wait()之后就将自己阻塞,直到有子进程退出为止。