Linux系统编程(四)--进程间关系

1 进程扇与进程链

请添加图片描述

进程扇

进程扇构造代码

// ps_swing.c
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    int pid, i = 4;

    while (i--) {
        pid = fork();
        if (pid == 0) {
            // 如果是子进程,就不能继续循环了。
            // 按照进程扇的定义,子进程是没有子进程的。
            break;
        }
        else if (pid < 0) {
            perror("fork");
            return -1;
        }
    }

    printf("pid: %d -> ppid: %d\n", getpid(), getppid());
    while (1) sleep(1);
    return 0;
}

编译运行

$ gcc ps_swing.c -o ps_swing
$ ./ps_swing 
pid: 38031 -> ppid: 38030
pid: 38030 -> ppid: 32611
pid: 38032 -> ppid: 38030
pid: 38033 -> ppid: 38030
pid: 38034 -> ppid: 38030

使用 pstree -ap 命令查看进程间关系

请添加图片描述

可以看出 ps_swing 这几个进程符合进程扇的定义。

请添加图片描述

进程链构造代码

// ps_link.c
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    int pid, i = 4;


    while (i--) {
        pid = fork();
        if (pid > 0) {
            // 如果是父进程,产生了子进程后就必须退出循环。
            // 因为按照进程链定义,一个父进程只能产生一个子进程。
            break;
        }
        else if (pid < 0) {
            perror("fork");
            return -1;
        }
    }

    printf("pid: %d -> ppid: %d\n", getpid(), getppid());
    while (1) sleep(1);
    return 0;
}

编译运行

$ gcc ps_link.c -o ps_link
$ ./ps_link 
pid: 38063 -> ppid: 32611
pid: 38064 -> ppid: 38063
pid: 38065 -> ppid: 38064
pid: 38066 -> ppid: 38065
pid: 38067 -> ppid: 38066

使用 pstree -ap 命令查看进程间关系

请添加图片描述

2 进程组

2.1 概念

进程组:组id相同的进程的集合。

信号与进程组:

使用 kill -n -pgid 可以将信号n发送到进程组 pgid 中的所有进程。

进程组的生命期:

从被创建开始,到其内所有进程终止或离开该组。

进程组组长:

进程组中的某一个进程,进程号等于进程组的进程号。

2.1 进程组的创建与设置

// 获取进程 pid 的进程组 id.
pid_t getpgid(pid_t pid);

// 指定 pid 为进程组组长或将 pid 加入到组 pgid.
int setpgid(pid_t pid, pid_t pgid);

新建进程组

将某一个进程指定为组长,就创建了一个新的进程组。相当于调用 setpgid(pid, pid),即将进程 pid 设置为进程组组长,同时创建进程组 pid。

将进程添加到进程组

使用setpgid(pid, pgid)将进程pid添加到已存在的进程组pgid。

注意:在使用上面的函数时,必须保证调用者进程、被设置的进程以及要添加的进程组属于同一个会话,否则会出现权限错误。

创建进程组时,要在父子进程中都调用setpgid(pid, pid),将进程添加进进程组,要在父子进程中都调用setpgid(pid, pgid)。防止要加的进程组根本不存在,或者已经消失。

实验:如图所示,将进程 0 (父进程)和进程 2 设置成一组,假设为组 1,将进程 1 和 进程 3 设置成另一个组,假设为组 2. 另外,我们希望进程 0 和进程 1 分别是这两个组的组长。

请添加图片描述

#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

