进程关系

2026-07-10 19:38:35

世界杯歌

终端 终端是用户与 Linux 内核之间进行交互的桥梁,用户通过终端可以向系统发出命令并查看系统的输出。 当Linux中,控制终端(Controlling Terminal...

终端

终端是用户与 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

char *ttyname(int fd);

参数说明

fd:一个与终端相关联的文件描述符。

返回值

如果函数执行成功,返回指向包含终端设备名称的字符串的指针。

如果函数执行失败,函数返回 NULL。

通常情况下,我们可以将标准输入、标准输出或标准错误的文件描述符传递给 ttyname() 函数,以获取与终端设备相关联的名称。

注意

ttyname() 函数的返回值是指向静态分配的缓冲区的指针,因此不应该尝试释放或修改这个指针指向的内存。

会话

会话是一个或多个进程组的集合。用户登录是一个会话的开始。登录之后,用户会得到一个跟用户使用的终端相连的进程,这个进程被称作是这个会话的leader,会话的ID就等于该进程的pid。由该进程fork出来的子进程都是这个会话的成员。

有如下特点:

一个会话只能有一个控制终端。

建立与控制终端连接的会话首进程被称为控制进程。

一个会话中只会有一个前台进程组,其他都是后台进程组。

终端退出将导致会话被关闭,如果一个会话关闭,则该会话下的所有进程组都会收到 SIGHUP 信号。

建立和改变终端与会话的联系只能由会话领导者(session leader)来进行。

查看会话ID-getsid()函数

getsid()函数用于获取指定进程的会话ID(SID)。该函数定义如下:

#include

pid_t getsid(pid_t pid);

参数说明

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

返回值

如果函数执行成功,函数返回指定进程的会话ID。

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

设置会话ID-setsid()函数

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

#include

pid_t setsid(void);

返回值

如果函数执行成功,函数返回新会话的会话ID。

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

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

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

当前进程已经是会话的领头进程。

当前进程是进程组中的组长进程。

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

守护进程(Daemon)

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

特点:

独立于控制终端:守护进程不受控制终端的影响,通常在后台运行。

无法终止:守护进程通常会捕获和忽略终端信号,以防止意外终止。

不输出到控制台:守护进程通常将输出写入日志文件,而非终端。

创建守护进程的步骤:

创建子进程,父进程退出。

调用 setsid() 函数,在子进程中创建新会话。

调用 chdir()函数,更改当前工作目录。进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。

重设文件权限掩码,为了不受终端信号影响,一般会调用 umask() 函数来设置文件掩码。

关闭文件描述符,关闭从父进程继承而来的文件描述符。

重定向标准输入、输出、错误:将标准输入、输出、错误重定向到 /dev/null 或日志文件。

执行守护进程,在完成上述步骤之后,守护进程可执行自己的任务或服务。

示例:

1 #include

2 #include

3 #include

4 #include

5 #include

6 #include

7 #include

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