一.任务管理
1.进程组概念
- 每个进程除了有一个进程ID之外,还属于一个进程组,进程组是一个或多个进程的集合。
- 通常,它们与同一作业相关联,可以接收来自同一终端的各种信号。每个进程组有一个唯一的进程组ID。每个进程组都可以有一个组长进程。组长进程ID = 进程组ID。组长进程可以创建一个进程组,创建该组中的进程,然后终止。
- 需要注意的是,只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。
解释: 所有seep进程的ppid都是bash,都属于同一个bash会话,同时创健建的sleep1,2,3属于同一个组,他们三个自成一组;他们三个合作起来共同完成一个任务,他们的组长是19561,pid == PGRP,组长是启动的第一个进程。
2.作业概念
- Shell分前后台来控制的不是进程而是作业(Job)或者进程组(Process Group)。
- 一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell可以运行一个前台作业和任意多个后台作业,这称为作业控制。
- 作业与进程组的区别:如果作业中的某个进程又创建了子进程,则子进程不属于作业。一旦作业运行结束,Shell就把自己提到前台,如果原来的前台进程还存在,也就是这个被创建的子进程还没有终止,那么它将自动变为后台进程组。
3.会话概念
- 会话(Session)是一个或多个进程组的集合。
- 一个会话可以有一个控制终端,这通常是登录到其上的终端设备(在终端登录情况下)或伪终端设备(在网络登录情况下)。建立与控制终端连接的会话首进程被称为控制进程。一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组。所以一个会话中,应该包括控制进程(会话首进程),一个前台进程组和任意多个后台进程组。
4.补充
(1) 前台进程 和 后台进程
①前台进程,./运行时 , 默认将程序放到前台运行,在前台运行的进程的状态后有一个 + 号
②一个终端只能有一个前台进程,为什么把一个进程放到前台之后,且我们输入的命令不起作用?(你把一个进程放到前台,bash自动变成后台进程)我们的前台进程以前是bash,只有在前台的时候才能接收你的命令,到了后台接收不了
③后台进程, ./运行时在后面加上&,可以指定将程序放到后台运行,后台运行的进程的状态后没有
+
号。
④将程序放到后台运行时会发现多了一行提示信息
[1] 17353
其中[1]是作业的编号,如果同时运行多个作业可以用这个编号进行区分,17353是该作业中某个进程的id(一个作业可以由多个进程组成)。
(2)jobs , fg , bg
①使用jobs命令查看当前会话中有哪些作业
②使用 fg 命令(foreground),可以将某个作业提至前台运行,如果该作业正在后台运行则直接提至前台运行,如果该作业处于停止状态,则给进程组的每个进程发SIGCONT信号使它继续运行并提至前台。
③使用 bg 命令,可以让某个停止的作业在后台继续运行(Running),本质就是给该作业的进程组的每个进程发SIGCONT信号
(3)ps指令查看指定选项
①携带-o选项,可以查看指定的信息。
- ps命令是一个系统级的命令,该命令能查看所有进程的信息,例如ps axj,只不过-o选项只查看当前会话的进程信息。
②关于会话
- 当我们用Xshell或是终端登录时,本质都是先创建一个bash进程,整体称之为一个会话(所有的命令行的进程都是bash的子进程),所有的命令行启动的任务都是在对应的会话内运行的。
- 实际我们每一次登录的过程都是新建会话的过程,同一个会话中的所有进程的SESS是相同的
二.守护进程
1.基本概念
- 守护进程也称精灵进程(Daemon),是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
- 为了保证一个任务,当终端会话退出的时候,这个任务还在服务后端跑,所以我们需要一种进程叫守护进程
- 守护进程是一种很有用的进程,Linux的大多数服务器就是用守护进程实现的,比如Internet服务器inetd,Web服务器httpd等。同时守护进程完成许多系统任务,比如作业规划进程crond等。
- Linux系统启动时会启动很多系统服务进程,这些系统服务进程没有控制终端,不能直接和用户交互。其他进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但系统服务进程不受用户登录注销的影响,它们一直在运行着,这种进程有一个名称叫守护进程(Daemon)。
2.守护进程的查看
使用ps axj 查看系统中的进程:
- 参数a表示不仅列出当前用户的进程,也列出所有其他用户的进程。
- 参数x表示不仅列出有控制终端的进程,也列出所有无控制终端的进程。
- 参数j表示列出与作业控制相关的信息。
- 凡是TPGID一栏写着-1的都是没有控制终端的进程,也就是守护进程
- 除此之外,在COMMAND一列用[ ]括起来的名字表示内核线程,这些线程在内核里创建,没有用户空间代码,因此没有程序文件名和命令行,通常采用以k开头的名字,表示Kernel。
- 个别说明:udevd负责维护/dev目录下的设备文件 ,acpid负责电源管理 , syslogd负责维护/var/log下的日志文件。
- 可以看出,守护进程通常采用以d结尾的名字,表示Daemon。
3.守护进程的创建
(1)创建步骤如下:
- 设置文件掩码为0。
- fork后终止父进程,子进程创建新会话。
- 忽略SIGCHLD信号。
- 再次fork,终止父进程,保持子进程不是会话首进程,从而保证后续不会再和其他终端相关联。
- 更改工作目录为根目录。
- 将标准输入、标准输出、标准错误重定向到/dev/null。
(2)相关说明:
- 将文件掩码设置为0,保证后续守护进程创建文件时,创建出来的文件的权限符合我们的预期。
- 调用setsid创建新会话的目的,是让当前进程自成会话,与当前bash脱离关系(创建守护进程的核心)。
- 调用setsid创建新会话时,要求调用进程不能是进程组组长,但是当我们在命令行上启动多个进程协同完成某种任务时,其中第一个被创建出来的进程就是组长进程,因此我们需要fork创建子进程,让子进程调用setsid创建新会话并继续执行后续代码,而父进程我们直接让其退出即可。
- 守护进程不能直接和用户交互,也就没有必要再打开某个终端了,而打开一个终端需要你是会话首进程,为了防止守护进程打开终端,我们需要再次fork创建子进程并让子进程继续执行后续代码,由于子进程不是会话首进程,也就没有能力打开其他终端了,而父进程我们直接让其退出即可。(这是一种防御性编程,该操作不是必须的)
- 我们一般会将守护进程的工作目录设置为根目录,便于让守护进程以绝对路径的形式访问某种资源。(该操作不是必须的)
- 守护进程不能直接和用户交互,也就是说守护进程已经与终端去关联了,因此一般我们会将守护进程的标准输入、标准输出以及标准错误都重定向到/dev/null,/dev/null是一个字符文件(设备),通常用于屏蔽/丢弃输入输出信息。(该操作不是必须的)
(3)代码
#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
//1、设置文件掩码为0
umask(0);
//2、fork后终止父进程,子进程创建新会话
if (fork() > 0){ //father
exit(0);
}
setsid();
//3、忽略SIGCHLD信号,
signal(SIGCHLD, SIG_IGN);
//4、再次fork,终止父进程,保持子进程不是会话首进程,从而保证后续不会再和其他终端相关联
//防御性操作,不是必须的
if (fork() > 0){
//father
exit(0);
}
//5、更改工作目录为根目录(可选的选项)
chdir("/");
//6、将标准输入、标准输出、标准错误重定向到/dev/null(可选的选项)
close(0);
int fd = open("/dev/null", O_RDWR);
dup2(fd, 1);
dup2(fd, 2);
while (1);
return 0;
}
(4)结果
- ①用ps命令查看该进程,会发现该进程的TPGID为-1,TTY显示 ? ,也就意味着该进程已经与终端去关联了。
- ②可以看到该进程的PID与其PGID和SID是不同的,也就是说该进程既不是组长进程也不是会话首进程。
- ③可以看到该进程的SID与bash进程的SID是不同的,即它们不属于同一个会话。
④找到该进程对应的进程文件 ,可以看到该进程的工作目录已经成功改为了根目录。
⑤在该进程文件中 , 可以看到该进程的标准输入、标准输出以及标准错误也成功重定向到了/dev/null。
4.调用daemon函数创建守护进程
我们创建守护进程时可以直接调用daemon接口进行创建,daemon函数的函数原型如下:
int daemon(int nochdir, int noclose);
参数说明:
- 如果参数nochdir为0,则将守护进程的工作目录该为根目录,否则不做处理。
- 如果参数noclose为0,则将守护进程的标准输入、标准输出以及标准错误重定向到/dev/null,否则不做处理
(1)使用示例
#include <unistd.h>
int main()
{
daemon(0, 0);
while (1);
return 0;
}
(2)结果
- 调用daemon函数创建的守护进程与我们原生创建的守护进程差距不大,唯一区别就是daemon函数创建出来的守护进程,既是组长进程也是会话首进程。
- 系统实现的daemon函数没有防止守护进程打开终端,因此我们实现的反而比系统更加完善
5.模拟实现daemon函数
只需要设置两个参数nochdir和noclose,当所给nochdir为0时,我们将守护进程的工作目录该为根目录,当所给noclose为0时,我们则将守护进程的标准输入、标准输出以及标准错误重定向到/dev/null
#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
void my_daemon(int nochdir, int noclose)
{
//1、设置文件掩码为0
umask(0);
//2、fork后终止父进程,子进程创建新会话
if (fork() > 0){
//father
exit(0);
}
setsid();
//3、忽略SIGCHLD信号
signal(SIGCHLD, SIG_IGN);
//4、再次fork,终止父进程,保持子进程不是会话首进程,从而保证后续不会再和其他终端相关联
//(不是必须的,防御性编程)
if (fork() > 0){
//father
exit(0);
}
//5、更改工作目录为根目录(可选的选项)
if (nochdir == 0){ //判断1
chdir("/");
}
//6、将标准输入、标准输出、标准错误重定向到/dev/null(可选的选项)
if (noclose == 0){ //判断2
close(0);
int fd = open("/dev/null", O_RDWR);
dup2(fd, 1);
dup2(fd, 2);
}
}
int main()
{
my_daemon(0, 0);
while (1);
return 0;
}