int main() {
    int pid, i;
    int group1, group2;

    setpgid(getpid(), getpid());
    group1 = getpgid(getpid());


    for (i = 0; i < 3; ++i) {
        pid = fork();
        if (pid > 0) {
            // father
            if (i == 0) {
                setpgid(pid, pid);
                group2 = getpgid(pid);
            }
            else if (i == 1) {
                setpgid(pid, group1);
            }
            else if (i == 2) {
                setpgid(pid, group2);
            }
            break;
        }
        else if (pid == 0) {
            // child
            if (i == 0) {
                setpgid(getpid(), getpid());
                group2 = getpgid(getpid());
            }
            else if (i == 1) {
                setpgid(getpid(), group1);
            }
            else if (i == 2) {
                setpgid(getpid(), group2);
            }
        }
        else if (pid < 0) {
            perror("fork");
            return -1;
        }
    }

    printf("进程 %d, pid: %d -> ppid: %d, pgid: [%d], (%s)\n", i, getpid(), getppid(), getpgid(getpid()), strerror(errno));
    while (1) sleep(1);
    return 0;
}

编译运行

$ gcc ps_swing_v1.c -o ps_swing_v1
$ ./ps_swing_v1 
进程 0, pid: 38121 -> ppid: 34090, pgid: [38121], (Success)
进程 1, pid: 38122 -> ppid: 38121, pgid: [38122], (Success)
进程 2, pid: 38123 -> ppid: 38122, pgid: [38121], (Success)
进程 3, pid: 38124 -> ppid: 38123, pgid: [38122], (Success)

进程 0 和进程 2 位于组 38121,组长是进程 0;而进程 1 和进程 3 位于组 38122,组长是进程 1。

3 会话

3.1 概念

进程组是一个或多个进程的集合。创建进程组的进程,是进程组组长。

会话(session),是一个或多个进程组的集合。创建会话的进程,是会话首进程(session leader)

请添加图片描述

进程组和会话中的进程安排

图中的会话,可以使用以下命令形成:

$ cat | cat &
$ cat | cat | cat

可以使用命令 ps ajx | sed '1p;/cat$/!d' 查看结果:

请添加图片描述

可以看到所有的 cat 进程都同属一个会话 32611。

3.2 创建会话

作用:创建一个新会话。

pid_t setsid(void)

调用此函数的进程不能是进程组组长,否则会失败。

理由:

假设该进程是进程组组长,创建完该会话后,该进程成为了其它会话中的 session leader。然而,其组员仍然存在于原来的会话中,这将导致同一个进程组中的进程处在不同会话中,这是不允许的。

为了防止进程是进程组组长,让你的进程 fork 出一个子进程,再 kill 掉父进程(不 kill 也没关系)。

创建新会话,发生3件事:

(1) 该进程变成新会话的 session leader,此时,该进程是新会话中的唯一进程。

(2) 该进程成为新进程组的组长。

(3) 该进程没有控制终端。如果在调用 setsid 之前有一个控制终端,也被切断。

作用:获取进程 pid 的会话 id

pid_t getsid(pid_t pid)

如果进程 pid 和调用进程不在同一个会话,调用失败。通常 pid 的值传 0,表示获取调用进程所在的会话 id。

实验:创建一个新会话,该会话中只有一个进程,就是创建该会话的进程。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    pid_t sid, pid;
    pid = fork();
    if (pid == 0) {
        // 子进程不可能是进程组组长,可以让其创建新的会话,
        // 同时它成为 session leader, group leader

        // 先查看子进程从属于哪个会话
        sid = getsid(getpid());
        printf("sid = %d\n", sid);

        // 让子进程创建新会话
        sid = setsid();
        if (sid < 0) {
            perror("setsid");
        }
        // 查看子进程当前从属于哪个会话
        printf("sid = %d\n", sid);
    }

    while (1) sleep(1);
    return 0;
}

编译运行

$ gcc session.c -o session
$ ./session 
sid = 34090
sid = 38203

4 控制终端、前台进程组与后台进程组

控制终端

默认情况下,每个进程的标准输入、标准输出和标准错误输出都指向控制终端。进程可以没有控制终端。

控制进程

建立与控制终端连接的会话首进程(session leader),被称为控制进程。如果终端设备与控制终端断开连接,系统会发送信号给控制进程。

前台进程组与后台进程组

快捷键 CTRL C、CTRL Z 等发送信号,默认发到前台进程组。执行 CTRL C 时,前台进程组中所有的进程都会中断。

