进程关系
进程组
进程组概念
在 Linux 中,进程组是一个或多个进程的集合,简称PGID。每个进程都属于一个进程组,目的是为了便于对一组进程进行管理。通过将多个相关进程划分到一个进程组中,可以更方便地对他们进行控制,像发送信号、终止等操作。
当父进程创建子进程时,子进程会默认加入父进程所属的组。进程组的ID默认是第一个创建该组的进程的ID,该进程也被称为组长进程。
进程组的生存周期:从进程组创建到最后一个进程离开进程组。
一个进程可以为自己或为其子进程设置进程组ID。
我们可以通过 ps -ajx 查看进程进程关系:
root@ubuntu22:~# cat | cat | cat &
[1] 32
root@ubuntu22:~# ps -ajx
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
0 1 1 1 ? 0 Ssl 0 0:00 /init
1 14 14 14 tty1 0 Ss 0 0:00 /init
14 15 15 14 tty1 0 S 0 0:00 -bash
15 30 30 14 tty1 0 T 0 0:00 cat
15 31 30 14 tty1 0 T 0 0:00 cat
15 32 30 14 tty1 0 T 0 0:00 cat
15 34 34 14 tty1 0 R 0 0:00 ps -ajx
在上面命令中,PGID列表示的是进程的组ID,我们发现3个 cat 进程都属于同一个组(30)。
可以使用 kill 命令向一组进程发送信号,只需将进程ID改为负数,例如:
通过上面命令,3个 cat 进程都会被杀死。
获取进程组ID
进程组表示用PGID表示,用于描述该进程所属的进程组。
getpgrp() 函数用于获取调用进程所属的进程组ID,该函数定义如下:
#include <unistd.h>
pid_t getpgrp(void);
返回值
- 如果函数执行成功,返回调用进程所属的进程组ID。
- 如果函数执行失败,函数返回 -1,可以通过 errno 变量来获取具体的错误信息。
getpgid() 函数用于获取指定进程的进程组ID,该函数定义如下:
#include <unistd.h>
pid_t getpgid(pid_t pid);
参数说明
- pid:进程ID,如果参数为0,表示当前调用进程。
返回值
- 如果函数执行成功,返回指定进程所属的进程组ID。
- 如果函数执行失败,函数返回 -1,可以通过 errno 变量来获取具体的错误信息。
设置进程组-setpgid()函数
setpgid()函数用于设置一个进程的进程组ID,即将一个进程移到另一个进程组中。该函数定义如下:
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
参数说明
- pid:进程ID,表示要修改进程组的进程。如果 pid 为0,则表示当前进程。
- pgid:进程组ID。如果 pgid 为0,则将当前进程的PID作为GID。
返回值
- 如果函数执行成功,返回指定进程所属的进程组ID。
- 如果函数执行失败,函数返回 -1,可以通过 errno 变量来获取具体的错误信息。
如果 pid 和 pgid 都为0,则系统会将调用进程移动到一个新的进程组,并作为组长进程。
终端
终端是用户与 Linux 内核之间进行交互的桥梁,用户通过终端可以向系统发出命令并查看系统的输出。
当Linux中,控制终端(Controlling Terminal)是一个终端设备,通常与Shell 进程相关联。当用户在终端上登录并启动一个 Shell 进程时,该终端会成为控制终端。这使得用户可以通过终端与 Shell 进行交互,管理作业以及控制进程的执行。
终端设备通常位于 /dev/ 目录下:
- 终端设备文件:/dev/tty[0-9],例如 /dev/tty0、/dev/tty1 等。
- 控制台设备:/dev/console
- 串行端口设备:/dev/ttyS[0-9],例如 /dev/ttyS0、/dev/ttyS1等。
- 伪终端设备(Pseudo Terminal):/dev/pts/[0-9],例如 /dev/pts/0、/dev/pts/1 等。
用户可以通过这些设备文件来与终端进行交互,包括登录控制终端、管理终端会话以及执行命令操作。
默认情况下,每个进程的标准输入、标准输出和标准错误输出都指向控制终端。标准输入读取用户的键盘输入,标准输出、标准错误将数据显示在显示器上。控制终端被保存在进程PCB中,当通过fork()函数创建子进程,对应的控制终端也会拷贝至子进程。
Linux启动流程如下:
fork+exec fork+exec 账号密码+fork
init------------>getty------------->login---------------->shell
为什么需要终端,直接从键盘和显示器中读取数据不行吗?终端实际上实现了下列功能:
- 回显:可以让输入字符自动被回显到终端的输出上,如果没有该功能,则会极大影响用户体验。
- 行编辑:用户输入的字符不是立马送到应用程序,而是在换行以后才能被读取到,提高容错性,当输错时,可以使用退格键重新编辑。
- 功能键:允许定义功能键。比如最常用的Ctrl+C,杀死前台进程,就是由终端来触发的。终端检测到Ctrl+C输入,会向前台进程组发送SIGTERM信号。
- 回车换行的转换:定义输入输出如何映射回车换行符。
- 输入输出流向控制:只有前台进程组能够从终端中读数据、而前台后台程序都能向终端写数据。当后台进程读取终端时,终端会向其发送 SIGTTIN 信号,导致进程被挂起。
获取终端名称-ttyname函数
#include <unistd.h>
char *ttyname(int fd);
参数说明
返回值
- 如果函数执行成功,返回指向包含终端设备名称的字符串的指针。
- 如果函数执行失败,函数返回 NULL。
通常情况下,我们可以将标准输入、标准输出或标准错误的文件描述符传递给 ttyname() 函数,以获取与终端设备相关联的名称。
注意
- ttyname() 函数的返回值是指向静态分配的缓冲区的指针,因此不应该尝试释放或修改这个指针指向的内存。
会话
会话是一个或多个进程组的集合。用户登录是一个会话的开始。登录之后,用户会得到一个跟用户使用的终端相连的进程,这个进程被称作是这个会话的leader,会话的ID就等于该进程的pid。由该进程fork出来的子进程都是这个会话的成员。
有如下特点:
- 一个会话只能有一个控制终端。
- 建立与控制终端连接的会话首进程被称为控制进程。
- 一个会话中只会有一个前台进程组,其他都是后台进程组。
- 终端退出将导致会话被关闭,如果一个会话关闭,则该会话下的所有进程组都会收到 SIGHUP 信号。
- 建立和改变终端与会话的联系只能由会话领导者(session leader)来进行。
查看会话ID-getsid()函数
getsid()函数用于获取指定进程的会话ID(SID)。该函数定义如下:
#include <unistd.h>
pid_t getsid(pid_t pid);
参数说明
返回值
- 如果函数执行成功,函数返回指定进程的会话ID。
- 如果函数执行失败,函数返回 -1,可以通过 errno 变量来获取具体的错误信息。
设置会话ID-setsid()函数
setsid()函数用于创建一个新的会话(session)并将调用进程设置为该会话的领头进程(session leader)。该函数定义如下:
#include <unistd.h>
pid_t setsid(void);
返回值
- 如果函数执行成功,函数返回新会话的会话ID。
- 如果函数执行失败,函数返回 -1,可以通过 errno 变量来获取具体的错误信息。
通过调用 setsid() 函数,进程可以脱离其当前的控制终端,成为一个新的会话的领头进程,这样就可以独立于原来的终端进行运行。
哪些进程无法成为领头进程:
- 当前进程已经是会话的领头进程。
- 当前进程是进程组中的组长进程。
想要成功设置会话ID,可以先调用fork,父进程终止,子进程调用setsid。
守护进程(Daemon)
守护进程是一种在后台运行、独立于控制终端的进程。通常在系统启动时启动,并在系统运行期间持续运行,执行特定的任务或提供特定的服务。
特点:
- 独立于控制终端:守护进程不受控制终端的影响,通常在后台运行。
- 无法终止:守护进程通常会捕获和忽略终端信号,以防止意外终止。
- 不输出到控制台:守护进程通常将输出写入日志文件,而非终端。
创建守护进程的步骤:
- 创建子进程,父进程退出。
- 调用 setsid() 函数,在子进程中创建新会话。
- 调用 chdir()函数,更改当前工作目录。进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。
- 重设文件权限掩码,为了不受终端信号影响,一般会调用 umask() 函数来设置文件掩码。
- 关闭文件描述符,关闭从父进程继承而来的文件描述符。
- 重定向标准输入、输出、错误:将标准输入、输出、错误重定向到 /dev/null 或日志文件。
- 执行守护进程,在完成上述步骤之后,守护进程可执行自己的任务或服务。
示例:
1 #include<stdio.h>
2 #include<fcntl.h>
3 #include<time.h>
4 #include<sys/stat.h>
5 #include<unistd.h>
6 #include<stdlib.h>
7 #include<sys/param.h>
8
9 void init_daemon()
10 {
11 pid_t pid = fork();
12 if(pid == -1)
13 {
14 perror("fork");
15 exit(-1);
16 }
17 else if(pid > 0)
18 {
19 exit(0);
20 }
21 setsid();
22 if(chdir("/") < 0)
23 {
24 perror("chdir");
25 exit(-1);
26 }
27
28 umask(0);
29 for (int i = 0; i < NOFILE; ++i)
30 {
31 close(i);
32 }
33 }
34
35 int main(int argc, char** argv)
36 {
37 init_daemon();
38 while (1)
39 {
40 FILE *fp = fopen("/my_daemonLog", "a+");
41 if(fp == NULL)
42 {
43 perror("fopen");
44 exit(-1);
45 }
46 time_t t;
47 time(&t);
48 fprintf(fp, "current time is : %s\n", ctime(&t));
49 fclose(fp);
50 sleep(1);
51 }
52 return 0;
53 }
编译运行:
$ gcc main.c
$ sudo ./a.out
根目录下会产生 my_daemonLog 文件,可以通过 cat 命令查看:
$ cat /my_daemonLog
current time is : Mon Mar 4 14:55:37 2024
current time is : Mon Mar 4 14:55:38 2024
如果要删除后台运行的进程: