Linux/UNIX 系统编程手册-下册(部分章节)

发布时间 2023-05-04 07:15:42作者: Theseus‘Ship
634.1 概述
进程组由一个或多个共享同一进程组标识符(PGID)的进程组成。进程组ID是一个数字,其类型与进程ID一样(pid_t)。一个进程组拥有一个进程组首进程,该进程是创建该组的进程,其进程ID为该进程组的ID,新进程会继承其父进程所属的进程组ID。
进程组拥有一个生命周期,其开始时间为首进程创建组的时刻,结束时间为最后一个成员进程退出组的时刻。一个进程可能会因为终止而退出进程组,也可能会因为加入了另外一个进程组而退出进程组。进程组首进程无需是最后一个离开进程组的成员。
会话是一组进程组的集合。进程的会话成员关系是由其会话标识符(SID)确定的,会话标识符与进程组ID一样,是一个类型为pid_t的数字。会话首进程是创建该新会话的进程,其进程ID会成为会话ID。新进程会继承其父进程的会话ID。
一个会话中的所有进程共享单个控制终端。控制终端会在会话首进程首次打开一个终端设备时被建立。一个终端最多可能会成为一个会话的控制终端。
在任一时刻,会话中的其中一个进程组会成为终端的前台进程组,其他进程组会成为后台进程组。只有前台进程组中的进程才能从控制终端中读取输入。当用户在控制终端中输入其中一个信号生成终端字符之后,该信号会被发送到前台进程组中的所有成员。这些字符包括生成 SIGINT的中断字符(通常是Control-C)、生成SIGQUIT的退出字符(通常是Control-\)、生成SIGSTP 的挂起字符(通常是Control-Z)。
当到控制终端的连接建立起来(即打开)之后,会话首进程会成为该终端的控制进程。成为控制进程的主要标志是当断开与终端之间的连接时内核会向该进程发送一个SIGHUP 信号。


通过检查Linux 特有的/proc/PID/stat文件,就能确定任意进程的进程组ID和会话ID此外,还能确定进程的控制终端的设备ID(一个十进制数字,包含主ID和辅ID)和控制该终端的控制进程的进程ID。更多细节信息请参考proc(5)手册。
会话和进程组的主要用途是用于shell作业控制。


在窗口环境中,控制终端是一个伪终端。每个终端窗口都有一个独立的会话,窗口的启动shell是会话首进程和终端的控制进程。
在除任务控制之外的其他场景中也有可能用到进程组,因为进程组具备两个有用的属性:在特定的进程组中父进程能够等待任意子进程(参见26.12节)和信号能够被发送给进程组中的所有成员(参见20.5节)。
~
getpgrp() 获取一个进程的进程组ID
setpgid() 将进程ID为pid的进程的进程组ID修改为pgid


如果将pid的值设置为0,那么调用进程的进程组ID就会被改变。如果将pgid的值设置为0,那么ID为pid的进程的进程组ID会被设置成pid的值。因此,下面的setpgid()调用是等价的。
setpgid(0,0);
setpgid(getpid(),0);
setpgid(getpid(), getpid());
如果pid和pgid参数指定了同一个进程(即pgid是0或者与ID为pid的进程的进程ID匹配),那么就会创建一个新进程组,并且指定的进程会成为这个新组的首进程(即进程的进程组ID与进程ID是一样的)。如果两个参数的值不同(即pgid不是0或者与ID为pid的进程的进程ID不匹配),那么setpgid()调用会将一个进程从一个进程组中移到另一个进程组中。
通常调用setpgid()(以及34.3节中介绍的setsid())函数的是shell和login(1)。在37.2节中将会看到一个程序在使自己变成daemon的过程中也会调用setsid()。
在调用setpgid()时存在以下限制。
* pid参数可以仅指定调用进程或其中一个子进程。违反这条规则会导致ESRCH错误。
* 在组之间移动进程时,调用进程、由pid指定的进程(可能是另外一个进程,也可能就是调用进程)以及目标进程组必须要属于同一个会话。违反这条规则会导致EPERM错误。
* pid 参数所指定的进程不能是会话首进程。违反这条规则会导致EPERM错误。
* 一个进程在其子进程已经执行exec()后就无法修改该子进程的进程组ID了。违反这条规则会导致EACCES错误。之所以会有这条约束条件的原因是在一个进程开始执行之后再修改其进程组ID的话会使程序变得混乱。


