进程关系

进程关系

进程组

进程组概念

在 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改为负数,例如:

kill -9 -30

通过上面命令,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

为什么需要终端,直接从键盘和显示器中读取数据不行吗?终端实际上实现了下列功能:

  1. 回显:可以让输入字符自动被回显到终端的输出上,如果没有该功能,则会极大影响用户体验。
  2. 行编辑:用户输入的字符不是立马送到应用程序,而是在换行以后才能被读取到,提高容错性,当输错时,可以使用退格键重新编辑。
  3. 功能键:允许定义功能键。比如最常用的Ctrl+C,杀死前台进程,就是由终端来触发的。终端检测到Ctrl+C输入,会向前台进程组发送SIGTERM信号。
  4. 回车换行的转换:定义输入输出如何映射回车换行符。
  5. 输入输出流向控制:只有前台进程组能够从终端中读数据、而前台后台程序都能向终端写数据。当后台进程读取终端时,终端会向其发送 SIGTTIN 信号,导致进程被挂起。

获取终端名称-ttyname函数

#include <unistd.h>

char *ttyname(int fd);

参数说明

  • 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);

参数说明

  • pid:进程ID,如果指定为0,表示当前进程。

返回值

  • 如果函数执行成功,函数返回指定进程的会话ID。
  • 如果函数执行失败,函数返回 -1,可以通过 errno 变量来获取具体的错误信息。

设置会话ID-setsid()函数

setsid()函数用于创建一个新的会话(session)并将调用进程设置为该会话的领头进程(session leader)。该函数定义如下:

#include <unistd.h>

pid_t setsid(void);

返回值

  • 如果函数执行成功,函数返回新会话的会话ID。
  • 如果函数执行失败,函数返回 -1,可以通过 errno 变量来获取具体的错误信息。

通过调用 setsid() 函数,进程可以脱离其当前的控制终端,成为一个新的会话的领头进程,这样就可以独立于原来的终端进行运行。

哪些进程无法成为领头进程:

  • 当前进程已经是会话的领头进程。
  • 当前进程是进程组中的组长进程。

想要成功设置会话ID,可以先调用fork,父进程终止,子进程调用setsid。

守护进程(Daemon

守护进程是一种在后台运行、独立于控制终端的进程。通常在系统启动时启动,并在系统运行期间持续运行,执行特定的任务或提供特定的服务。

特点:

  • 独立于控制终端:守护进程不受控制终端的影响,通常在后台运行。
  • 无法终止:守护进程通常会捕获和忽略终端信号,以防止意外终止。
  • 不输出到控制台:守护进程通常将输出写入日志文件,而非终端。

创建守护进程的步骤:

  1. 创建子进程,父进程退出。
  2. 调用 setsid() 函数,在子进程中创建新会话。
  3. 调用 chdir()函数,更改当前工作目录。进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。
  4.  重设文件权限掩码,为了不受终端信号影响,一般会调用 umask() 函数来设置文件掩码。
  5. 关闭文件描述符,关闭从父进程继承而来的文件描述符。
  6. 重定向标准输入、输出、错误:将标准输入、输出、错误重定向到 /dev/null 或日志文件。
  7. 执行守护进程,在完成上述步骤之后,守护进程可执行自己的任务或服务。

示例:

 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

如果要删除后台运行的进程:

$ sudo killall ./a.out

 

posted @ 2024-03-07 20:14  西兰花战士  阅读(16)  评论(0编辑  收藏  举报