目录
进程组
什么是进程组
组长进程
会话
什么是会话
作业控制
守护进程
进程组
什么是进程组
我们在命令行中输入sleep 1 | sleep 2 | sleep 3,然后查看进程,
我们看到它们有不同的pid,表明它们是不同的进程,它们的ppid是一样的,那么它们是兄弟进程。除了pid和ppid外,我们看到第三列还有pgid(process group id,进程组id),它们对应的pgid是一样的,表明它们属于同一个组,这三个进程的组长是第一个被创建的进程,即sleep 1000。我们再来写这样一段简单代码:
我们发现这一个进程也有自己的pgid,其pgid就是它自己的pid本身。也就是说,哪怕只有一个进程,也能自成进程组。
我们再来写这样一段代码:
我们发现,它们的pid不一样说明这是两个进程。但是我们发现它们的pgid是一样的,就是父进程的pid。如果我们将来在写一个多进程程序的时候,多个进程其实是以进程组的方式来共同完成一件任务的。进程组是一个或者多个进程的集合, 一个进程组可以包含多个进程。
组长进程
每一个进程组都有一个组长进程,组长id就是其进程id。
- 进程组长的作用:可以创建一个进程组或者创建该进程组中的进程。
- 进程组的生命周期:从进程组创建开始到其中最后一个进程离开为止。注意:只要某个进程组中有一个进程存在,则该进程就存在,与其组长进程是否已经终止无关。
-e 选项表示every的意思,表示输出每一个进程的信息。
-o 选项以逗号操作符(,)作为分界符,可以指定要输出的列
会话
什么是会话
在使用xshell登录linux服务器时,linux会为我们创建一个终端文件,还创建一个bash进程,以进程组的方式来给我们提供命令行解释的服务。bash进程通过终端文件进行读写数据,
一个会话内部可以有多个进程组。
我们再看这样的命令:
加上&表示让进程在后端运行,同时继续运行自己写的process_task进程:
我们发现,运行的sleep和process_task,它们属于不同的进程组。更重要的是,后面有一列叫SID,即会话ID,它们的会话ID是一样的,会话ID是一样的。会话ID一般是会话中的第一个进程,一般是bash。会话可以看成是一个或多个进程组的集合, 一个会话可以包含多个进程组。 每一个会话也有一个会话 ID(SID)。
如果想把这个进程放在后台,就在后面加&。
这种前台进程运行时,我们输入一些命令pwd、ls,发现没有反应。
实际上,同一个会话中,可以运行同时存在多个进程组,但是任何时刻只允许一个前台进程(组),可以允许多个后台进程(组)。当刚开始时,前台进程是bash,输入指令,bash就会响应,而当执行我们自己的任务进程时,我们的任务就变成了前台进程了,这是bash自动把自己切换到后台。当输入指令时,都给了前台进程,bash就收不到了,输入的命令当然就无法执行了。当把我们自己的进程终止了,bash又把自己重新切换到前台,等待用户输入。所以,我们现在发现,所谓前后台,就是谁应该从标准输入中获取数据!这就是后台进程使用ctrl+c也杀不掉的原因。这样正是我们自己运行的程序可以通过ctrl+c终止。
作业控制
作业是针对用户来讲, 用户完成某项任务而启动的进程, 一个作业既可以只包含一个进程, 也可以包含多个进程, 进程之间互相协作完成任务, 通常是一个进程管道。
当我们把完成某个作业的进程放在后台运行时,如下
[1]表示作业号,
我们再创建一个作业,
作业号就是2了,如果我们想查询当前的作业,可以使用jobs (-l),
这两个作业都在后台,如果想把某个作业放在前台,比如想把1号作业放在前台运行,就输入fg 1,此时这个作业就能接收键盘输入的命令,
如果想把刚才移到前台的作业放到后台,就需要先暂停这个前台作业,使用ctrl+z暂停,
然后再输入bg 2,将2号作业放到后台,
关于默认作业:对于一个用户来说,只能有一个默认作业(+) ,同时也只能有一个即将成为默认作业的作业(-),当默认作业退出后,该作业会成为默认作业。
常见作业状态
作业状态 | 含义 |
正在运行【Running】 | 后台作业(&),表示正在执行 |
完成【Done】 | 作业已完成,返回的状态码为0 |
完成并退出【Done(code)】 | 作业已完成并退出,返回的状态码非0 |
已停止【Stopped】 | 前台作业,当前被Ctrl+Z终止 |
已终止【Terminated】 | 作业被终止 |
守护进程
进程组,无论是前台还是后台,都属于同一个会话。如果我们把某一个进程组从当前会话a中拿出来,形成一个单独的会话b,这样和刚才的会话a由包含关系变成并列关系。即使把a会话删除,也并不影响b会话,我们把这个进程称为守护进程。这个守护进程不会因用户登录或退出受影响,那一个进程如何将自己变成独立的会话从而变成守护进程呢?就需要使用setsid函数:
#include <unistd.h>
pid_t setsid(void); # 功能:创建会话# 返回值:创建成功返回SID,失败返回-1
当某一个进程调用setsid函数来创建一个会话,前提是调用进程不能是一个进程组的组长。需要注意的是,这个接口如果调用进程原来是进程组组长,则会报错。为了避免这种情况,可以使用fork创建子进程,然后让父进程退出,子进程调用setsid。此时这个子进程(守护进程)就变成了孤儿进程,守护进程是孤儿进程的一种特殊情况。
该调用接口后会发生:
- 调用进程会变成新会话的会话首进程。此时,新会话中只有一个唯一一个进程。
- 调用进程会变成进程组的组长,新进程组ID就是当前调用进程ID。
- 该进程没有控制终端,如果在调用setsid之前该进程存在控制终端,则调用之后会切断联系。
在变成守护进程后,不需要和用户进行输入输出了,我们可以将012重定向到/dev/null,这样就从/dev/null里读不到内容,写入/dev/null的内容也会被自动丢弃(/dev/null是一个字符文件)。也就是说,我们可以以读写方式打开/dev/null,把012全部重定向到/dev/null。并且可以选择是否更改工作目录。我们把守护进程写成一个函数Daemon:
#include <iostream>
#include <string>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstdlib>const std::string defaultpath = "/";
const std::string defaultdev = "/dev/null";void Daemon(bool ischdir, bool isclose)
{// 1.忽略不要的信号signal(SIGCHLD, SIG_IGN);signal(SIGPIPE, SIG_IGN);// 2.forkif (fork() > 0)exit(0);// 3.setsidsetsid();// 4.确认是否要更改工作目录if (ischdir)chdir(defaultpath.c_str());// 5.对012进行重定向if (isclose){::close(0);::close(1);::close(2);}else{int fd = open(defaultdev.c_str(), O_RDWR);if(fd > 0){dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);::close(fd);}}
}
然后再main函数中调用Daemon,让当前进程成为守护进程,
此时这个process_task进程并没有结束,而是在后端运行。由于process_task不属于当前会话,因此不会阻塞我们输入的命令。并且我们可以查到这个进程:
我们发现其ppid是1,也就是变成了孤儿进程,被系统领养。其pid、pgid、sid都是1086564,自成进程组,自成会话。其所对应的默认工作路径:
其所对应的012都指向了/dev/null:
未来如果后面有打印输入的程序,就什么都不做。
如果想让这种进程结束,只能使用kill命令。如果我们把Deamon里面的参数设为true,那么
发现其工作目录变成了根目录,
由于所有的012都被关掉了,就看不到了。