~
getsid()
setsid()


ioctl(fd, TIOCNOTTY)
~
ctermid() 获取表示控制终端的路径名
通常会生成字符串/dev/tty
~
作业控制
34.7.1 在shell中使用作业控制
当输入的命令以&符号结束时,该命令会作为后台任务运行,如下面的示例所示。
$ grep -r SIGHUP /usr/src/linux >x&
[1] 18932
$ sleep 60 &   Job 1: process running grep has PID 18932
[2] 18934   Job 2: process running sleep has PID 18934
shell会为后台的每个进程赋一个唯一的作业号。当作业在后台运行之后以及在使用各种作业控制命令操作或监控作业时作业号会显示在方括号中。作业号后面的数字是执行这个命令的进程的进程ID或管道中最后一个进程的进程ID。在后面几个段落中介绍的命令中会使用%num来引用作业,其中num是shell赋给作业的作业号。
在很多情况下是可以省略%num的,当省略%num时默认指当前作业。当前作业是在前台最新被停止的作业(使用下面介绍的挂起字符)或者如果没有这样的作业的话,最新作业是在后台启动的任务。
不同shell确定哪个后台作业为当前作业的细节方面稍微有些不同。)另外,%%和%+符号指的是当前作业,%-符号指的是上一个当前作业。在 jobs 命令的输出中,当前的和上一个当前作业分别用加号(+)和减号(一)标记。
jobs 是shell内置的一个命令,它会列出所有后台作业。
fg 将后台作业移动到前台
bg
stty


~
DAEMON
37.1 概述
daemon 是一种具备下列特征的进程。
* 它的生命周期很长。通常,一个daemon 会在系统启动的时候被创建并一直运行直至系统被关闭。
* 它在后台运行并且不拥有控制终端。控制终端的缺失确保了内核永远不会为daemon自动生成任何任务控制信号以及终端相关的信号(如SIGINT、SIGTSTP和SIGHUP)。
daemon 是用来执行特殊任务的,如下面的示例所示。
* cron:一个在规定时间执行命令的daemon。
* sshd:安全shell daemon,允许在远程主机上使用一个安全的通信协议登录系统。
* httpd:HTTP服务器daemon(Apache),它用于服务Web页面。
* inetd:Internet 超级服务器daemon(参见60.5节),它监听从指定的TCP/IP端口上进入的网络连接并启动相应的服务器程序来处理这些连接。
很多标准的daemon 会作为特权进程运行(即有效用户ID为0),因此在编写daemon程序时应该遵循第38章中给出的指南。
通常会将daemon 程序的名称以字母d结尾(但并不是所有人都遵循这个惯例)。


在Linux 上,特定的daemon 会作为内核线程运行。实现此类daemon的代码是内核的部分,它们通常在系统启动的时候被创建。当使用 ps(1)列出线程时,这些daemon的名称会用方括号([])括起来。其中一个内核线程是pdflush,它会定期将脏页面(即高速缓冲区中的页面)写入磁盘。
~
37.2 创建一个 daemon
要变成daemon,一个程序需要完成下面的步骤。