作用:获取和设置当前会话中的前台进程组:

pid_t tcgetpgrp(int fd);

int tcsetpgrp(int fd, pid_t pgrpid);

实验

$ cat | cat &
$ cat | cat | cat

再另一个终端中执行:

$ ps ajx | sed -n '1p;/cat$/p;/bash$/p'

请添加图片描述

可以看到会话 32611 的前台进程组 id 号是 38158。 该会话中的其它进程组都是后台进程组。

上面的例子抽象出来如图所示。

请添加图片描述

前台进程组,后台进程组,控制进程

实验:设置前台进程组

  • 目标

任务1: 在 bash 中启动你的进程,然后将前台进程组设置为 bash 所在的进程组。
任务2:关闭启动你进程的终端,查看你的进程能收到什么信号。

  • 思路

在任务 1 中,首先获取 bash 进程的进程组 id,而 bash 进程又是进程组组长,所以它的进程组 id 就相当于 bash 进程 id。

任务 2 中,你的进程可以收到 SIGHUP 信号,所以捕获些信号即可。

// ct.c
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

void handler(int sig) {
    if (sig == SIGHUP) {
        int fd = open("tmp", O_WRONLY | O_CREAT, 0664);
        write(fd, "hello SIGHUP\n", 32);
        exit(0);
    }
}

void print() {
    pid_t pid, sid, pgid, tpgid;

    pid = getpid();
    sid = getsid(pid);
    pgid = getpgid(pid);
    tpgid = tcgetpgrp(0);
    if (tpgid < 0) {
        perror("tcgetpgrp");
    }

    printf("pid = %d, pgid = %d, sid = %d, tpgid = %d\n", pid, pgid, sid, tpgid);
}
int main(int argc, char* argv[]) {
    signal(SIGHUP, handler);

    // 打印当前进程 id,组 id,会话 id,当前会话中的前台进程组 id.
    print();

    // 将前台进程组设置为 bash 进程组的 id.
    tcsetpgrp(0, getppid());

    print();

    while (1) sleep(1);
    return 0;
}

编译和运行

$ gcc ct.c -o ct
skx@ubuntu:~/pra/learn_linux/70$ ./ct 
pid = 38324, pgid = 38324, sid = 34090, tpgid = 38324
pid = 38324, pgid = 38324, sid = 34090, tpgid = 34090

启动你的程序后,无论键入 CTRL C、CTRL \ 还是 CTRL Z 都没反应了。程序第一行打印的是未设置前台进程组前的结果,第二行打印是设置后的结果。可以看到设置后的前台进程组 id = 34090。

直接关闭终端,当前文件夹下会生成 tmp 文件。

5 后台进程组与控制终端

后台进程组中的进程读写控制终端:

读控制终端:

终端驱动程序会检测到,并向后台进程组中的所有进程发送一个特定信号 SIGTTIN。 默认情况下导致此后台进程组停止。

写控制终端

终端驱动程序会检测到并向后台进程组中的所有进程发送信号 SIGTTOU。

请添加图片描述

6 孤儿进程与孤儿进程组

孤儿进程

如果一个进程,它的父进程先终止了,则该进程成为孤儿进程。此后,该进程的父进程变为1号init进程。孤儿进程不像僵尸进程,它是无害的,不需要回收。

孤儿进程组

该进程组的每个成员的父进程要么是该组的成员,要么在其它会话中。

请添加图片描述

进程组2是孤儿进程组

孤儿进程组的特性:

如果一个进程组包含一个或一个以上的停止的进程,当该进程组变成孤儿进程组时,刚产生的孤独进程组中的每一个进程都能收到 SIGHUP 信号。

实验

下面的程序功能:进程组中存在停止的进程,该进程组成为孤儿进程组后,该进程组所有的进程会收到 SIGHUP 信号。另外,该程序还演示了后台进程组试图读控制终端会产生错误。

需要注意的是,父进程终止,子进程进入后台进程组。

