一、任务管理
1.1 进程组概念
进程组是一个或多个进程的集合,每个进程都属于一个进程组,引入进程组的目的是为了简化对进程的管理。每个进程组都可以有一个组长进程(由进程组第一个创建的进程担任),组长进程ID等于其进程组ID。组长进程可以创建一个进程组,创建该组中的进程,然后终止
通常,进程组与同一作业相关联,可以接收来自同一终端的各种信号
注意:只要在某个进程组中有一个进程存在,则该进程组就存在,与其组长进程是否终止无关
1.2 会话概念
会话(Session)是一个或多个进程组的集合
一个会话可以有一个控制终端,这通常是登录到其上的终端设备(在终端登录情况下)或伪终端设备(在网络登录情况下)。建立与控制终端连接的会话首进程被称为控制进程。一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组。所以
所以一个会话中,应该包括控制进程(会话首进程),一个前台进程组和任意多个后台进程组
下面使用sleep命令生成了五个进程(两个后台进程&&三个前台进程)
其中sleep 10000与sleep 20000属于同一个后台进程组,sleep 30000、sleep 40000和sleep 50000属于同一个前台进程组
这些进程组的控制终端相同,同属于一个会话,当用户在控制终端输入特殊的控制键(如Ctrl+C产生SIGINT,Ctrl+\产生SIGQUIT,Ctrl+Z产生SIGTSTP),内核就会发送相应的信号给前台进程组中的所有进程
1.3 作业概念
Shell分前后台来控制的不是进程而是作业(Job)或者进程组(Process Group)。
一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell可以运行一个前台作业和任意多个后台作业,这被称为作业控制
作业与进程组的区别:
若作业中的某个前台进程创建了子进程,子进程并不属于该作业。一旦作业运行结束,Shell就将自己提到前台,若原来的前台进程创建的子进程还没有终止,该子进程将自动变为后台进程组
1.4 相关操作
1.4.1 前台进程&后台进程
直接运行某一可执行程序,如./可执行程序,默认将进程放到前台运行,其状态后有+号,如R+
运行可执行程序时在后面加上&,可指定将程序放到后台运行,如./可执行程序,其状态后无+号
将程序放到后台运行时会发现多了一行提示信息,如上图的 [1] 17824
其中[1]是作业的编号,若同时运行多个作业可以用这个编号进行区分,17824是该后台进程的PID
1.4.2 jobs、fg、bg
使用jobs命令,可以查看当前会话中有哪些作业
使用fg命令,可以将某个作业提至前台运行,若该作业正在后台运行则直接提至前台运行,若该作业处于停止状态,则给进程组的每个进程发SIGCONT信号使其继续运行并提至前台
由于1号作业被提至前台运行,所以其运行状态也由R变成了R+
注意:前台进程只能有一个,当某个进程变成前台进程后,bash会自动变为后台进程,此时bash无法进行命令行解释
使用Ctrl+Z可以将进程放入后台,但使用后该进程就会处于停止状态(Stopped)
使用bg命令,可以让某个停止的作业在后台继续运行(Running)
其本质就是给该作业的进程组的每个进程发SIGCONT信号
1.4.3 ps命令查看指定的选项
使用ps命令时携带-o选项,可以查看指定的信息
当用Xshell或是终端登录时,本质都是先创建一个bash进程,整体称之为一个会话(所有的命令行的进程都是bash的子进程),所有的命令行启动的任务都是在对应的会话内运行
实际每一次登录的过程都是新建会话的过程,同一个会话中的所有进程的SESS是相同的
注意: ps命令是系统级的命令,能查看所有进程的信息,如ps -axj,只不过-o选项只查看当前会话的进程信息
二、守护进程
2.1 概念
守护进程也称精灵进程(Daemon),是运行在后台的一种特殊进程,独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件
守护进程是用处广泛,Linux的大多数服务器都是用守护进程实现的,如Internet服务器inetd,Web服务器httpd等。同时守护进程完成许多系统任务,如作业规划进程crond等
Linux系统启动时会启动很多系统服务进程,这些系统服务进程没有控制终端,不能直接和用户交互。其他进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但系统服务进程不受用户登录注销的影响,一直在运行,这种进程被称为守护进程(Daemon),且守护进程一般采用以d结尾的名字
2.2 守护进程的查看
使用ps axj命令查看系统中的进程:
参数a表示不仅列出当前用户的进程,也列出所有其他用户的进程
参数x表示不仅列出有控制终端的进程,也列出所有无控制终端的进程
参数j表示列出与作业控制相关的信息
TPGID处为-1的都是没有控制终端的进程,即守护进程
在COMMAND一列用[ ]括起来的名字表示内核线程,在内核里创建,没有用户空间代码,因此没有程序文件名和命令行,通常采用以k开头的名字,表示Kernel
2.3 创建守护进程
2.3.1 原生创建守护进程
守护进程的创建步骤如下:
设置文件掩码为0
- 忽略SIGCHLD、SIGPIPE等信号
- fork后终止父进程,确保子进程不为进程组组长
- 创建新会话
- 再次fork,终止父进程,保持子进程不是会话首进程,从而保证后续不会和其他终端相关联
- 更改工作目录为根目录
- 将标准输入、标准输出、标准错误重定向到/dev/null
相关说明:
- 子进程会继承父进程的掩码。将文件掩码设置为0,保证后续守护进程创建出来的文件的权限符合预期
- 忽略SIGCHLD、SIGPIPE,确保守护进程不会因为子进程或管道通信而崩溃
- 调用setsid创建新会话时,要求调用进程不能是进程组组长,但是在命令行上启动多个进程协同完成某种任务时,第一个被创建出来的进程就是组长进程,因此需fork创建子进程,让子进程调用setsid创建新会话并继续执行后续代码,而父进程直接退出即可
- 调用setsid让当前进程自成会话,与当前bash脱离关系(创建守护进程的核心)
- 守护进程不能直接和用户交互,也就没有必要再打开某个终端了,而打开一个终端需要是会话首进程,为了防止守护进程打开终端,再次fork创建子进程并让子进程继续执行后续代码,由于子进程不是会话首进程,也就没有能力打开其他终端了,而父进程直接退出即可(防御性编程,该操作不是必须的)
- 一般会将守护进程的工作目录设置为根目录,便于让守护进程以绝对路径的形式访问某种资源(该操作不是必须的)
- 守护进程不能直接和用户交互,即守护进程已经与终端去关联了,因此一般会将守护进程的标准输入、标准输出以及标准错误都重定向到/dev/null,/dev/null是一个字符文件(设备),通常用于屏蔽/丢弃输入输出信息(该操作不是必须的)
#include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> void DaemonSelf() { //1. 设置文件掩码 umask(0); //2. 忽略SIGCHLD、SIGPIPE信号 signal(SIGCHLD, SIG_IGN); signal(SIGPIPE, SIG_IGN); //3. fork后终止父进程,保证子进程不为进程组组长 if(fork() > 0) exit(0); //4. 创建新会话 setsid(); //5. fork后终止父进程,保持子进程不是会话首进程,从而保证后续不会再和其他终端相关联 //(不是必须的,防御性编程) if(fork() > 0) exit(0); //6. 更改工作目录为根目录 chdir("/"); //7. 标准输入、标准输出、标准错误重定向到/dev/null int devNull = open("/dev/null", O_RDWR); if(devNull > 0) { dup2(0, devNull); dup2(1, devNull); dup2(2, devNull); close(devNull); } }
运行代码,用ps命令查看该进程,会发现该进程的TPGID为-1,TTY显示?,也就意味着该进程已经与终端去关联了。还可以发现该进程的PID与其PGID和SID是不同的,也就是说该进程既不是组长进程也不是会话首进程
该进程的SID与bash进程的SID是不同的,即不属于同一个会话
使用 ls /proc/进程ID -al 命令,可以看到该进程的工作目录已经成功改为了根目录
使用 ls /proc/进程ID/fd -al 命令,可以看到该进程的标准输入、标准输出以及标准错误也成功重定向到了 /dev/null
2.3.2 daemon函数创建守护进程
实际当我们创建守护进程时可以直接调用daemon接口进行创建,daemon函数的函数原型如下:
int daemon(int nochdir, int noclose);
若参数nochdir为0,则将守护进程的工作目录该为根目录,否则不做处理
若参数noclose为0,则将守护进程的标准输入、标准输出以及标准错误重定向到/dev/null,否则不做处理
#include <unistd.h> int main() { daemon(0, 0); while (1); return 0; }
调用daemon函数创建的守护进程与自主创建的守护进程差别不大,唯一区别就是daemon函数创建出来的守护进程,既是组长进程也是会话首进程
系统实现的daemon函数没有防止守护进程打开终端,上面自主实现的反而比系统更加完善