linux 之进程基础 (七)、守护进程、进程组等概念
7. 守护进程、进程组等概念
7.1 守护进程特点
- 运行方式
守护进程,也就是通常所说的Daemon进程,是Linux中的后台服务进程。周期性的执行某种任务或等待处理某些发生的事件。Linux系统有很多守护进程,大多数服务都是用守护进程实现的。比如:像我们的tftp,samba,nfs等相关服务。同时Linux的大多数服务器就是用守护进程实现的。比如,Internet服务器inetd,Web服务器httpd等。 - 生命周期
守护进程会长时间运行,常常在系统启动时就开始运行,直到系统关闭时才终止 - 守护进程不依赖于终端
显而异见,从终端开始运行的进程都会依附于这个终端,这个终端称为这些进程的控制终端。当控制终端被关闭时,相应的进程都会被自动关闭。咱们平常写进程时,一个死循环程序关闭终端的同时也关闭了我们的程序,但是对于守护进程来说,其生命周期守护需要突破这种限制,它从开始运行,直到整个系统关闭才会退出,所以守护进程不能依赖于终端。 - 守护进程的父进程为init 进程
7.2 查看守护进程
7.2.1 查看
命令:ps -ajx
- -a: 显示所有
- -x:显示没有控制终端的进程
- -j:显示与作业有关的信息(显示的列):会话期ID(SID),进程组ID(PGID),控制终端(TTY),终端进程组ID(TRGID)
7.2.2 识别守护进程
输入ps -ajx 命令后
- 所有的守护进程都是以超级用户启动的(UID为0);
- 没有控制终端(TTY为?);
- 终端进程组ID为-1(TPGID表示终端进程组ID,该值表示与控制终端相关的前台进程组,如果未和任何终端相关,其值为-1;
- 所有的守护进程的父进程都为init进程(PID为1的进程)。
例如:cron 进程就是一个守护进程
7.3 与进程相关的一些概念
7.3. 1 进程组
进程组是一个或多个进程的集合。进程组由进程组ID(PGID)来唯一标识。每个进程组都有一个组长进程,进程组ID就是组长进程的进程号。
7.3.2 会话
是一个或多个进程组的集合。
一般一个用户登录后新建一个会话(打开终端),每个会话也有一个ID来标识(SID)。登录后的第一个进程叫做会话领头进程(session leader),通常是一个shell/bash,即一个shell程序。对于会话领头进程,其PID=SID。
为什么是一个用户登录后新建一个会话呢?
一般取去公司上班,我们见不到服务器真机,而是公司为每个员工分配一个 用户名和密码,我们可以通过远程进行登录。这个时候我们就登录到了一个终端,因此说一个用户登录后新建一个会话。
7.3.3 控制终端
- 一个会话一般会拥有一个控制终端用于执行IO操作。
- 会话的领头进程(一般是shell进程)打开一个终端之后, 该终端就成为该会话的控制终端。
- 与控制终端建立连接的会话领头进程也称为控制进程 (controlling process) 。一个会话只能有一个控制终端。
7.3.4 前台进程组
- 该进程组中的进程能够向终端设备进行读、写操作的进程组。
例如:在终端中执行./a.out 这个程序就能通过向终端读写
7.3.5 后台进程组ID
- 该进程组中的进程只能够向终端设备写。
例如$ ./a.out &
那么这个进程只能向终端打印内容,但是不能读取终端中的内容。
注意:每个会话有且只有一个前台进程组,但会有0个或者多个后台进程组。
7.3.6 终端进程组ID
每个进程还有一个属性,终端进程组ID(TPGID),用来标识一个进程是否处于一个和终端相关的进程组中。
-
前台进程组中的进程的TPGID=PGID,
-
后台进程组的PGID≠TPGID(d一般前台进程组ID等于终端ID,而后台进程组ID不等于终端ID)。
-
若该进程和任何终端无关,其值为-1。
通过进程进程组PPID比较终端进程组ID来判断一个进程是属于前台进程组,还是后台进程组,还是与终端无关的进程。
7.3.7 创建一个会话(打开一个登录shell)
- 登录时第一个创建的进程是shell(/bin/bash),也就是会话的领头进程。
- 该领头进程 缺省的 处于一个前台进程组中,并打开一个终端设备,等待数据的读写(命令、程序执行)。
- 当在shell里运行一行命令后(不带&)创建一个新的进程组,命令行中如果有多个命令会创建多个进程,这些进程都处于该新建进程组中,shell将该新建的进程组设置为前台进程组并将自己暂时设置为后台进程组。(如下图的两个箭头。)
该图说明,shell也只是一个程序,称为命令行解释器。用来解读终端发送给操作系统的命令。打开一个终端后,会首先启动一个shell进程。并且该终端会与这个shell进程关联起来。shell解读终端传输的命令,发送给操作系统,然后调用相关的系统调用,然后将结果再传递到终端上。
7.3.8 登录过程
- 通过终端用户登录后,会创建一个会话
- 启动第一个进程,即shell进程,也就是这个会话的领头进程,也是会话的控制进程。
- 在终端没有输入命令(执行程序)的时候,shell暂时处于前台进程组。
- 当有命令输入时,shell进程将会被置为后台进程组,然后将执行的命令的进程置为前台进程组。
- 当终端再次没有输入时(没有通过运行程序时),shell再被置为前台进程组。
7.3.9 实验验证
(1)实验步骤:
1)在终端中执行下述命令
2)重新开启一个终端,执行: ps -ajx
命令
3)观察第一个终端中两个进程的进程状态,结果如下图:
4)在第一个终端中按ctrl+z 再次观察结果
通过第一个实验结果可以看出:
(2)观察行号232行233行:
- 两个进程的PGID相同,说明两个进程属于同一个进程组。
- 两个进程的PGID 与第一个进程的PID相等,说明 第一个进程是该进程组的组长
- 二者的TPGID和PGID不相同,说明二者属于后台进程组。
(3)观察行号236行237行:
- 两个进程的PGID相同,说明两个进程属于同一个进程组。
- 两个进程的PGID 与第一个进程的PID相等,说明 第一个进程是该进程组的组长
- 二者的TPGID和PGID相同,说明二者属于前台进程组。
(4)同时观察观察前232 233 236 237行
- 四PPID相同,即四者的父进程ID相同,为29005,查看231
行可以发现,29005为bash的ID。即打开终端后运行的第一个进程为bash进程。 - 四个进程的SID相等,说明四个进程同属于一个会话。
- 四者的tty相同,说明二者属于同一个控制终端。
(5)通过第2个实验结果可以看出
- 前台运行的进程消失了,说明ctrl +z 只对前台进程起作用
- bash成为了新的前台进程组,说明在输入缺省状态下,shell为前台进程组。而当执行命令后,shell被置为后台进程组,执行的进程被设置为前台。
7.4 守护进程的书写步骤
7.4.1书写守护进程的一般步骤
(1)创建新进程将成为守护进程,并使其父进程退出
保证祖父进程确认父进程已结束,且守护进程不是组长进程
(2)守护进程创建新进程组和新会话
并成为两者的首进程,此时刚创建的新会话还没有关联控制终端。
#include <unistd.h>
pid_t setsid(void);
//返回值:若成功,返回新的会话期ID;若出错,返回-1。
setsid函数的作用
- 首先内核会创建一个新的会话,并让该进程成为该会话的leader进程。
- 同时伴随该session的建立,一个新的进程组也会被创建,同时该进程成为该进程组的组长。
- 该进程此时还没有和任何控制终端关联。如果调用setsid之前该进程有一个控制终端,那么这种联系也被切断。
注意:
如果该调用进程已经是一个进程组组长,则此函数返回出错。回想一下我们为了保证不处于这种情况我们是如何处理的?第一步,先调用fork,然后父进程终止,而子进程继续。因为子进程继承了父进程的进程组ID,而其进程ID是新分配的,两者不可能相等,这就保证了子进程不是一个进程组组长。
再新建一个进程,并且关闭第一子进程。这么做的目的是,避免第一子进程再次与终端产生联系(第一子进程是会话领头进程,当它与终端脱离联系后,它可以再通过某种方式再与终端建立联系。为了避免这种情况产生,所以再fork一个子进程,第二子进程不再是会话的领头进程,所以就完全与终端脱离联系了。)
(4)改变工作目录:
守护进程一般随系统启动,工作目录不应继续使用继承的工作目录。
这一步也是必要的步骤。使用fork()创建的子进程继承了父进程的当前工作目录。由于在进程运行过程中,当前目录所在的文件系统(如“/mnt/usb”等)是不能卸载的,这对以后的使用会造成诸多的麻烦。因此,通常的做法是让“/”作为守护进程的当前工作目录,这样就可以避免上述问题(避免工作目录被卸载)。当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp。改变工作目录的常见函数是chdir()。
(5)重设文件权限掩码:
守护进程不需要继承文件权限掩码。
进程从创建它的父进程那里继承了文件创建掩码。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0)。
(6)关闭所有文件描述符:
不需要继承任何打开的文件描述符。
进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。
一般的关闭方法:
for(i=0;i<=getdtablesize();i++)
close(i);
#include <unistd.h>
int getdtablesize(void);
//返回:进程最多打开文件描述符的个数(1024)
(7)标准流重定向到/dev/null
7.4.2 书写守护进程举栗子
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/fs.h>
int main()
{
//第一步:创建一个进程,使其父进程退出
pid_t pid = fork();
if( pid == -1 )
return -1;
else if( pid != 0 )
exit( EXIT_SUCCESS );
else
//第二步:守护进程创建新进程组和新会话
if( setsid() == -1 )//如果创建失败,返回
return -2;
//第三步:设置工作目录
if( chdir( "/" ) == -1 )//设置错误的话返回
return -3;
//第四步:重设文件权限掩码
umask( 0 );
//第五步:关闭文件描述符
for( int i = 0; i < getdtablesize(); i++ )
close( i );
//第六步:重定向标准流
open( "/dev/null", O_RDWR ); // 重定向stdin
dup( 0 ); // 重定向stdout
dup( 0 ); // 重定向stderr
//守护进程的实际工作代码在此
return 0;
}
重定向的原理:
所有的文件描述符都已经关闭,因此,第一个打开的是0号文件描述符,然后dup(0) 两次,是把0号代表的文件复制给1号2号文件描述符。