1.执行一个fork(),之后父进程退出,子进程继续执行。(结果是daemon成为了init进程的子进程。)之所以要做这一步是因为下面两个原因。
* 假设daemon是从命令行启动的,父进程的终止会被shell发现,shell在发现之后会显示出另一个shell 提示符并让子进程继续在后台运行.
* 子进程被确保不会成为一个进程组首进程,因为它从其父进程那里继承了进程组ID并且拥有了自己的唯一的进程ID,而这个进程ID与继承而来的进程组ID是不同的,这样才能够成功地执行下面一个步骤。
2.子进程调用setsid()(参见34.3节)开启一个新会话并释放它与控制终端之间的所有关联关系。
3.如果daemon 从来没有打开过终端设备,那么就无需担心 daemon会重新请求一个控制终端了。如果daemon后面可能会打开一个终端设备,那么必须要采取措施来确保这个设备不会成为控制终端。这可以通过下面两种方式实现。
* 在所有可能应用到一个终端设备上的open()调用中指定O_NOCTTY标记。
* 或者更简单地说,在setsid()调用之后执行第二个fork(),然后再次让父进程退出并让孙子进程继续执行。这样就确保了子进程不会成为会话组长,因此根据SystemV中获取终端的规则(Linux 也遵循了这个规则),进程永远不会重新请求一个控制终端(参见34.4节)。


在遵循BSD规则的实现中,一个进程只能通过一个显式的ioctl() TIOCSCTTY操作来获取一个控制终端,因此第二个fork()调用对控制终端的获取并没有任何影响,但多一个fork()调用不会带来任何坏处。
4.清除进程的umask(参见15.4.6节)以确保当daemon创建文件和目录时拥有所需的权限。
5.修改进程的当前工作目录,通常会改为根目录(/)。这样做是有必要的,因为daemon通常会一直运行直至系统关闭为止。如果daemon的当前工作目录为不包含/的文件系统,那么就无法卸载该文件系统(参见14.8.2节)。或者daemon可以将工作目录改为完成任务时所在的目录或在配置文件中定义的一个目录,只要包含这个目录的文件系统永远不会被卸载即可。如cron会将自身放在/var/spool/cron目录下。
6.关闭daemon从其父进程继承而来的所有打开着的文件描述符。(daemon可能需要保持继承而来的文件描述的打开状态,因此这一步是可选的或者是可变更的。)之所以需要这样做的原因有很多。由于daemon失去了控制终端并且是在后台运行的,因此让 daemon 保持文件描述符0、1和2的打开状态毫无意义,因为它们指向的就是控制终端。此外,无法卸载长时间运行的daemon打开的文件所在的文件系统。因此,通常的做法是关闭所有无用的打开着的文件描述符,因为文件描述符是一种有限的资源。


一些UNIX 实现(如 Solaris 9 和一些最新的BSD发行版)提供了一个名为closefrom(n)(或类似的名称)的函数,它关闭所有大于或等于 n的文件描述符。Linux 上并不存在这个函数。


7.在关闭了文件描述符0、1和2之后,daemon 通常会打开/dev/null并使用dup2()(或类似
的函数)使所有这些描述符指向这个设备。之所以要这样做是因为下面两个原因。
* 它确保了当daemon 调用了在这些描述符上执行I/O的库函数时不会出乎意料地失败。
* 它防止了daemon 后面使用描述符1或2打开一个文件的情况,因为库函数会将这些描述符当做标准输出和标准错误来写入数据(进而破坏了原有的数据)。


/dev/null 是一个虚拟设备,它总会将写入的数据丢弃。当需要删除一个shell命令的标准输出和错误时可以将它们重定向到这个文件。从这个设备中读取数据总是会返回文件结束的错误。
~
GNU C库提供了一个非标准的daemon()函数,它将调用者变成一个daemon。
~
syslog工具 集中式日志工具,系统中所有应用程序都可以使用这个工具记录日志。
syslog工具有两个主要组件:syslogd daemon和syslog(3)库函数。
System Log daemon syslogd从两个不同的源接收日志消息:一个是UNIX domain socket /dev/log,它保存本地产生的消息;另一个是Internet domain socket(UDP 端口 514,如果启用),它保存通过TCP/IP网络发送的消息。(在其他一些UNIX实现中,syslog socket位于/var/run/log)


