目录
任务管理
进程组概念
- 每一个进程除了有一个进程ID之外,还有一个进程组ID,进程组是一个或多个进程的集合。
- 通常它们与同一个作业相关联,可以接收来自同一个终端的各种信号。每个进程组都有一个唯一的进程组ID。每一个进程组都有一个组长进程。组长进程ID==进程组ID。组长进程可以创建一个进程组,创建该组中的进程,然后终止。
- 需要注意,只要在某个进程组中有一个进程存在,则该进程就存在,这与组长进程是否存在没有关系。
作业概念
- Shell分前后台来控制的不是进程而是作业(Job)或者进程组(Process Group).
- 一个前台作业可以有多个进程组成,Shell可以运行一个前台作业和多个后台作业,这称为作业控制。
- 作业与进程的区别:如果作业中某个进程又创建了子进程,则子进程不属于作业。一旦作业运行结束,Shell就把自己提到前台,如果原来的前台进程还存在,也就是这个被创建的子进程还没有终止,那么它就将自动变为后台进程组。
会话概念
- 会话(Session)是一个或多个进程组的集合。
- 一个会话可以又一个控制终端,这里通常是登录到其他设备的终端(在终端登录情况下)或伪终端设备(在网络登录情况下)。建立于控制终端连接的会话进程被称为控制进程。一个会话中的进程组可以被分为一个前台进程组以及多个后台进程组。
- 会话也有ID用来标识,这里会话ID==bash的PID。
补充
(1)前台进程和后台进程
前台进程:./运行时,默认将程序放到前台执行,在前台运行时,进程状态后面会有一个+号
后台进程: ./XXX & .在执行可执行程序时在后面加上 & 。就时将程序放到后台执行。进程的状态后面没有+号。
这里我们将程序放到后台执行我们会发现多了一行提示信息。
这里[3]时作业编号,如果同时运行多个作业可以用这个编号区分,23612时该作业中某个进程的PID(一个作业可以由多个进程组成)。
对于前后台进程经常使用到的命令
bash
jobs //查看当前会话中由哪些作业
fg 后台进程编号 //将编号对应的作业提至前台运行
bg 后台进程编号 //可以让某个停止的作业到后台重新运行
关于会话
- 当我们用一个Xshell或者终端登录时,本质都是先创建一个bash进程,整体称之为一个会话(所有的命令行的进程都是bash的子进程),所有的命令行启动的任务都是在对应的会话内运行的。
- 实际我们每一次登录的过程都是新建会话的过程,同一个会话中的所有进程的SESS是相同
守护进程
基本概念
- 守护进程也称精灵进程(Daemon),是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
- 为了保证一个任务,当终端会话退出的时候,这个任务还在服务后端跑,所以我们需要一种进程叫守护进程
- 守护进程是一种很有用的进程,Linux的大多数服务器就是用守护进程实现的,比如Internet服务器inetd,Web服务器httpd等。同时守护进程完成许多系统任务,比如作业规划进程crond等。
- Linux系统启动时会启动很多系统服务进程,这些系统服务进程没有控制终端,不能直接和用户交互。其他进程都是在用户登录或运行程序时创建,在运行结束或用户注销时终止,但系统服务进程不受用户登录注销的影响,它们一直在运行着,这种进程有一个名称叫守护进程(Daemon)。
守护进程的查看
- 参数a表示不仅列出当前用户的进程,也列出所有其他用户的进程。
- 参数x表示不仅列出有控制终端的进程,也列出所有无控制终端的进程。
- 参数j表示列出与作业控制相关的信息
- 凡是TPGID一栏写着-1的都是没有控制终端的进程,也就是守护进程
- 除此之外,在COMMAND一列用[ ]括起来的名字表示内核线程,这些线程在内核里创建,没有用户空间代码,因此没有程序文件名和命令行,通常采用以k开头的名字,表示Kernel。
- 个别说明:udevd负责维护/dev目录下的设备文件 ,acpid负责电源管理 , syslogd负责维护/var/log下的日志文件。
- 可以看出,守护进程通常采用以d结尾的名字,表示Daemon。
守护进程的创建
自己手写守护进程
创建思路:
- 1.设置文件掩码为0.
- 2.fork后终止父进程,子进程调用setsid创建新会话。
- 3.忽略SIGCHLD信号。
- 4.更改工作目录为根目录。
- 5.将标准输入,标准输出,标准错误重定向到/dev/null。
解释:
- 1.将文件掩码设置为0,是为了保证后续创建进程是,创建出来的文件权限符合我们的预期。
- 2.setsid函数创建新会话的目的是,让当前进程自成会话,与目前bash脱离关系(即关闭当前页面进程仍可以在我们的云服务器上运行)。这也是创建守护进程的核心。
- 3.之所以让子进程调用setside创建新会话,是因为创建新会话的进程不能是组长进程,但是当我们在命令行上启动多个进程协同完成一个任务时,第一个被我们创建的进程就是组长进程,所以这里我们使用fork创建子进程,让子进程调用setsid创建新的会话并继续执行后续代码,而父进程直接退出。
- 4.我们一般会将守护进程的工作目录设置为根目录,便于让守护进程以绝对路径的形式访问某种资源。(该操作不是必须的)
- 6.守护进程不能直接和用户交互,也就是说守护进程已经与终端去关联了,因此一般我们会将守护进程的标准输入、标准输出以及标准错误都重定向到/dev/null,/dev/null是一个字符文件(设备),通常用于屏蔽/丢弃输入输出信息。(该操作不是必须的)
守护进程代码(Demon.hpp):
cpp
#pragma once
#include <iostream>
#include <string>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<cstdlib>
const std::string nullfile = "/dev/null";
void Demon(const std::string &cwd = "")
{
// 忽略其他异常信号
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
signal(SIGSTOP, SIG_IGN);
// 2.将自己变为独立的会话
// 因为进程组的组长无法调用系统调用函数setsid,所以这里采用fork创建子进程,
// 让父进程直接结束,子进程创建独立的会话,并执行下面的代码
if (fork() > 0)
exit(0);
setsid(); // 子进程创建独立的会话
// 改变进程所在目录
if (!cwd.empty())
{
chdir(cwd.c_str());
}
// 将标准输入,输出,错误重定向到/dev/null文件,这个文间相当于垃圾站
int fd = open(nullfile.c_str(), O_RDWR);
if (fd > 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
}
测试代码(test.cpp):
cpp
#include<iostream>
#include"Demon.hpp"
int main()
{
Demon();
while(1)
{
std::cout<<"1adasd"<<std::endl;
sleep(1);
}
return 0;
}
这里我们启动进程后可以使用指令:
bash
//该指令用来查看我们的守护进程是否真的运行起来了
ps -ajx | head -1 && ps -ajx | grep test
- 这里我们可以发现使用ps产看给进程的信息时,发现给进程的TTY为?,意味着该进程与终端失去关联。
- test进程的SID与grep进程的SID不同,即它们不属于同一个会话。
同时我们也可以使用指令:
bash
//查看该进程的标准输出,输入,错误是否已经重定向到/dev/null
ls /proc/进程PID/fd -al
使用系统调用函数创建守护进程
我们创建守护进程时可以直接调用daemon接口进行创建,daemon函数的函数原型如下:
int daemon(int nochdir, int noclose);
参数说明:
- 如果参数nochdir为0,则将守护进程的工作目录该为根目录,否则不做处理。
- 如果参数noclose为0,则将守护进程的标准输入、标准输出以及标准错误重定向到/dev/null,否则不做处理。
例子:
cpp
#include<iostream>
#include<unistd.h>
int main()
{
daemon(0,0);
while(1)
{
std::cout<<"1adasd"<<std::endl;
sleep(1);
}
return 0;
}
- 这里调用系统调用函数daemon创建的守护进程与我们手动创建的守护进程区别不大。唯一的区别就是,daemon函数创建的守护进程即使组长进程,又是会话首进程。
- 系统调用的进程没有防止守护进程打开终端