Linux/Uinx 系统编程:进程管理(2)

Linux/Uinx 系统编程:进程管理(2)

文章目录

  • Linux/Uinx 系统编程:进程管理(2)
    • subreaper进程
      • subreaper进程代码示例
    • exec() 更改进程执行映像
      • exec()

我们接着上一章的内容继续讲解。

在讲解exec()函数之前,我们先来讲解一下一类特殊的进程:subreaper进程。

上一篇文章的进程的执行顺序的内容块中留下了一个问题:parent = 1,详细内容可阅读Linux/Uinx 系统编程:进程管理(1)

subreaper进程

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信息。

subreaper进程代码示例

下面是一个示例代码:

#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;
}

运行之后得到如下结果:
Linux/Uinx 系统编程:进程管理(2)_第1张图片
Linux/Uinx 系统编程:进程管理(2)_第2张图片

解释:

  • 第一个进程首先将自己标记为subreaper
  • 第一个进程复刻出一个子进程,并且使用while循环等待僵尸子进程
  • 子进程再复刻出自己的一个子进程,是第一个进程的孙子进程
  • 程序运行时,子进程或者孙子进程可以先终止。
  • 如果孙子进程先终止,那么他的父进程将会保持不变,第一张图的情况。
  • 如果子进程先终止,而且没有任何活跃的祖先进程被标记为subreaper,孙子进程将会成为第一个进程的子进程,在上面图示的第二张图中表述了这种情况,可以看出,父进程为19075,子进程为19076,孙子进程为19077,首先运行的是子进程19076,然后子进程退出(4,5行),最后孙子进程19077开始运行,发现它的父亲为19076(6行),接着,19075这个进程对19076这个进程进行了回收(7行),然后孙子进程运行结束时,发现原来的父进程消失了,这个时候其父进程变成了被标记为subreaper进程的第一个进程(8行),最后被subreaper进程回收(9行)

exec() 更改进程执行映像

下面是该函数的使用方法(文档):

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 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。

由于各个子进程执行的顺序无法控制,所以有可能出现一个比较混乱的输出–各子进程打印的结果交杂在一起,而不是严格按照程序中列出的次序。

常见问题

  1. 找不到文件或路径,此时errno被设置为ENOENT
  2. 数组argvenvp忘记用NULL结束,此时errno被设置为EFAULT
  3. 没有对要执行文件的运行权限,此时errno被设置为EACCES

你可能感兴趣的:(Linux/Uinx系统编程,linux,网络,运维)