linux之守护进程
什么是守护进程?
这是我们自己的编写的一个TCP服务器
==当我们关闭终端的掉服务器终端的时候==
可以看到服务器的进程没了!这有问题吗?——==这很有问题!因为服务器竟然受到用户的登陆和退出的影响!==服务器的运行方式应该是==不会受到用户的登陆和注销的影响!==服务器应该要一直运行下去!==除非我们自己要手动的kill掉这个服务器!==
==这种不受用户登陆和注销影响的进程我们就称之为守护进程!==
是会话,作业与进程组的概念
==在此之前我们先要了解一个概念——对话!==
会话
==这个对话这样说起来也是很抽象的!那么它究竟是什么呢?==
作业
==我们可以通过jobs指令来查看作业!==
进程组
这就像是什么?有一个包工头,有一个任务——将楼下的1吨垃圾给拉走,然后我们给任务取一个编号就是2号任务,为了完成2号任务,于是找到了一个人,说有一个2号任务,你自己建团队,给我把这个任务给完成了,完成任务就给你薪酬
于是8816这个人就找到了自己的兄弟8862,8863一起来组了一个组来共同完成这个任务!
==任务是要由进程组来完成的!这三个进程合起来的目的就是为了完成这个任务!==
还有一个id——就是SID(session id)在创建一批任务后就有3组作业了!但是我们可以看看到,这些进程的SID都是一样的!
==虽然这两批任务是不一样的!但是他们的sid都是一样的!==
==这就像是包工头,又有了一个新的任务——把这栋楼的瓷砖给贴好,于是找来了8908,让它自己组一个队伍,然后去执行这个3号任务!==
==但是无论是前面执行2号任务的组还是现在执行3号任务的组!他们都是属于同一个包工头的名下的!——这就是SID==
==他们都是有同样的SID——即都是在同一个会话下面运行的!——而这个SID其实是以bash这个进程pid来命名的!==
fg指令是用来将后台任务变成前台的!——==变成前台后我们发现什么指令都无法执行了因为此时我们的bash已经变成了后台!==
==这里也证明了会话里面有且只有一个前台任务!==
当我们ctrl + c之后将任务杀死,那么bash就会自动回到前台!那么此时bash就可以解析我们的任务了!
ctrl+z可以暂停任务!
bg(background)指令—— bg+ 作业编号可以让暂停的后台任务重新运行!
==我们得到的一个结论是会话里面的每一个独立的作业都是可以前后台互相转换的!==
==以上就是会话,进程组,作业之间的关系!==
==那么当我们退出终端的时候呢?——那么我创建的bash和前后端进程都有可能会被关闭!==
==这样子当我们的终端退出的时候!那么操作系统只会清除掉终端对应的会话!而不会清除与其无关的会话!——这样我们就可以创建出一个与用户的登录注销不直接相关的任务!——这样的任务以进程方式呈现!我们就将其称之为守护进程!==
守护进程可以一直运行!除非未来我们想要杀死它!
守护进程化的原理与daemon函数实现
这个daemon函数就是操作系统为我们提供的,进程守护进程化的函数!
第一个参数就是是否要改变进程的工作路径!
第二个参数就是是否要关闭标准输入,输入错误
==我们接下来会讲解一些守护进程化的原理!同时自己实现一个!——我们推荐是使用自己写的daemon函数!因为操作系统为我们提供的函数有很多未定义的行为!我们使用自己实现的就可以有很多的定制化操作!==
如何守护进程化呢?
我们知道守护进程话本质就是——让进程自成会话和进程组!那么我们就需要使用如下的函数
sid就是我们刚刚介绍的会话id(session id)——这个函数的作用就是谁调用了这个函数,就给谁创建一个会话和将自己的pid设置为一个进程组id
==这个函数什么参数都没有!直接调用即可使用——但是调用这个函数是不可以随便调用的!要满足这个进程能是某个会话的进程组的组长!==
就像是一个公司,里面分为很多的小组!里面的小组组长是不能说脑子一拍就要打算出去创业,如果他出去了,他组里面的人该怎么办?——只有那些普通的组员,可以自己出去创业
==所以setsid直接调用是会报错的!就是为了防止组长直接调用这个函数!==
daemon函数的实现
//daemon.hpp #pragma once #include<unistd.h> #include<signal.h> #include<stdlib.h> #include<cassert> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #define DEV "/dev/null" void daemonSelf(const char* CurrPath = nullptr) { //1.让调用该函数的进程,屏蔽掉异常的信号! //什么叫做异常的信号呢?——比如说客户端给服务端发送一个消息! //服务端请求完这个消息正准备响应会去,这时候客户端崩溃了!那么这时候相当于给一个已经关闭的文件描述符进行写入! //那么就像是管道读写一样!如果读端关闭!写端存在!那么操作系统就会发送SIGPIPE信号杀死写端!这里也是同样如此! //所以我们要忽略掉一些系统信号! signal(SIGPIPE,SIG_IGN);//忽略SIGPIPE,防止错误写入! //2.我们要调用setsid!但是setsid是不能直接调用的!操作系统不允许!直接调用会报错! //为什么呢?——为的就是防止组长调用setsid! //所以我们要如何让自己不是组长! //一般在一个进程里面谁早谁就是组长! if(fork()>0) exit(0);//父进程直接退出!子进程变成孤儿进程 //走到这里的肯定是子进程!——子进程就一定不是组组长! //守护进程,又叫做精灵进程——是孤儿进程的一种! pid_t n = setsid(); assert(n != -1); //3.守护进程是脱离终端的!所以我们要关闭或重定向以前默认打开的文件! //因为是脱离终端的是不需要关心键盘或者显示器上的任何时事件所以012号的描述符应该关闭或者重定向! //此时只有用网络端口才能访问到这个进程! //我们最好对012的文件描述符进行重定向!而不是关闭! //万一我们有些日志没有处理干净是往显示器上打印的怎么办?对一个已经关闭的文件描述符写入,会立马报错导致进程挂掉了! //在linux下我们可以向/dev/null这个文件进行重定向! //这个文件像是一个黑洞,默认处理数据的方式就是凡是写入的都抛弃 //我们读取的时候,即不阻塞,但是也什么都没有! int fd = open(DEV,O_RDWR); if(fd >= 0) { dup2(fd,0); dup2(fd,1); dup2(fd,2); close(fd);//以后就不需要用了 } else { close(0); close(1); close(2); } //4.可选,进程的执行路径是否更改!——即修改当前进程的当前路径 if(CurrPath) chdir(CurrPath); }
#include"tcpServer.hpp" #include<memory> #include"daemon.hpp" using namespace server; using namespace std; static void usage(std::string proc) { std::cout << "\nUsage:\n\t" << proc << " local_port\n\t\n"; } int main(int argc,char* argv[]) { if(argc != 2) { usage(argv[0]); exit(USAGE_ERR); } uint16_t port = atoi(argv[1]);//将字符串转换为整数 unique_ptr<TcpServer> tsvr(new TcpServer()); tsvr->initServer(); daemonSelf();//调用守护进程的逻辑就是先daemon,守护进程化 //然后执行服务器的核心逻辑! tsvr->start(); return 0; }
==这就是这个服务器进程就会,自成进程组组长,自成会话!——前提这个进程不是组长,它才能成为组长==
==当我们关闭我们的启动服务器的终端!==
==我们可以发现仍然是可以运行的!——我们这就完成了服务器的守护进程化!==