文章目录

  • 1 进程扇与进程链
  • 2 进程组
    • 2.1 概念
    • 2.1 进程组的创建与设置
  • 3 会话
    • 3.1 概念
    • 3.2 创建会话
  • 4 控制终端、前台进程组与后台进程组
  • 5 后台进程组与控制终端
  • 6 孤儿进程与孤儿进程组
  • 7 守护进程
    • 7.1 守护进程的概念
    • 7.2 创建守护进程(调用系统函数)
    • 7.3 创建守护进程(自己实现)

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) {// fatherif (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) {// childif (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/nullprintf("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. 设置 umaskumask(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的结果是一样。

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

  1. Linux系统编程06 --进程间关系

    文章目录 六.进程间关系 终端 **tty** 网络终端 进程组 会话 setsid()创建新会话函数 getsid()获取会话sid函数 守护进程 守护进程创建步骤 六.进程间关系 终端 ​ 在UN ...

  2. linux系统编程之进程(八):守护进程详解及创建,daemon()使用

    linux系统编程之进程(八):守护进程详解及创建,daemon()使用 一,守护进程概述 Linux Daemon(守护进程)是运行在后台的一种特殊进程.它独立于控制终端并且周期性地执行某种任务或等 ...

  3. 【Linux系统编程】进程替换:exec 函数族

    00. 目录 文章目录 00. 目录 01. exec函数族 02. 参考示例 2.1 execl函数示例 2.2 execv函数示例 2.3 execlp() 或 execvp()函数示例 2.4 ...

  4. vbs结束进程代码_物联网学习教程—Linux系统编程之进程控制

    Linux系统编程之进程控制 一.结束进程 首先,我们回顾一下 C 语言中 continue, break, return 的作用: continue: 结束本次循环 break: 跳出整个循环,或跳 ...

  5. Linux系统编程之进程与线程控制原语对比

    Linux系统编程之进程与线程控制原语对比 进程 线程 fork pthread_create exit pthread_exit wait pthread_join kill pthread_can ...

  6. 【Linux系统编程】进程概述和进程号

    00. 目录 文章目录 00. 目录 01. 进程概述 02. 进程状态 03. 进程控制块 04. 进程号 05. 进程号相关函数 06. 案例实战 07. 附录 01. 进程概述 我们平时写的 C ...

  7. 【Linux系统编程】进程退出和回收进程资源

    00. 目录 文章目录 00. 目录 01. 进程退出函数 02. 进程退出讨论 03. 回收进程资源 04. 附录 01. 进程退出函数 #include <stdlib.h>void ...

  8. linux系统编程之进程概念(操作系统---管理,进程创建,进程状态,进程优先级, 环境变量,程序地址空间,进程O(1)调度方法)

    系统编程: 进程概念->进程控制->基础IO->进程间通信->进程信号->多线程 进程概念 冯诺依曼体系结构----现代计算机硬件体系结构 冯诺依曼体系结构----现代计 ...

  9. 【Linux系统编程】进程内存模型

    00. 目录 文章目录 00. 目录 01. Linux可执行程序结构 02. Linux进程结构 03. 存储类型总结 04. 附录 01. Linux可执行程序结构 在 Linux 下,程序是一个 ...

最新文章

  1. oracle 输出 三角形,10. 三角形(示例代码)
  2. mysql主从的原理_Mysql主从的原理
  3. 25+开源的在线购物软件(PHP, JavaScript 和 ASP.Net)
  4. 苹果Mac白噪音软件:Noise Machine
  5. 如何升级linux内核
  6. WES7 定制界面完整过程(去除所有windows标识)
  7. 【JavaScript游戏开发】使用HTML5 canvas开发的网页版中国象棋项目
  8. GAN网络学习笔记系列2-Cluster GAN
  9. A Feature Descriptor: Shape Context
  10. Kinect+unity 实现体感格斗闯关小游戏
  11. 使用am start命令启动android apk应用程序
  12. 苹果+android+扫码支付,苹果iOS12捷径扫码付款怎么设置 微信支付宝扫码二合一支付捷径...
  13. 【2022-05-31】JS逆向之易企秀
  14. ORA-27301: OS failure message: No buffer space available
  15. Python--fractions库【分数、有理数】
  16. 【镀金与沉金工艺的区别,今后得选“沉金”】
  17. android入门整理
  18. 搭建gataway所遇问题及解决
  19. c#程序连接oracle失败问题
  20. Xdoc 水印 解决方式

热门文章

  1. 如何打开docx文件
  2. matlab画圆柱,Matlab 画三维圆柱体
  3. 第二次作业——个人项目实战:数独
  4. MythType插入公式后出现段标志符
  5. moment常用操作
  6. [附源码]计算机毕业设计JAVA海南生鲜冷链物流配送系统
  7. 倾斜摄影在道路测绘设计中的应用
  8. vue如何实现一个auto-complete组件
  9. 卖场惊魂!鼎好电子城剑拔弩张的笔记本购买经过
  10. 儿歌nbsp;nbsp;--nbsp;悠悠寸草心