1、进程组
1-1****什么是进程组
之前我们提到了进程的概念, 其实每一个进程除了有一个进程 ID(PID) 之外 还属于一
个进程组。 进程组是 一个 或者 多个 进程的集合, 一个进程组可以包含多个进程。 每一
个进程组也有一个唯一的进程组 ID(PGID) , 并且这个 PGID 类似于进程 ID , 同样是
一个正整数, 可以存放在 pid_t 数据类型中。
bash
$ ps -eo pid,pgid,ppid,comm | grep test
-e 选项表示 every 的意思, 表示输出每一个进程信息
-o 选项以逗号操作符(,)作为定界符, 可以指定要输出的列
1-2 组长进程
每一个进程组都有一个组长进程。 组长进程的 ID 等于其进程 ID。我们可以通过 ps 命 令看到组长进程的现象

从结果上看 ps 进程的 PID 和 PGID 相同, 那也就是说明 ps 进程是该进程组的组长进 程, 该进程组包括 ps 和 cat 两个进程
进程组组长的作用:
进程组组长可以创建一个进程组或者创建该组中的进程
进程组的生命周期: 从进程组创建开始到其中最后一个进程离开为止。
注意: 主要某个进程组中有一个进程存在, 则该进程组就存在, 这与其组长进程是否已经终 止无关。
2、会话&&控制终端
前面写的服务器是不是直接这样跑起来就行了?

并不是,cal_server真正想要在服务器上跑是要变成守护进程的
变成守护进程的做法
命令行式
nohup ./cal_server 8888 1>/dev/null 2>/dev/null &

此时我们的进程已经变成类似于守护进程的东西了,但是严格意义上还不算守护进程
此时进程已经在后台上跑了
cpp
ps axj | grep cal_server

用client发送请求也有响应

现在就算把Xshell全关了这个服务还是会在服务器上跑

当我们登录机器时,Linux服务器会做两件事
1.打开一个终端文件(以下这种窗口)

开启几个窗口/dev/pts就会多出几个终端文件

2.新启动一个bash进程来给我们进行命令行解释

关掉后只剩一个

TTY显示的是和哪个终端文件相关联

在一个终端文件打开进程,可以看到它们的PPID、PID、PGID都不一样,说明他们两个是独立的,属于不同的进程组。

又可以看到他们的SID、TTY是一样的。tty说明了他们俩往同一个终端文件中打印,sid竟然就是bash所对应的会话。
在Linux中将以上过程打包起来总结为一个内容------新建会话
新开一个窗口(打开一个会话)
新启动一个bash进程
会话也有标识符------会话id
会话id一般都是会话中的第一个进程bash------话首进程
在一个会话中新启动的任何任务,比如上面的cal_server,在新启动的时候,一定是默认属于该会话的。
那么问题来了:关闭会话(窗口)会不会连带着其中的任务一起关闭呢?·会的,原则上我们要关闭时会话的内容会被全部释放,会话相当于Linux服务器对各个任务的一个管理容器
这会导致一种结果------服务器因为会话的关闭而退出或者出现自定义行为
小知识:windows中每次登录都会建一个会话(桌面),注销就是关闭会话
如何保证一个服务无论会话怎么退出,该服务都能够一直运行。
把进程单独拎出来生成一个新的会话就可以了
setid
pid_t setsid(void);
创建一个新的会话,但是调此函数的进程不能是进程组的组长

如何解决?
cpp
//父进程
if(fork()>0)
{
exit(0);
}
setsid();
fork()后(子进程)一定不是组长;守护/精灵进程------也是一种孤儿进程
自己写代码可以将进程变为守护进程,也可以调用系统调用接口daemon直接一步到位,把那些fork、setsid啥的全做了
man daemon
cpp
#include <unistd.h>
int daemon(int nochdir, int noclose);
nochdir:要不要更改当前进程的工作目录
要:切换成根目录/
不要:进程启动所在的路径
守护进程一般都要与自己的终端文件相关联,有的进程变成守护进程进入后台后不用关联终端文件
这时候还向终端文件去写就会出现问题。
noclose:是否需要进行输入输出的处理
0(标准输入)、1(标准输出)、2(标准错误)重定向到 /dev/null当中
相当于把代码的所有输入输出全部丢弃
返回值:成功返回0,失败返回-1

cpp
#include <iostream>
#include<unistd.h>
using namespace std;
int main()
{
cout<<"pid is:"<<getpid()<<endl;
sleep(1);
daemon(0,0);
//执行下面代码的不是当前进程,而是当前进程的子进程
while (true)
{
cout<<"hello"<<endl;
sleep(1);
}
return 0;
}
程序还在后台跑,此时的pid会比原来的大一,因为是子进程在执行,而父进程在daemon时就已经退出了,会话ID此时也是自己的,和任何一个bash进程都不一样。这时就算关闭Xshell它还会在你的云服务器上跑
cpp
ls /proc//111417 -l
工作目录此时改为根目录

输入输出也被重定向到dev/null

daemon(0,0)表示要更改工作目录、要重定向输入输出
而daemon(1,!)表示不更改工作目录,不重定向输入输出
3、作业控制
3-1什么是作业(job)和作业控制(Job Control)?
作业是针对用户来讲,用户完成某项任务而启动的进程,一个作业既可以只包含 一个进程,也可以包含多个进程,进程之间互相协作完成任务, 通常是一个进程 管道。
Shell 分前后台来控制的不是进程而是作业 或者进程组。一个前台作业可以由多 个进程组成,一个后台作业也可以由多个进程组成,Shell 可以同时运⾏一个前台 作业和任意多个后台作业,这称为作业控制。
下面的进程组就可以看作一个作用作业号是[1]

在写一个

放在后台执行的程序或命令称为后台命令,可以在命令的后面加上&符号从而让 Shell 识别这是一个后台命令,后台命令不用等待该命令执⾏完成,就可立即接收 新的命令,另外后台进程执行完后会返回一个作业号以及一个进程号(PID)
- : 表示该作业号是默认作业
-:表示该作业即将成为默认作业
无符号: 表示其他作业

想要把作业放在前台可以用fg 作业号
此时作业跑到前台了

如何放到后台呢?要先把作业暂停,Linux不允许一个暂停状态的进程处在前台(前台进程跟用户的键盘输入有关,如果暂停了还在前台键盘输入无效果系统不就卡死了)
ctrl +Z就暂停跑后台去了

bg 作业号就可以启动进程了


4、守护进程
Daemon.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
const char *root = "/";
const char *dev_null = "/dev/null";
void Daemon(bool ischdir, bool isclose)
{
// 1. 忽略可能引起程序异常退出的信号(不要的信号)
signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
// 2.fork,让自己不要成为组长
if (fork() > 0)
exit(0);
// 3.setsid
setsid();
// 4.确认是否更改工作目录
if (ischdir)
{
chdir(root);
}
// 5.确认是否重定向输入输出
if (isclose)
{
close(0);
close(1);
close(2);
}
else
{
int fd = open(dev_null, O_RDWR);
if (fd > 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
}
}