Linux任务管理与守护进程

任务管理

进程组概念

每个进程除了有一个进程ID之外,还属于一个进程组,进程组是一个或多个进程的集合。

通常,它们与同一作业相关联,可以接收来自同一终端的各种信号。每个进程组有一个唯一的进程组ID。每个进程组都可以有一个组长进程。组长进程的标识是,其进程组ID等于其进程ID。组长进程可以创建一个进程组,创建该组中的进程,然后终止。

需要注意的是,只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。

作业概念

Shell分前后台来控制的不是进程而是作业(Job)或者进程组(Process Group)。

一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell可以运行一个前台作业和任意多个后台作业,这称为作业控制。

作业与进程组的区别:
如果作业中的某个进程又创建了子进程,则子进程不属于作业。一旦作业运行结束,Shell就把自己提到前台,如果原来的前台进程还存在,也就是这个被创建的子进程还没有终止,那么它将自动变为后台进程组。

会话概念

会话(Session)是一个或多个进程组的集合。

一个会话可以有一个控制终端,这通常是登录到其上的终端设备(在终端登录情况下)或伪终端设备(在网络登录情况下)。建立与控制终端连接的会话首进程被称为控制进程。一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组。所以一个会话中,应该包括控制进程(会话首进程),一个前台进程组和任意多个后台进程组。

例如,下面我们用同一个死循环代码生成了5个可执行程序。
在这里插入图片描述
我们将mytest1和mytest2放到后台运行,将mytest3、mytest4和mytest5放到前台运行。
在这里插入图片描述
其中mytest1与mytest2属于同一个后台进程组,mytest3、mytest4和mytest5属于同一个前台进程组,而Shell本身属于一个单独的进程组。
在这里插入图片描述
这些进程组的控制终端相同,它们同属于一个会话,当用户在控制终端输入特殊的控制键(如Ctrl+C产生SIGINT,Ctrl+产生SIGQUIT,Ctrl+Z产生SIGTSTP),内核就会发送相应的信号给前台进程组中的所有进程。
在这里插入图片描述

相关操作

前台进程&后台进程

直接运行某一可执行程序,例如./可执行程序,此时默认将程序放到前台运行,在前台运行的进程的状态后有一个+号,例如R+
在这里插入图片描述
运行可执行程序时在后面加上&,可以指定将程序放到后台运行,例如./可执行程序 &,在后台运行的进程的状态后没有+号。
在这里插入图片描述
我们将程序放到后台运行时会发现多了一行提示信息,例如上述的:

[1] 16437

其中[1]是作业的编号,如果同时运行多个作业可以用这个编号进行区分,16437是该作业中某个进程的id(一个作业可以由多个进程组成)。

我们可以用该可执行程序同时创建四个进程放到后台运行:
在这里插入图片描述
此时我们就可以将它们分别叫做当前终端下的1号作业、2号作业、3号作业和4号作业。

jobs、fg、bg

使用jobs命令,可以查看当前会话当中有哪些作业。
在这里插入图片描述
使用fg命令(foreground),可以将某个作业提至前台运行,如果该作业正在后台运行则直接提至前台运行,如果该作业处于停止状态,则给进程组的每个进程发SIGCONT信号使它继续运行并提至前台。

例如,使用fg 1命令将1号作业提到前台运行。
在这里插入图片描述
由于1号作业被提至前台运行,所以其运行状态也由R变成了R+

需要注意的是,前台进程只能有一个,当一个进程变成前台进程后,bash会自动变为后台进程,此时bash就无法进行命令行解释了。

例如,我们将1号作业提至前台运行后,bash进程的状态后面的+号就没有了,也就意味着bash自动由前台进程变为了后台进程。
在这里插入图片描述
将一个前台进程放到后台运行可以使用Ctrl+Z,但使用Ctrl+Z后该进程就会处于停止状态(Stopped)。
在这里插入图片描述
使用bg命令,可以让某个停止的作业在后台继续运行(Running),本质就是给该作业的进程组的每个进程发SIGCONT信号。

例如,使用bg 1命令让1号作业在后台继续运行。
在这里插入图片描述

ps命令查看指定的选项

使用ps命令时携带-o选项,可以查看指定的信息。
在这里插入图片描述
当我们用Xshell或是终端登录时,本质都是先创建一个bash进程,整体称之为一个会话(所有的命令行的进程都是bash的子进程),所有的命令行启动的任务都是在对应的会话内运行的。

