LV05-01-进程-04-进程的关系与守护进程
本文主要是进程——进程的关系与守护进程的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows | windows11 |
Ubuntu | Ubuntu16.04的64位版本 |
VMware® Workstation 16 Pro | 16.2.3 build-19376536 |
SecureCRT | Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日 |
开发板 | 正点原子 i.MX6ULL Linux阿尔法开发板 |
uboot | NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03) |
linux内核 | linux-4.15(NXP官方提供) |
STM32开发板 | 正点原子战舰V3(STM32F103ZET6) |
点击查看本文参考资料
参考方向 | 参考原文 |
--- | --- |
点击查看相关文件下载
--- | --- |
一、进程的关系
1. 无关系
两个进程之间没有任何关系,相互独立。
2. 父子进程
两个进程间构成父子进程关系,例如一个进程创建出了另一个进程,那么这两个进程间就构成了父子进程关系,如果“生父”先于子进程结束,那么 init 进程(“养父”)就会成为子进程的父进程,它们之间同样也是父子进程关系。
3. 进程组
3.1 进程组概念
每个进程除了有一个进程 ID 、父进程 ID 之外,还有一个进程组 ID ,用于标识该进程属于哪一个进程组,进程组( Process Group )是一个或多个进程的集合,这些进程并不是孤立的,它们彼此之间或者存在父子、兄弟关系,或者在功能上有联系。
【注意】
(1)每个进程必定属于某一个进程组、且只能属于一个进程组。
(2)每一个进程组有一个组长进程,组长进程的 ID 就等于进程组 ID ,组长进程不能再创建新的进程组。
(3)在组长进程的 ID 前面加上一个负号即是操作进程组。
(4)只要进程组中还存在一个进程,则该进程组就存在,这与其组长进程是否终止无关。
(5)默认情况下,新创建的进程会继承父进程的进程组 ID 。
(6)一个进程组可以包含一个或多个进程,进程组的生命周期从被创建开始,到其内所有进程终止或离开该进程组。
3.2 进程组常用函数
3.2.1 getpgid()
3.2.1.1 函数说明
在 linux 下可以使用 man getpgid 命令查看该函数的帮助手册。
1 |
|
【函数说明】该函数会获取指定进程所属的进程组 ID 。
【函数参数】
- pid : pid_t 类型,指定的进程 PID ,参数 pid 为 0 表示获取调用者进程的进程组 ID 。
【返回值】返回值为 pid_t 类型,成功则返回 pid 进程所属的进程组 ID ,失败则返回 -1 ,并设置 errno 。
【注意事项】 none
3.2.1.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
程序执行后,终端将会显示以下信息:
1 | This process's gid: 7986 |
每次运行获得的 ID 可能会不同。
3.2.2 getpgrp()
3.2.2.1 函数说明
在 linux 下可以使用 man getpgrp 命令查看该函数的帮助手册。
1 |
|
【函数说明】该函数会获取指定进程所属的进程组 ID 。
【函数参数】 none
【返回值】返回值为 pid_t 类型,成功则返回当前进程所属的进程组 ID ,失败则返回 -1 ,并设置 errno 。
【注意事项】 getpgrp() 就等价于 getpgid(0) 。
3.2.2.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
程序执行后,终端将会显示以下信息:
1 | Process group ID<8021>---getpgrp() |
每次运行获得的 ID 可能会不同。
3.2.3 setpgid()
3.2.3.1 函数说明
在 linux 下可以使用 man setpgid 命令查看该函数的帮助手册。
1 |
|
【函数说明】该函数将 pid 进程的进程组 ID 设置成 pgid 。
【函数参数】
- pid : pid_t 类型,需要修改进程组的进程的 PID 。
- pgid : pid_t 类型,要设置目标的进程组 ID 。
【返回值】返回值为 int 类型,成功则返回目标进程组 ID ,失败则返回 -1 ,并设置 errno 。
【注意事项】
(1)如果函数两个参数相等( pid == gpid ),则由 pid 指定的进程变成为进程组的组长进程,创建了一个新的进程;如果参数pid等于0,则使用调用者的进程ID;另外,如果参数gpid等于0,则创建一个新的进程组,由参数pid指定的进程作为进程组组长进程。
3.2.3.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
程序执行后,终端将会显示以下信息:
1 | Process group ID<8021>---getpgrp() |
每次运行获得的 ID 可能会不同。
4. 会话
4.1 相关概念
会话( Session ):是一个或多个进程组的集合,其与进程组、进程之间的关系如下图所示:
控制终端( Controlling Terminal ):每个会话可以有一个单独的控制终端,与控制终端连接的 Leader 就是控制进程( Controlling Process )。
一个会话可包含一个或多个进程组,但只能有一个前台进程组,其它的是后台进程组;每个会话都有一个会话首领( leader ),即创建会话的进程。一个会话可以有控制终端、也可没有控制终端,在有控制终端的情况下也只能连接一个控制终端,这通常是登录到其上的终端设备(在终端登录情况下)或伪终端设备(例如通过 SSH 协议网络登录),一个会话中的进程组可被分为一个前台进程组以及一个或多个后台进程组。
会话的首领进程连接一个终端之后,该终端就成为会话的控制终端,与控制终端建立连接的会话首领进程被称为控制进程;产生在终端上的输入和信号将发送给会话的前台进程组中的所有进程,例如 Ctrl + C (产生 SIGINT 信号)、 Ctrl + Z (产生 SIGTSTP 信号)、 Ctrl + \ (产生 SIGQUIT 信号)等等这些由控制终端产生的信号。
当用户在某个终端登录时,一个新的会话就开始了;当我们在 Linux 系统下打开了多个终端窗口时,实际上就是创建了多个终端会话。一个进程组由组长进程的 ID 标识,而对于会话来说,会话的首领进程的进程组 ID 将作为该会话的标识,也就是会话 ID ( sid ),在默认情况下,新创建的进程会继承父进程的会话 ID 。
4.2 常用函数
4.2.1 getsid()
4.2.1.1 函数说明
在 linux 下可以使用 man getsid 命令查看该函数的帮助手册。
1 |
|
【函数说明】通过系统调用该函数获取进程的会话 ID 。
【函数参数】
- pid : pid_t 类型,如果参数 pid 为 0 ,则返回调用者进程的会话 ID ;如果参数 pid 不为 0 ,则返回参数 pid 指定的进程对应的会话 ID 。
【返回值】返回值为 pid_t 类型,成功返回会话 ID ,失败则返回 -1 、并设置 errno 。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】 none
4.2.1.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
程序执行后,终端将会显示以下信息:
1 | Session ID<11992> |
4.2.2 setsid()
4.2.2.1 函数说明
在 linux 下可以使用 man 3 setsid 命令查看该函数的帮助手册。
1 |
|
【函数说明】创建一个新的会话。
【函数参数】 none
【返回值】返回值为 pid_t 类型,成功将返回新会话的会话 ID ;失败将返回 -1 ,并设置 errno 。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】如果调用者进程不是进程组的组长进程,调用 setsid() 将创建一个新的会话,调用者进程是新会话的首领进程,同样也是一个新的进程组的组长进程,调用 setsid() 创建的会话将没有控制终端。也就是说,这个函数有以下作用:
(1)让进程摆脱原会话的控制
(2)让进程摆脱原进程组的控制
(3)让进程摆脱原控制终端的控制
4.2.2.2 使用实例
主要是在创建守护进程的时候使用,例子看下边守护进程一节的例子就可以啦。
二、守护进程
1. 什么是守护进程?
守护进程( Daemon Process )是 Linux 三种进程类型之一,也称为精灵进程,是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些事情的发生,主要有以下特点:
- 长期运行。守护进程是一种生存期很长的一种进程,它们一般在系统启动时开始运行,除非强行终止,否则直到系统关机都会保持运行。与守护进程相比,普通进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但守护进程不受用户登录注销的影响,它们将会一直运行着、直到系统关机。
- 与控制终端脱离。在 Linux 中,系统与用户交互的界面称为终端,每一个从终端开始运行的进程都会依附于这个终端,也就是会话的控制终端。当控制终端被关闭的时候,该会话就会退出,由控制终端运行的所有进程都会被终止,这使得普通进程都是和运行该进程的终端相绑定的;但守护进程能突破这种限制,它脱离终端并且在后台运行,脱离终端的目的是为了避免进程在运行的过程中的信息在终端显示并且进程也不会被任何终端所产生的信息所打断。
- 周期性的执行某种任务或等待处理特定事件。
Linux 中大多数服务器就是用守护进程实现的,例如, Internet 服务器 inetd 、 Web 服务器 httpd 等。同时,守护进程完成许多系统任务,譬如作业规划进程 crond 等。
守护进程 Daemon ,通常简称为 d ,一般进程名后面带有 d 就表示它是一个守护进程。守护进程与终端无任何关联,用户的登录与注销与守护进程无关、不受其影响,守护进程自成进程组、自成会话,即 pid = gid = sid 。在终端执行以下命令:
1 | ps -ajx |
会有如下输入信息:
1 | PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND |
TTY 一栏是问号 ? 表示该进程没有控制终端,也就是守护进程,其中 COMMAND 一栏使用中括号 [] 括起来的表示内核线程,这些线程是在内核里创建,没有用户空间代码,因此没有程序文件名和命令行,通常采用 k 开头的名字,表示 Kernel 。
2. 创建守护进程
2.1 创建步骤
- (1)创建子进程,终止父进程
父进程调用 fork() 创建子进程,然后父进程使用 exit() 退出,然后子进程变成孤儿进程,被 init 进程(字符模式的话)收养,子进程在后台运行。
这样做,第一,如果该守护进程是作为一条简单地 shell 命令启动,那么父进程终止会让 shell 认为这条命令已经执行完毕。第二,虽然子进程继承了父进程的进程组 ID ,但它有自己独立的进程 ID ,这保证了子进程不是一个进程组的组长进程,这是后边要调用 setsid 函数的先决条件。
- (2)子进程调用 setsid() 创建会话
在子进程中调用 setsid() 函数创建新的会话,由于之前子进程并不是进程组的组长进程,所以调用 setsid() 会使得子进程创建一个新的会话,子进程成为新会话的首领进程,同样也创建了新的进程组、子进程成为组长进程,此时创建的会话将没有控制终端。所以这里调用 setsid 有三个作用:让子进程摆脱原会话的控制、让子进程摆脱原进程组的控制和让子进程摆脱原控制终端的控制。
在调用 fork 函数时,子进程继承了父进程的会话、进程组、控制终端等,虽然父进程退出了,但原先的会话期、进程组、控制终端等并没有改变,因此,那还不是真正意义上使两者独立开来。 setsid 函数能够使子进程完全独立出来,从而脱离所有其他进程的控制。
- (3)将工作目录更改为根目录
子进程是继承了父进程的当前工作目录,由于在进程运行中,当前目录所在的文件系统是不能卸载的,这对以后使用会造成很多的麻烦。因此通常的做法是让 / (根目录)作为守护进程的当前目录,当然也可以指定其它目录来作为守护进程的工作目录。
- (4)重设文件权限掩码 umask
文件权限掩码 umask 用于对新建文件的权限位进行屏蔽。由于使用 fork 函数新建的子进程继承了父进程的文件权限掩码,这就给子进程使用文件带来了诸多的麻烦。因此,把文件权限掩码设置为 0 ,确保子进程有最大操作权限、这样可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是 umask ,通常的使用方法为 umask(0) 。
- (5)关闭不再需要的文件描述符
子进程继承了父进程的所有文件描述符,这些被打开的文件可能永远不会被守护进程(此时守护进程指的就是子进程,父进程退出、子进程成为守护进程)读或写,但它们一样消耗系统资源,可能导致所在的文件系统无法卸载,所以必须关闭这些文件,这使得守护进程不再持有从其父进程继承过来的任何文件描述符。
- (6)将文件描述符号为 0 、 1 、 2 定位到 /dev/null 或者直接关闭也可以
将守护进程的标准输入、标准输出以及标准错误重定向到 /dev/null ,这使得守护进程的输出无处显示、也无处从交互式用户那里接收输入。
- (7)其它操作:忽略 SIGCHLD 信号
处理 SIGCHLD 信号不是必须的,但对于某些进程,特别是并发服务器进程往往是特别重要的,服务器进程在接收到客户端请求时会创建子进程去处理该请求,如果子进程结束之后,父进程没有去 wait 回收子进程,则子进程将成为僵尸进程;如果父进程 wait 等待子进程退出,将又会增加父进程的负担、也就是增加服务器的负担,影响服务器进程的并发性能,在 Linux 下,可以将 SIGCHLD 信号的处理方式设置为 SIG_IGN ,也就是忽略该信号,可让内核将僵尸进程转交给 init 进程去处理,这样既不会产生僵尸进程、又省去了服务器进程回收子进程所占用的时间。
2.2 相关函数
2.2.1 getcwd()
2.2.1.1 函数说明
在 linux 下可以使用 man getcwd 命令查看该函数的帮助手册。
1 |
|
【函数说明】获取当前工作目录。
【函数参数】
- buf : char * 类型,参数 buf 为保存当前工作目录绝对路径的字符型指针。
- size : size_t 类型,为 buf 的长度。
【返回值】返回值为 char * 类型,成功返回当前工作目录绝对路径存储区域的首地址,失败则返回 NULL 、并设置 errno 。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】 none
2.2.1.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
程序执行后,终端将会显示以下信息:
1 | path=/mnt/hgfs/Sharedfiles |
2.2.2 chdir()
2.2.2.1 函数说明
在 linux 下可以使用 man chdir 命令查看该函数的帮助手册。
1 |
|
【函数说明】更改当前工作目录到 path 所指向的目录。
【函数参数】
- path : const char * 类型,需要修改到的目标目录。
【返回值】返回值为 int 类型,成功返回 0 ,失败则返回 -1 、并设置 errno 。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】 none
2.2.2.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
程序执行后,终端将会显示以下信息:
1 | path=/mnt/hgfs/Sharedfiles |
2.2.3 sysconf()
2.2.3.1 函数说明
在 linux 下可以使用 man sysconf 命令查看该函数的帮助手册。
1 |
|
【函数说明】获取系统执行的配置信息。
【函数参数】
- name : int 类型,配置项的宏名称。
点击查看常用配置项宏名称
_SC_PAGESIZE | 查看缓存内存页面的大小 |
_SC_PHYS_PAGES | 查看内存的总页数 |
_SC_AVPHYS_PAGES | 查看可以利用的总页数 |
_SC_NPROCESSORS_CONF | 查看cpu的个数 |
_SC_NPROCESSORS_ONLN | 查看在使用的cpu个数 |
_SC_LOGIN_NAME_MAX | 查看最大登录名长度 |
_SC_HOST_NAME_MAX | 查看最大主机长度 |
_SC_OPEN_MAX | 每个进程运行时打开的最大文件数目 |
_SC_CLK_TCK | 查看每秒中跑过的运算速率 |
【返回值】返回值为 long 类型,返回值有以下情况:
- 如果 name 对应于一个最大或最小限制,并且该限制是不确定的,则返回 -1 ,并且不更改 errno 。
- 如果发生错误,则返回 -1 ,并设置 errno 来指示错误的原因。
- 如果 name 对应一个选项,则支持该选项时返回正值,不支持该选项时返回 -1 。
- 否则,返回选项或限制的当前值。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】 none
2.2.3.2 使用实例
点击查看实例
1 |
|
在终端执行以下命令:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
程序执行后,终端将会显示以下信息:
1 | The number of processors configured is :2 |
3. 创建守护进程实例
3.1 创建实例1
点击查看实例1
1 |
|
在终端执行以下命令:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
程序执行后,终端将会显示以下信息:
1 | before father fork! |
然后我们会发现程序正常结束了,接下来我们输入以下命令查看 a.out 进程情况:
1 | ps -ajx|grep a.out |
然后会看到如下信息:
1 | 935 18395 18395 18395 ? -1 Ss 1000 0:00 ./a.out |
发现 a.out 进程的 TTY 已经变成了 ? ,这就表示它已经是一个守护进程了。
3.2 创建实例2
创建守护进程,每隔1秒将系统时间写入文件 time.log 。
点击查看实例1
1 |
|
在终端执行以下命令:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
程序执行后,终端将会显示以下信息:
1 | before father fork! |
然后我们会发现程序正常结束了,接下来我们输入以下命令查看 time.log 文件内容:
1 | vim ~/time.log |
然后会看到如下信息:
1 | Mon May 23 19:00:59 2022 |
会发现,守护进程的存在系统会每隔 1s 向该文件写入一次时间。