.
.
.
.
.
目录
(一) 一起学 Unix 环境高级编程(APUE) 之 标准IO
(二) 一起学 Unix 环境高级编程(APUE) 之 文件 IO
(三) 一起学 Unix 环境高级编程(APUE) 之 文件和目录
(四) 一起学 Unix 环境高级编程(APUE) 之 系统数据文件和信息
(五) 一起学 Unix 环境高级编程(APUE) 之 进程环境
(六) 一起学 Unix 环境高级编程(APUE) 之 进程控制
(七) 一起学 Unix 环境高级编程(APUE) 之 进程关系 和 守护进程
进程关系是《APUE》第三版的第九章,这章的内容对我们来说通常意义不大,因为它终篇贯穿着一个概念,叫做“终端”,而现在已经几乎无法见到真正的终端了。
但是不了解这章就无法解释第13章(守护进程)的内容,大家要了解终端和进程之间关系才能了解守护进程是个什么东西。
进程关系与守护进程这两章内容不多,而且有诸多关联,所以我们把这两章的内容放到一起讨论。
先来看看几个概念。
1.终端
真正意义上的终端是“笨设备”,只能接收命令的输入并返回结果。你问它 1+1=? 它也不知道,它只能把你的问题传给计算机,再把计算机返回的结果显示给你。
它出现在计算机既昂贵又庞大的年代。那时候的计算机昂贵到了只有一部分公司买得起、另一部分公司买不起,而且有些公司只能买一台,买第二台就要破产了的程度。
所以这么昂贵的设备如果只能给一个人使用太浪费了,于是为了让计算机可以被多人使用,就出现了终端这种设备。
2.会话(Session)
一次成功的终端登录就是一个会话。现在一次 shell 的成功登录,相当于那时候终端的成功登录。会话相当于是进程组的容器,它能承载一个或多个进程组。
3.进程组
进程组用来承载进程,一个进程组中有一个或多个进程,它是一个或多个进程的集合(也可以看作是容器)。一个进程不但拥有唯一的 PID,同时也属于一个进程组。
如何产生一个进程组呢?很简单:
1 # 使用管道可以用一条命令产生一个进程组。 2 ls | more
进程组分为前台进程组和后台进程组。
一个会话中只能有一个前台进程组,也可以没有前台进程组。
终端设备(如键盘)只能与前台进程通讯,不能与后台进程通讯,根据约定,如果终端设备与一个后台进程关联,就会杀掉这个后台进程。
什么是前台进程组呢?比如你正在使用 tar 命令进行打包的时候是无法再输入其它命令的。如果 tar 命令执行的时间很长,我们就会在命令后面添加一个 & 参数,把它放到后台去运行。
ps(1) 命令的 SID(Session ID)列 就是程序运行的会话 ID。
进程是先出现的,后来人们发现进程可以拆分为多个小任务分别执行,于是便出现了线程的概念,这个到后面线程的章节会详细讨论。
如今进程已经退化为容器了,它的存在就是为了承载线程。PID 看似是进程号,实际上是线程在消耗它。
进程和线程只是我们的说法,内核中只能看到线程,内核所谓的进程管理其实就是线程管理,内核永远以线程为单位执行任务。
总结来说:会话用来承载进程组,进程组用来承载进程,进程用来承载线程。
第九章了解这几个概念就差不多了,还记得我们前面提到的 myshell 吗,用 fork(2) + exec(3) + wait(2) 来实现一个可以执行外部命令的 shell。如果你想实现一个支持内部命令的 shell 那么可以仔细学习一下第九章的内容,shell 内部命令处理的主要知识点都在第九章。我们这里就不对第九章讨论得那么详细了,感兴趣的小伙伴可以自己看看书,有什么疑问可以加入博客上方标注的邮件列表讨论。
4.setsid(2)
1 setsid - create session and set process group ID 2 3 #include <unistd.h> 4 5 pid_t setsid(void);
创建一个会话并设置进程组的ID。这个函数是我们在第 9 章最有价值的函数,没有这个函数,我们后面就无法创建守护进程。
调用者不能是进程组组长,调用者在调用之后自动变为新进程组组长,并且脱离控制终端,进程 ID 将被设为进程组 ID 和会话 ID,所以守护进程通常 PID、PGID、SID 是相同的。通常的用法是父进程 fork(2) 一个子进程,然后子进程调用 setsid(2) 将自己变成守护进程,父进程退出即可。
5.守护进程
守护进程的栗子我就不写了,因为《APUE》第三版 P375 图13-1 已经有详细的代码了,我针对书上的栗子总结一下。
守护进程的特点:
1)脱离控制终端,ps(1) axj tty 为问号(?)。
2)是进程组的 leader,也就是 PID 和 PGID 相同。
3)通常没有父进程,由 1 号 init 接管。
4)创建了一个新会话,是 session 的 leader,所以 PID 与 SID 相同。
使用 ps(1) axj 命令查看,PID、PGID、SID 相同的进程就是守护进程。
守护进程也可以使用标准输出,但是不符合常理了,因为守护进程没有控制终端,所以守护进程一般会关闭或重定向标准输入输出流。
写守护进程的时候我们会切换工作路径,把它切换到一个一定会存在的路径,比如 /。因为假设你的守护进程是在一个可卸载设备(如U盘)上被启动的,如果不修改工作路径,该设备无法被卸载。
调用 umask(2) 是为了将文件模式创建掩码设置为一个已知值,因为通过继承得来的掩码可能会被设置为拒绝某些权限,如果守护进程中需要这些权限则要设置它。
对于书上的栗子,有两点要吐槽:
1)SIGHUP 信号用于通知服务进程软重启,比如修改了某服务的配置文件之后可以通过给服务进程发 SIGHUP 信号使它重新读取配置文件,所以如果没有特殊要求不必忽略该信号。
2)如果没有特殊要求,不必关闭所有的文件描述符,仅关闭标准输入、标准输出和标注错误即可。
6.系统日志
守护进程不应使用标准输出,那么当守护进程需要记录一些事件或者是错误的时候怎么办呢?那就要采用系统日志了。
系统日志一般保存在 /var/log/ 目录下,但是这个目录下的日志文件权限几乎都是只有 root 才能读写,那么普通用户的日志如何写入呢?这就需要借助系统日志函数来写日志了。
root 用户授权给 syslogd 服务专门写日志,然后其它程序都需要通过封装好的一系列函数调用 syslogd 服务来记录日志。这样就提高了日志的安全性了,可以防止日志文件被非法篡改。
1 closelog, openlog, syslog - send messages to the system logger 2 3 #include <syslog.h> 4 5 void openlog(const char *ident, int option, int facility); 6 void syslog(int priority, const char *format, ...); 7 void closelog(void);
openlog(3) 函数并非是打开日志文件,而是与 syslogd 服务建立链接,表示当前进程要写日志。
参数列表:
ident:表明自己的身份,由程序员自行指定,写什么都行。
option:要在日志中附加什么内容,多个选项用按位或链接。LOG_PID 是附加 PID,这个是最常用的。
facility:消息来源。一般只能指定一个。
消息来源 | 含义 |
LOG_CRON | 消息来自定时任务 |
LOG_DAEMON | 消息来自守护进程 |
LOG_FTP | 消息来自 FTP 服务 |
LOG_KERN | 消息来自内核 |
LOG_USER | 默认,常规用户级别消息 |
表1 facility 参数的常见需选项
syslog(3) 函数用于提交日志内容,参数列表:
priority:优先级。详见下表:
级别 | 含义 |
LOG_EMERG | 「严重」导致系统不可用的问题 |
LOG_ALERT | 「严重」必须立即处理的情况 |
LOG_CRIT | 「严重」临界条件 |
LOG_ERR | 「严重」错误 |
LOG_WARNING | 警告 |
LOG_NOTICE | 正常 |
LOG_INFO | 信息 |
LOG_DEBUG | 调试 |
表2 日志优先级
以 LOG_ERR 为分界线,如果遇到了程序无法继续运行的问题,要报 LOG_ERR 以上的级别(包括 LOG_ERR)。
如果遇到的问题不会影响程序继续运行,报 LOG_ERR 以下级别的就可以了。
日志太多肯定对磁盘空间的要求就比较高,而且无用的日志太多会影响日志审计。日志文件中会记录哪些级别的日志是在配置文件中配置的,默认的情况是 LOG_DEBUG 以上级别的日志都会被记录。
format:类似于 printf(3) 函数的格式化字符串。注意不要使用转义字符 \n,否则日志中会记录一个字符串"\n"而不是记录一个换行符。
...:format 中占位符的参数。
closelog(3) 表示日志写入结束。
7.单实例守护进程
有些守护进程需要在同一时间只能有一个实例在运行,它们称为单实例守护进程。它们在启动的时候会在 /var/run 下面创建一个进程锁文件,守护进程启动的时候会先判断这个锁文件是否存在,如果已存在就报错并退出,如果不存在就继续运行并创建一个锁文件,退出的时候再删除它。
守护进程如果想要开机自动启动,可以配置到自动启动脚本中:/etc/rc.d/rc.local