实际我们每一次登录的过程都是新建会话的过程,同一个会话中的所有进程的SESS是相同的。
在这里插入图片描述
说明一下: ps命令是一个系统级的命令,该命令能查看所有进程的信息,例如ps axj,只不过-o选项只查看当前会话的进程信息。

守护进程

守护进程的概念

守护进程也称精灵进程(Daemon),是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。

守护进程是一种很有用的进程,Linux的大多数服务器就是用守护进程实现的,比如Internet服务器inetd,Web服务器httpd等。同时守护进程完成许多系统任务,比如作业规划进程crond等。

Linux系统启动时会启动很多系统服务进程,这些系统服务进程没有控制终端,不能直接和用户交互。其他进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但系统服务进程不受用户登录注销的影响,它们一直在运行着,这种进程有一个名称叫守护进程(Daemon)。

守护进程的查看

我们可以用ps axj命令查看系统中的进程:

  • 参数a表示不仅列出当前用户的进程,也列出所有其他用户的进程。
  • 参数x表示不仅列出有控制终端的进程,也列出所有无控制终端的进程。
  • 参数j表示列出与作业控制相关的信息。

凡是TPGID一栏写着-1的都是没有控制终端的进程,也就是守护进程。
在这里插入图片描述
除此之外,在COMMAND一列用[ ]括起来的名字表示内核线程,这些线程在内核里创建,没有用户空间代码,因此没有程序文件名和命令行,通常采用以k开头的名字,表示Kernel。

个别说明:

  • udevd负责维护/dev目录下的设备文件。
  • acpid负责电源管理。
  • syslogd负责维护/var/log下的日志文件。

可以看出,守护进程通常采用以d结尾的名字,表示Daemon。

守护进程的创建

原生创建守护进程

守护进程的创建步骤如下:

  1. 设置文件掩码为0。
  2. fork后终止父进程,子进程创建新会话。
  3. 忽略SIGCHLD信号。
  4. 再次fork,终止父进程,保持子进程不是会话首进程,从而保证后续不会再和其他终端相关联。
  5. 更改工作目录为根目录。
  6. 将标准输入、标准输出、标准错误重定向到/dev/null。

相关说明:

  1. 将文件掩码设置为0,保证后续守护进程创建文件时,创建出来的文件的权限符合我们的预期。
  2. 调用setsid创建新会话的目的,是让当前进程自成会话,与当前bash脱离关系(创建守护进程的核心)。
  3. 调用setsid创建新会话时,要求调用进程不能是进程组组长,但是当我们在命令行上启动多个进程协同完成某种任务时,其中第一个被创建出来的进程就是组长进程,因此我们需要fork创建子进程,让子进程调用setsid创建新会话并继续执行后续代码,而父进程我们直接让其退出即可。
  4. 守护进程不能直接和用户交互,也就没有必要再打开某个终端了,而打开一个终端需要你是会话首进程,为了防止守护进程打开终端,我们需要再次fork创建子进程并让子进程继续执行后续代码,由于子进程不是会话首进程,也就没有能力打开其他终端了,而父进程我们直接让其退出即可。(这是一种防御性编程,该操作不是必须的)
  5. 我们一般会将守护进程的工作目录设置为根目录,便于让守护进程以绝对路径的形式访问某种资源。(该操作不是必须的)
  6. 守护进程不能直接和用户交互,也就是说守护进程已经与终端去关联了,因此一般我们会将守护进程的标准输入、标准输出以及标准错误都重定向到/dev/null/dev/null是一个字符文件(设备),通常用于屏蔽/丢弃输入输出信息。(该操作不是必须的)

代码如下:

#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;
}

运行代码,用ps命令查看该进程,会发现该进程的TPGID为-1,TTY显示的是,也就意味着该进程已经与终端去关联了。
其次,我们还可以看到该进程的PID与其PGID和SID是不同的,也就是说该进程既不是组长进程也不是会话首进程。
在这里插入图片描述
此外,我们还可以看到该进程的SID与bash进程的SID是不同的,即它们不属于同一个会话。
在这里插入图片描述
通过ls /proc/进程id -al命令,可以看到该进程的工作目录已经成功改为了根目录。
在这里插入图片描述
通过ls /proc/进程id/fd -al命令,可以看到该进程的标准输入、标准输出以及标准错误也成功重定向到了/dev/null
在这里插入图片描述

调用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函数没有防止守护进程打开终端,因此我们实现的反而比系统更加完善。

模拟实现daemon函数

有了上述创建守护进程的代码,要模拟实现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){
		chdir("/");
	}

	//6、将标准输入、标准输出、标准错误重定向到/dev/null(可选的选项)
	if (noclose == 0){
		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;
}

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>