一、任务管理
1、进程组概念
在Linux
中,每个进程除了有一个进程ID之外,还有一个属性是进程组(PGID),进程组是一个或多个进程的集合。
通常,进程组内的所有进程它们与同一作业(任务)相关联,可以接收来自同一终端的各种消息和信号。每个进程组有一个唯一的进程组ID。每个进程组都可以有一个组长进程。组长进程的标识是:其进程组ID等于其进程ID,组长进程可以创建一个进程组,创建该组中的进程。
需要注意的是,只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关,也就是说: 进程组的生命周期从被创建开始,到其内所有进程终止或离开该组。
2、作业概念
Shell
区分前后台不是按照进程来区分的,而是按照作业(Job)或者进程组(Process Group)。
一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell
可以运行一个前台作业和任意多个后台作业,这称为作业控制。
作业与进程组的区别:
如果作业中的某个进程又创建了子进程,则子进程不属于作业。一旦作业运行结束,Shell
就把自己提到前台(Shell
也是一个作业,当其他前台作业在运行时,Shell
就会变成后台作业,这就是为什么我们完成其他作业时,无法使用Shell
),如果原来的前台进程还存在,也就是这个被创建的子进程还没有终止,那么它将自动变为后台进程组。
3、会话概念
Linux
是多用户多任务的分时系统,所以必须要支持多个用户同时使用一个操作系统。当一个用户登录一次系统就形成一次会话,一个会话可包含多个进程组,但只能有一个前台进程组。每个会话都有一个会话首领(leader),即创建会话的进程。
Linux
给我们提供了一个系统调用:setsid()
调用此函数能创建一个会话。
- 必须注意的是,只有当前进程不是进程组的组长时,才能创建一个新的会话。调用
setsid
之后,调用此函数的进程成为新会话的leader。 - 此函数的返回值是:成功则返回调用进程的ID。当出现错误时,返回 -1,并设置
errno
来指示错误。
- 一个会话可以有
1
个0
个或控制终端(会话的领头进程打开一个终端之后, 该终端就成为该会话的控制终端),在这个会话中的所有进程组都关联这个终端文件。 - 建立与控制终端连接的会话首进程又被称为控制进程。
- 一个会话中的几个进程组可被分为一个前台进程组和多个后台进程组,所以一个会话中,应该包括控制进程(会话首进程),一个前台进程组和任意多个后台进程组。
例如,下面我们用运行多个sleep
,让它们协同完成我们的任务,则它们应该属于同一个进程组,这些进程组的控制终端相同,它们同属于一个会话,当用户在控制终端输入特殊的控制键(如Ctrl+C产生SIGINT,Ctrl+\产生SIGQUIT,Ctrl+Z产生SIGTSTP),内核就会发送相应的信号给前台进程组中的所有进程。
我们看到了这个进程组的会话id是28977
,我们来查看一下,这个会话首领是谁?
结果显示是bash
,所以bash是当前会话的会话首领,也是控制进程。当我们用Xshell或是终端登录时,本质都是先创建一个会话,然后启动bash
进程,该进程成为组长,所有的命令行启动的任务都是在对应的会话内运行的。
实际我们每一次登录的过程都是新建会话的过程,同一个会话中的所有进程的SID是相同的。
4、相关操作
前台进程&后台进程
直接运行某一可执行程序,此时默认将程序放到前台运行,在前台运行的进程的状态后有一个+号,例如S+
。
运行可执行程序时在后面加上&
,可以指定将程序放到后台运行,在后台运行的进程的状态后没有+
号。
我们将程序放到后台运行时会发现多了一行提示信息,例如上述的:
[1] 7560
其中[1]是作业的编号,如果同时运行多个作业可以用这个编号进行区分,7560是该作业中最后一个进程的id(一个作业可以由多个进程组成)。
jobs、fg、bg命令
使用jobs
命令,可以查看当前会话当中有哪些作业。
使用fg
命令(foreground),可以将某个作业提至前台运行,如果该作业正在后台运行则直接提至前台运行,如果该作业处于停止状态,则给进程组的每个进程发SIGCONT
信号使它继续运行并提至前台。
例如,使用fg 1
命令将1号作业提到前台运行
使用bg
命令,可以让某个停止的作业在后台继续运行(Running),本质就是给该作业的进程组的每个进程发SIGCONT
信号。
二、守护进程
守护进程也称精灵进程(Daemon),是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
守护进程是一种很有用的进程,Linux的大多数服务器就是用守护进程实现的,守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。
用户登录时创建会话,当用户退出时销毁会话,会话销毁会话内的进程就有可能受到影响,因为我们的服务器进程是要一直运行的而且不能受用户登录和登出的影响,所以服务器要采用守护进程的方式运行!
1、守护进程的创建
守护进程创建的原理是:当我们运行一个服务器程序时,不要在当前用户登录的会话内运行服务器程序,而是创建一个新的会话(利用系统调用setsid()
),让这个服务器程序在这个新的会话内运行,这样用户的登录与登出都不会影响我们服务器程序。
守护进程的创建步骤:
- 设置文件掩码为0,(保证我们后续设置的权限不受权限掩码的影响)。
- 忽略异常信号,如:
SIGCHLD
SIGPIPE
(避免不必要的信号干扰服务器的运行)。 fork
后终止父进程,子进程创建新会话(保证当前进程不是组长进程)。- 再次
fork
,终止父进程,保持子进程不是会话首进程,从而保证后续不会再和其他终端相关联
守护进程不能直接和用户交互,也就没有必要再打开某个终端了,而打开一个终端需要你是会话首进程,为了防止守护进程打开终端,我们直接让父进程退出,由于子进程不是会话首进程,也就没有能力打开其他终端了。(这是一种防御性编程,该操作不是必须的)
- 更改工作目录为根目录(方便帮我们使用和查找文件)。
- 将标准输入、标准输出、标准错误重定向到
/dev/null
。
守护进程不能直接和用户交互,也就是说守护进程已经与终端去关联了,因此一般我们会将守护进程的标准输入、标准输出以及标准错误都重定向到
/dev/null
,/dev/null
是一个字符文件(设备),通常用于屏蔽/丢弃输入输出信息。(该操作不是必须的)
从上面的操作中我们也能看出:守护进程本质是孤儿进程
#pragma once #include <iostream> #include <cstring> #include <cerrno> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <signal.h> #include <fcntl.h> void Daemon() { // 1. 将权限掩码设置为0 umask(0); // 2.忽略SIGCHILD, SIGPIPE信号 if (SIG_ERR == signal(SIGCHLD, SIG_IGN)) { std::cerr << "signal fail: " << strerror(errno) << std::endl; exit(errno); } if (SIG_ERR == signal(SIGPIPE, SIG_IGN)) { std::cerr << "signal fail: " << strerror(errno) << std::endl; exit(errno); } // 3. 创建子进程,建立新的会话 pid_t id = fork(); if (id < 0) { std::cerr << "fork fail: " << strerror(errno) << std::endl; exit(errno); } else if (id > 0) exit(0); // 父进程退出 if (setsid() < 0) { std::cerr << "setsid fail: " << strerror(errno) << std::endl; exit(errno); } // 4.再次fork,终止父进程,保持子进程不是会话首进程,从而保证后续不会再和其他终端相关联 if (fork() > 0) exit(0); // 5.更改工作目录 if (chdir("/") < 0) { std::cerr << "chdir fail: " << strerror(errno) << std::endl; exit(errno); } // 6.将输入输出重定向到/dev/null int fd = open("/dev/null", O_RDWR); if (fd < 0) { std::cerr << "open fail: " << strerror(errno) << std::endl; exit(errno); } if (dup2(fd, 0) < 0) { std::cerr << "dup2 fail: " << strerror(errno) << std::endl; exit(errno); } if (dup2(fd, 1) < 0) { std::cerr << "dup2 fail: " << strerror(errno) << std::endl; exit(errno); } if (dup2(fd, 2) < 0) { std::cerr << "dup2 fail: " << strerror(errno) << std::endl; exit(errno); } // 关闭不需要的文件描述符 close(fd); }
将此代码加到我们的服务器的启动前,或者一个死循环的程序的运行前,运行程序,我们用ps命令查看该进程信息:
我们发现该进程的TPGID为-1
,TTY显示的是?
也就意味着该进程已经与终端去关联了。
其次,我们还可以看到该进程的PID与其PGID和SID是不同的,也就是说该进程既不是组长进程也不是会话首进程。
此外,我们还可以看到服务器进程的SID(6069)与bash进程的SID(就是上面的642)是不同的,即它们不属于同一个会话。
通过ls /proc/进程id -al
命令,可以看到该进程的工作目录已经成功改为了根目录。
通过ls /proc/进程id/fd -al
命令,可以看到该进程的标准输入、标准输出以及标准错误也成功重定向到了/dev/null
。
2、守护进程的库函数
C语言给我们实现了一个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
函数没有进行信号屏蔽。
在实际使用中我们更加倾向于自己写daemon
函数,这样定制性更好一些。