// orphan.c
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <errno.h>

void handler(int sig) {
    printf("hello sighup, pid = %d\n", getpid());
}

void print(char* name) {
    printf("%s: pid = %d, ppid = %d, pgrp = %d, tpgrp = %d\n",
        name, getpid(), getppid(), getpgid(getpid()), tcgetpgrp(0));
    fflush(stdout);
}

int main() {
    char c;
    pid_t pid;
    print("parent");

    pid = fork();
    if (pid < 0) {
        perror("fork");
    }
    else if (pid > 0) {
        sleep(5);
    }
    else {
        print("child");
        signal(SIGHUP, handler);
        kill(getpid(), SIGTSTP); // 让子进程暂停
        print("child"); // 如果执行了此行,说明已经收到了 SIGHUP 信号

        if (read(STDIN_FILENO, &c, 1) != 1) {
            printf("read error, error number: %d\n", errno);
        }
        exit(0);
    }
    return 0;
}

编译和运行

$ gcc orphan.c -o orphan
$ ./orphan 
parent: pid = 38397, ppid = 37676, pgrp = 38397, tpgrp = 38397
child: pid = 38398, ppid = 38397, pgrp = 38397, tpgrp = 38397
hello sighup, pid = 38398
child: pid = 38398, ppid = 1698, pgrp = 38397, tpgrp = 37676
read error, error number: 5

程序一开始让父进程打印信息,接着 fork 出子进程,让子进程打印信息,然后子进程进入停止状态。

等待父进程结束后,子进程所在的组变成了孤儿进程组,同时它也是后台进程组。接着所有子进程会收到 SIGHUP 信号,因为子进程对该信号进行了捕获,信号处理函数向控制终端输出 hello sighup, pid = 38398。

接下来子进程继续执行 print 向屏幕输出信息,注意此时的前台进程组 tpgrp = 37676,这是 bash 进程所在的进程组 id,也等于 bash 进程的 id。

最后,子进程试图读控制终端,于是收到 SIGTTIN 信号,read 返回出错,errno 被设置为 5,也就是 EIO,解释如下:

EIO I/O 错误。当进程位于后台进程组时,还试图从控制终端读,就会产生此错误。

7 守护进程

7.1 守护进程的概念

之前的程序中,如果关闭控制终端,就很可能会导致在该控制终端中运行的进程也退出。

控制终端关闭时,该控制终端符合下面条件的进程会收到 SIGHUP 信号:

1、前台进程组的所有进程

2、某个后台进程组中存在停止的进程(ps ajx 命令中,进程状态显示为 T)。控制终端关闭,导致该后台进程组成为孤儿进程组,则该孤儿进程组中的所有进程都收到 SIGHUP 信号。

并不是所有情况下控制终端中的进程都会收到 SIGHUP 信号。

守护进程概念:

不和任何控制终端挂钩(TTY 一栏是问号),你关闭终端对它没影响。向终端读写数据(需要关闭标准输入、标准输出和标准错误),一直在后台运行着。

7.2 创建守护进程(调用系统函数)

int daemon(int nochdir, int noclose);

参数 nochdir 如果为0,表示将当前工作目录切换到根目录 “/”;否则当前目录不变。

参数 noclose 如果为0,表示重定向标准输入、标准输出和标准错误到文件 /dev/null;否则这些文件描述符不变。

实验:使用系统函数创建守护进程

// daemon.c
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int  main() {
    // 先打印进程的 pid 等信息
    printf("pid: %d, ppid: %d, sid: %d\n", getpid(), getppid(), getsid(getpid()));

    // 将进程设置为守护进程
    daemon(0, 0);

    int fd;
    char buf[256];

    // 守护进程向标准输出打印信息,并向 test.log 写信息
    while (1) {
        // 实际上,这一行永远不会打印到屏幕,因为守护进程没有控制终端,标准输出也被重定向到了 /dev/null
        printf("pid: %d, ppid: %d, sid: %d\n", getpid(), getppid(), getsid(getpid()));
        // 因为 daemon 函数改变了当前工作目录,所以这里使用全路径定位到当前目录。
        fd = open("/home/allen/apue/relationship/daemon/test.log", O_WRONLY | O_APPEND | O_CREAT, 0664);
        if (fd > 0) {
            sprintf(buf, "pid: %d, ppid: %d, sid: %d\n", getpid(), getppid(), getsid(getpid()));
            write(fd, buf, strlen(buf));
        }

        sleep(3);
    }
    return 0;
}