每条由syslogd处理的消息都具备几个特性,其中包括一个facility,它指定了产生消息的程序类型;还有一个是level,它指定了消息的严重程度(优先级)。syslogd daemon会检查每条消息的facility 和 level,然后根据一个相关配置文件/etc/syslog.conf中的指令将消息传递到几个可能目的地中的一个。可能的目的地包括终端或虚拟控制台、磁盘文件、FIFO、一个或多个(或所有)登录过的用户以及位于另一个系统上的通过TCP/IP网络连接的进程(通常是另一个syslogd daemon)。(将消息发送到另一个系统上的进程有助于通过将多个系统中的日志信息集中到一个位置以降低管理负担。)一条消息可以被发送到多个目的地(或不发送到任何目的地),具备不同的facility和 level组合的消息可以被发送到不同的目的地或不同的目的地实例(即不同的控制台、不同的磁盘文件等)。


通过 TCP/IP 网络将syslog消息发送到另一个系统还有助于发现系统非法入侵。非法入侵通常会在系统日志中留下踪迹,但攻击者通常会删除日志记录以掩盖他们的行为。有了远程日志记录之后,攻击者就需要侵入另一个系统才能删除目志记录。


通常,任意进程都可以使用syslog(3)库函数来记录消息。这个函数会使用传入的参数以标准的格式构建一条消息,然后将这条消息写入/dev/log socket 以供 syslogd 读取。
/dev/log 中的消息的另一个来源是Kernel Log daemon klogd,它会收集内核日志消息(内核使用printk()函数生成的消息)。这些消息的收集可以通过两个等价的Linux特有的接口中的一个来完成(即/proc/kmsg 文件和 syslog(2)系统调用),然后使用 syslog(3)库函数将它们写入/dev/log。
~
37.5.2 syslog API
syslogAPI由以下三个主要函数构成。
* openlog()函数为后续的的syslog()调用建立了默认设置。syslog()的调用是可选的,如果省略了这个调用,那么就会使用首次调用syslog()时采用的默认设置来建立到日志记录工具的连接。
* syslog()函数记录一条日志消息。
* 当完成日志记录消息之后需要调用closelog()函数拆除与日志之间的连接。
所有这些函数都不会返回一个状态值,这是因为系统日志服务应该总是处于可用状态(系统管理员应该在服务不可用时立即能发现这个问题)。此外,如果在系统记录日志的过程中发生了一个错误,应用程序通常也无法做更多的事情来报告这个错误。
~
/etc/syslog.conf文件
由规则和注释(以#字符打头)构成
规则的形式如下:
facility.level                    action
facility和level组合一起被称为选择器
action 指定了与选择器匹配的消息被发送到何处
选择器和action之间用空白字符隔开
一个规则可以包含多个选择器,选择器之间用分号分隔
level设置为none时,则表示排除所有属于相应facility的消息。
文件名前的连接符(-)表示无需每次写入文件时都将文件同步到磁盘,这意味着写入操作变得更快,但如果系统在写入之后崩溃可能会丢失一部分数据。
每次修改syslog.conf文件之后,让daemon根据该文件重新初始化自身:$killall -HUP syslogd
~
37.6总结
daemon 是一个长时间运行并且没有控制终端的进程(即它运行在后台)。daemon 执行特定的任务,如提供一个网络登录工具或服务Web页面。一个程序要成为daemon 需要按序执行一组步骤,包括调用fork()和setsid()。
daemon 应该在合适的地方正确地处理SIGTERM和SIGHUP信号。SIGTERM信号的处理方式应该是按序关闭这个daemon,而 SIGHUP 信号则提供了一种机制让daemon通过读取器配置文件并重新打开所使用的所有日志文件来重新初始化自身。
syslog 工具为daemon(以及其他应用程序)提供了一种便捷的方式来将错误和其他消息记录到一个中心位置。这些消息由syslogd daemon 处理,syslogd会根据syslogd.conf配置文件中的指令来重新分发消息。可以将消息重新分发到几个目标上,包括终端、磁盘文件、登录的用户以及通过TCPAIP 网络分发到远程主机上的进程中(通常是其他 syslogd daemon)。
~
41 章 共享库基础  *** 需再读
42 章 共享库高级特性
43 章 进程间通信简介
44 章 管道和FIFO


POSIX 信号量
POSIX 消息队列
POSIX 共享内存


文件锁


socket
* UNIX Domain


其他I/O 模型
* 多路复用 select() poll()
* 信号驱动
* epoll()
~