我们接着上一章的内容继续讲解。
在讲解exec()
函数之前,我们先来讲解一下一类特殊的进程:subreaper
进程。
上一篇文章的进程的执行顺序的内容块中留下了一个问题:parent = 1,详细内容可阅读Linux/Uinx 系统编程:进程管理(1)
Linux内核3.4版本以后,出现了subreaper进程,使得Linux内核对于孤儿进程的处理方式进行了改变。在此之前,所有的孤儿进程都会被Init进程“收尸”,也就是说,当一个进程的父进程死亡后,它的父进程将会变成Init(pid = 1)进程,这也就是为什么上一篇文章中parent = 1
的原因。
进程可以用系统调用将自己定义为收割进程,也就是subreaper
:
prctl(PR_SET_CHILD_SUBREAPER);
这个标记的功能是:标记有subreaper
的最近活跃祖先进程将会称为新的父进程。
假如现在有一个进程,生出一个子进程,其子进程又生出一个子进程,那么他们的关系将会是如下的链表结构:
1–>2–>3
如果此时2进程结束,3进程的父进程将会变为1,如下:
1–>3
这样做看似没有问题,但是实际上会给进程1造成负担。
如果出现大量的类似于上述情况的3号进程,那么1号进程在给他们回收资源,也就是给孤儿进程死亡后回收“尸体”时,会十分的累。
另外一个原因就是:在Linux中,通常有很多用户服务管理器进程(任务管理器等),我们知道,所有的进程都是有共同的祖先的,我们这里假定所有的用户进程和服务管理器进程有共同的父进程,如下:
1–>ser
1–>2–>3
ser为服务管理器进程,2,3为用户进程,当2死亡时,3进程会变成1的子进程:
1–>3
问题来了,如果规定只有父进程可以回收子进程,那么回收3进程的只能是1,但是服务管理器进程的功能就是管理其他进程的,本来就是为了给孤儿"收尸"而存在的,上面的规定显然不符合让ser进程给3进程“收尸”的要求。
通过subreaper机制,可以使得除了1号进程之外的进程也变成所有孤儿进程的祖先进程。
用一个形象的比喻就是,在早先的Linux中,只有P1有权经营孤儿院。但是subreaper机制使得任何一个进程都有权力去经营孤儿院。
每个用户Init进程均被标记为subreaper,在Umode模式下属于用户。读者可以使用sh命令:
ps fxau | grep USERNAME | grep "/sbin/upstart"
显示subreaper进程的PID信息。
下面是一个示例代码:
#include
#include
#include
#include
int main() {
int pid, r, status;
printf("mark process %d as a subreaper\n", getpid());
r = prctl(PR_SET_CHILD_SUBREAPER);
pid = fork();
if (pid) {
printf("subreaper %d child=%d\n", getpid(), pid);
while(1) {
pid = wait(&status); //wait for zombie chlidren
if (pid > 0) printf("%d waited a zombie=%d\n", getpid(), pid);
else break;
}
} else {
printf("child %d parent=%d\n", getpid(), (pid_t)getppid());
pid = fork();
if (pid) {
printf("child %d start : grandchild=%d\n", getpid(), pid);
printf("child %d EXIT : grandchild=%d\n", getpid(), pid);
} else {
printf("grandchild = %d start : myparent=%d\n", getpid(), getppid());
printf("grandchild = %d EXIT : myparent=%d\n", getpid(), getppid());
}
}
return 0;
}
解释:
下面是该函数的使用方法(文档):
函数描述:
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 execvp (const char *file, char *const argv []);
int execve (const char *path, char *const argv [], char *const envp []);
其中只有execve
是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。
各参数说明:
path
参数指定了要执行的可执行文件的路径。arg
参数表示传递给可执行文件的命令行参数。NULL
参数标志着参数列表的结束。执行过程:
当进程调用一种 exec
函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec
并不创建新进程,所以调用 exec
前后该进程的id并未改变。
底层实现:
exec
函数族装入并运行程序pathname,并将参数arg0 (arg1,arg2,argv [],envp [])传递给子程序。
执行示例:
#include
#include
#include
#include
int main (int argc,char *argv [],char *envp []) {
char *arg []= {"ls","-a", NULL };
if (fork ()==0) {
printf ("execl...........\\n");
if (execl ("/bin/ls","ls","-a",NULL)<0) {
fprintf (stderr,"execl failed:%s",strerror (errno));
return -1;
}
}
if (fork ()==0) {
printf ("execv...........\\n");
if (execv ("/bin/ls",arg)<0) {
fprintf (stderr,"execl failed:%s\\n",strerror (errno));
return -1;
}
}
if (fork ()==0) {
printf ("execlp...........\\n");
if (execlp ("ls","ls","-a",NULL)<0) {
fprintf (stderr,"execl failed:%s",strerror (errno));
return -1;
}
}
if (fork ()==0) {
printf ("execvp...........\\n");
if (execvp ("ls",arg)<0) {
fprintf (stderr,"execl failed:%s\\n",strerror (errno));
return -1;
}
}
if (fork ()==0) {
printf ("execle...........\\n");
if (execle ("/bin/ls","ls","-a",NULL,envp)<0) {
fprintf (stderr,"execl failed:%s",strerror (errno));
return -1;
}
}
if (fork ()==0) {
printf ("execve...........\\n");
if (execve ("/bin/ls",arg,envp)<0) {
fprintf (stderr,"execl failed:%s\\n",strerror (errno));
return -1;
}
}
return 0;
}
这个程序调用了ls
这个 Linux 常用的系统命令。
返回值:
exec
函数族的函数执行成功后不会返回,调用失败时,会设置errno
并返回-1。
执行结果:
在exec()
之后,进程中所有打开文件都保持打开状态。被标记为close-on-exec
的打开文件描述符将会被关闭。进程的大部分信号被重置为默认值。如果可执行文件打开了setuid
位,进程有效uid/gid将被更改为可执行文件的所有者,执行结束时将重置为保存的进程uid/gid。
由于各个子进程执行的顺序无法控制,所以有可能出现一个比较混乱的输出–各子进程打印的结果交杂在一起,而不是严格按照程序中列出的次序。
常见问题:
errno
被设置为ENOENT
。argv
和envp
忘记用NULL
结束,此时errno
被设置为EFAULT
。errno
被设置为EACCES
。