编译运行

$ gcc daemon.c -o daemon
$ ./daemon 
pid:38470, ppid:37676, sid:37676

当我们运行 ./daemon 后,发现屏幕只输出一行。使用 ls 命令查看当前文件夹下,发现生成了 test.log 文件。

7.3 创建守护进程(自己实现)

守护进程编写规则

(1) 设置 umask。

(2) 调用 fork,然后父进程退出。保证子进程不是进程组组长,为第三步提供保证。

(3) 调用 setsid 创建新会话。这一步可以保证子进程没有控制终端。

(4) 捕获 SIGHUP 信号,防止因为孤儿进程组中的进程收到 SIGHUP 信号而终止。

(5) 切换当前工作目录。(对应 daemon 函数的第一个参数。)

(6) 关闭不再需要的文件描述符。这一步保证标准输入、标准输出和标准错误被关闭。(对应 daemon 函数的第二个参数。)

(7) 将标准输入、标准输出和标准错误定向到文件描述符 0、1 和 2.

备注:1、4、5 步骤是可选的,不写也没事。

实验:实现守护进程

#include <stdio.h>
#include <fcntl.h>
#include <sys/resource.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <signal.h>

void daemonize() {
    pid_t pid;
    int i, fd0, fd1, fd2;
    struct rlimit rl;

    getrlimit(RLIMIT_NOFILE, &rl);

    // 1. 设置 umask
    umask(0);

    // 2. 调用 fork,让父进程退出,保存子进程不是进程组组长
    if ((pid = fork()) < 0) {
        perror("fork");
        exit(-1);
    }
    else if (pid > 0) {
        // 让父进程退出
        exit(0);
    }

    // 3. 调用 setsid 创建新会话
    if (setsid() < 0) {
        perror("setsid");
        exit(-1);
    }

    // 4. 捕获 SIGHUP 信号
    struct sigaction sa;
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if (sigaction(SIGHUP, &sa, NULL) < 0) {
        perror("sigaction");
        exit(-1);
    }


    // 5. 更改工作目录
    if (chdir("/") < 0) {
        perror("chdir");
        exit(-1);
    }

    // 6. 关闭不用的描述符,保证标准输入、标准输出和标准错误的文件描述符与控制终端文件脱离关系
    if (rl.rlim_max == RLIM_INFINITY) {
        rl.rlim_max = 1024;
    }
    for (i = 0; i < rl.rlim_max; ++i) {
        close(i);
    }

    // 7. 为守护进程打开 /dev/null,并让描述符 0、1、2 指向它
    fd0 = open("/dev/null", O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);
}


// 这部分和上一篇的基本差不多,只是把 daemon 函数换成了 daemonize 而已。
int  main() {

    printf("pid: %d, ppid: %d, sid: %d\n", getpid(), getppid(), getsid(getpid()));
    daemonize();

    int fd;
    char buf[256];

    while (1) {

        
        printf("pid: %d, ppid: %d, sid: %d\n", getpid(), getppid(), getsid(getpid()));
        fd = open("/home/allen/apue/relationship/daemon/test.log", O_WRONLY | O_APPEND | O_CREAT, 0664);
        if (fd > 0) {
            sprintf(buf, "pid: %d, ppid: %d, sid: %d\n", getpid(), getppid(), getsid(getpid()));
            write(fd, buf, strlen(buf));
        }

        sleep(3);
    }
    return 0;
}

运行结果和使用系统函数daemon的结果是一样。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值