0%

linux进程(二)

Linux进程(二)

进程的状态

  • Linux 系统下进程通常存在 6 种不同的状态,分为:就绪态、运行态、僵尸态、可中断睡眠状态(浅度 睡眠)、不可中断睡眠状态(深度睡眠)以及暂停态。
    • 就绪态(Ready):指该进程满足被 CPU 调度的所有条件但此时并没有被调度执行,只要得到 CPU 就能够直接运行;意味着该进程已经准备好被 CPU 执行,当一个进程的时间片到达,操作系统调 度程序会从就绪态链表中调度一个进程;
    • 运行态:指该进程当前正在被 CPU 调度运行,处于就绪态的进程得到 CPU 调度就会进入运行态;
    • 僵尸态:僵尸态进程其实指的就是僵尸进程,指该进程已经结束、但其父进程还未给它“收尸”;
    • 可中断睡眠状态:可中断睡眠也称为浅度睡眠,表示睡的不够“死”,还可以被唤醒,一般来说可 以通过信号来唤醒;
    • 不可中断睡眠状态:不可中断睡眠称为深度睡眠,深度睡眠无法被信号唤醒,只能等待相应的条件 成立才能结束睡眠状态。把浅度睡眠和深度睡眠统称为等待态(或者叫阻塞态),表示进程处于一 种等待状态,等待某种条件成立之后便会进入到就绪态;所以,处于等待态的进程是无法参与进程 系统调度的。
    • 暂停态:暂停并不是进程的终止,表示进程暂停运行,一般可通过信号将进程暂停,譬如 SIGSTOP 信号;处于暂停态的进程是可以恢复进入到就绪态的,譬如收到 SIGCONT 信号。

image-20220121212212609

进程组

  • 每个进程除了有一个进程 ID、父进程 ID 之外,还有一个进程组 ID,用于标识该进程属于哪一个进程 组,进程组是一个或多个进程的集合,这些进程并不是孤立的,它们彼此之间或者存在父子、兄弟关系,或 者在功能上有联系。

  • Linux 系统设计进程组实质上是为了方便对进程进行管理。假设为了完成一个任务,需要并发运行 100 个进程,但当处于某种场景时需要终止这 100 个进程,若没有进程组就需要一个一个去终止,这样非常麻烦 且容易出现一些问题;有了进程组的概念之后,就可以将这 100 个进程设置为一个进程组,这些进程共享一 个进程组 ID,这样一来,终止这 100 个进程只需要终止该进程组即可。

  • 进程组的特性

    • 每个进程必定属于某一个进程组、且只能属于一个进程组;
    • 每一个进程组有一个组长进程,组长进程的 ID 就等于进程组 ID;
    • 在组长进程的 ID 前面加上一个负号即是操作进程组;
    • 组长进程不能再创建新的进程组;
    • 只要进程组中还存在一个进程,则该进程组就存在,这与其组长进程是否终止无关;
    • 一个进程组可以包含一个或多个进程,进程组的生命周期从被创建开始,到其内所有进程终止或离 开该进程组;
    • 默认情况下,新创建的进程会继承父进程的进程组 ID。
  • 通过系统调用 getpgrp()或 getpgid()可以获取进程对应的进程组 ID

#include <unistd.h>
pid_t getpgid(pid_t pid);//对应的线程的进程组ID
pid_t getpgrp(void);
  • 调用系统调用 setpgid()或 setpgrp()可以加入一个现有的进程组或创建一个新的进程组
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
int setpgrp(void);
  • setpgid()函数将参数 pid 指定的进程的进程组 ID 设置为参数 gpid。如果这两个参数相等(pid==gpid), 则由 pid 指定的进程变成为进程组的组长进程,创建了一个新的进程;如果参数 pid 等于 0,则使用调用者 的进程 ID;另外,如果参数 gpid 等于 0,则创建一个新的进程组,由参数 pid 指定的进程作为进程组组长 进程
  • setpgrp()函数等价于 setpgid(0, 0)。
  • 一个进程只能为它自己或它的子进程设置进程组 ID,在它的子进程调用 exec 函数后,它就不能更改该 子进程的进程组 ID 了

会话

  • 会话是一个或多个进程组的集合

  • image-20220121213639860

  • 一个会话可包含一个或多个进程组,但只能有一个前台进程组,其它的是后台进程组;每个会话都有一 个会话首领(leader),即创建会话的进程。

  • 一个会话可以有控制终端、也可没有控制终端,在有控制终端的情况下也只能连接一个控制终端,这通常是登录到其上的终端设备(在终端登录情况下)或伪终端设备 (譬如通过 SSH 协议网络登录),一个会话中的进程组可被分为一个前台进程组以及一个或多个后台进程 组。

  • 会话的首领进程连接一个终端之后,该终端就成为会话的控制终端,与控制终端建立连接的会话首领进 程被称为控制进程;产生在终端上的输入和信号将发送给会话的前台进程组中的所有进程,譬如 Ctrl + C(产 生 SIGINT 信号)、Ctrl + Z(产生 SIGTSTP 信号)、Ctrl + \(产生 SIGQUIT 信号)等等

  • 一个进程组由组长进程的 ID 标识,而对于会话来说,会话的首领进程的进程组 ID 将作为该会话的标 识,也就是会话 ID(sid),在默认情况下,新创建的进程会继承父进程的会话 ID。通过系统调用 getsid()可 以获取进程的会话 ID

#include <unistd.h>
pid_t getsid(pid_t pid);
  • 使用系统调用 setsid()可以创建一个会话,其函数原型如下所示
#include <unistd.h>
pid_t setsid(void);
  • 如果调用者进程不是进程组的组长进程,调用 setsid()将创建一个新的会话调用者进程是新会话的首 领进程,同样也是一个新的进程组的组长进程,调用 setsid()创建的会话将没有控制终端。

守护进程

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

  • 长期运行。守护进程是一种生存期很长的一种进程,它们一般在系统启动时开始运行,除非强行终 止,否则直到系统关机都会保持运行。与守护进程相比,普通进程都是在用户登录或运行程序时创 建,在运行结束或用户注销时终止,但守护进程不受用户登录注销的影响,它们将会一直运行着、 直到系统关机。

  • 与控制终端脱离。在 Linux 中,系统与用户交互的界面称为终端,每一个从终端开始运行的进程都 会依附于这个终端,这是上一小节给大家介绍的控制终端,也就是会话的控制终端。当控制终端被 关闭的时候,该会话就会退出,由控制终端运行的所有进程都会被终止,这使得普通进程都是和运 行该进程的终端相绑定的;但守护进程能突破这种限制,它脱离终端并且在后台运行,脱离终端的 目的是为了避免进程在运行的过程中的信息在终端显示并且进程也不会被任何终端所产生的信息 所打断。

  • 守护进程 Daemon,通常简称为 d,一般进程名后面带有 d 就表示它是一个守护进程。

创建守护进程的步骤

  • 创建子进程、终止父进程
    • 父进程调用 fork()创建子进程,然后父进程使用 exit()退出,这样做实现了下面几点。第一,如果该守护 进程是作为一条简单地 shell 命令启动,那么父进程终止会让 shell 认为这条命令已经执行完毕。第二,虽然 子进程继承了父进程的进程组ID,但它有自己独立的进程ID,这保证了子进程不是一个进程组的组长进程, 这是下面将要调用 setsid 函数的先决条件!
  • 子进程调用 setsid 创建会话
    • 这步是关键,在子进程中调用上一小节给大家介绍的 setsid()函数创建新的会话,由于之前子进程并不 是进程组的组长进程,所以调用 setsid()会使得子进程创建一个新的会话,子进程成为新会话的首领进程, 同样也创建了新的进程组、子进程成为组长进程,此时创建的会话将没有控制终端。所以这里调用 setsid 有 三个作用:让子进程摆脱原会话的控制、让子进程摆脱原进程组的控制和让子进程摆脱原控制终端的控制。 在调用 fork 函数时,子进程继承了父进程的会话、进程组、控制终端等,虽然父进程退出了,但原先 的会话期、进程组、控制终端等并没有改变,因此,那还不是真正意义上使两者独立开来。setsid 函数能够 使子进程完全独立出来,从而脱离所有其他进程的控制
  • 将工作目录更改为根目录
    • 子进程是继承了父进程的当前工作目录,由于在进程运行中,当前目录所在的文件系统是不能卸载的, 这对以后使用会造成很多的麻烦。因此通常的做法是让“/”作为守护进程的当前目录,当然也可以指定其 它目录来作为守护进程的工作目录。
  • 重设文件权限掩码 umask
    • 文件权限掩码 umask 用于对新建文件的权限位进行屏蔽,在 5.5.5 小节中有介绍。由于使用 fork 函数新 建的子进程继承了父进程的文件权限掩码,这就给子进程使用文件带来了诸多的麻烦。因此,把文件权限掩 码设置为 0,确保子进程有最大操作权限、这样可以大大增强该守护进程的灵活性。设置文件权限掩码的函 数是 umask,通常的使用方法为 umask(0)。
  • 关闭不再需要的文件描述符
    • 子进程继承了父进程的所有文件描述符,这些被打开的文件可能永远不会被守护进程(此时守护进程指 的就是子进程,父进程退出、子进程成为守护进程)读或写,但它们一样消耗系统资源,可能导致所在的文 件系统无法卸载,所以必须关闭这些文件,这使得守护进程不再持有从其父进程继承过来的任何文件描述 符。
  • 将文件描述符号为 0、1、2 定位到/dev/null
    • 将守护进程的标准输入、标准输出以及标准错误重定向到/dev/null,这使得守护进程的输出无处显示、 也无处从交互式用户那里接收输入。
  • 其它:忽略 SIGCHLD 信号
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
int main(void)
{
pid_t pid;
int i;
/* 创建子进程 */
pid = fork();
if (0 > pid)
{
perror("fork error");
exit(-1);
}
else if (0 < pid) //父进
exit(0); //直接退出
/*
*子进程
*/
/* 1.创建新的会话、脱离控制终端 */
if (0 > setsid())
{
perror("setsid error");
exit(-1);
}
/* 2.设置当前工作目录为根目录 */
if (0 > chdir("/"))
{
perror("chdir error");
exit(-1);
}
/* 3.重设文件权限掩码 umask */
umask(0);
/* 4.关闭所有文件描述符 */
for (i = 0; i < sysconf(_SC_OPEN_MAX); i++)
close(i);
/* 5.将文件描述符号为 0、1、2 定位到/dev/null */
open("/dev/null", O_RDWR);
dup(0);
dup(0);
/* 6.忽略 SIGCHLD 信号 */
signal(SIGCHLD, SIG_IGN);
/* 正式进入到守护进程 */
for (;;)
{
sleep(1);
puts("守护进程运行中......");
}
exit(0);
}

image-20220121223837693

  • 可见没有输出,因为输出已经重定向到dev/null

  • ps -ajx中看到这个进程

  • image-20220121224001123

  • 这个进程可以用kill pid停止

单例模式

  • 对于有些程序设计来说,程序只能被执行一次,只要该程序没有结束,就无法 再次运行,我们把这种情况称为单例模式运行,多次同时运行并没有意义、甚至还会带来错误!。

方法:

  • 通过一个特定的文件存在与否进行判断(进程开始的时候新建一个文件,结束的时候删除)
  • 文件锁
    • 当程序启动之后,首先打开该文件,调用 open 时一般使用 O_WRONLY | O_CREAT 标志,当文件不存在则创建该文件,然后尝试去获取文件锁,若是成功,则将程序 的进程号(PID)写入到该文件中,写入后不要关闭文件或解锁(释放文件锁),保证进程一直持有该文件 锁;若是程序获取锁失败,代表程序已经被运行、则退出本次启动。
    • 通过系统调用 flock()fcntl()或库函数 lockf()均可实现对文件进行上锁,本小节我们以系统调用flock()为例,系统调用 flock() 产生的是咨询锁(建议性锁)、并不能产生强制性锁
#include <stdio.h>
#include <stdlib.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#define LOCK_FILE "./testApp.pid"
int main(void)
{
char str[20] = {0};
int fd;
/* 打开 lock 文件,如果文件不存在则创建 */
fd = open(LOCK_FILE, O_WRONLY | O_CREAT, 0666);
if (-1 == fd)
{
perror("open error");
exit(-1);
}
/* 以非阻塞方式获取文件锁 */
if (-1 == flock(fd, LOCK_EX | LOCK_NB))
{
fputs("不能重复执行该程序!\n", stderr);
close(fd);
exit(-1);
}
puts("程序运行中...");
ftruncate(fd, 0); //将文件长度截断为 0
sprintf(str, "%d\n", getpid());
write(fd, str, strlen(str)); //写入 pid
for (;;)
sleep(1);
exit(0);
}

image-20220121225414970

  • 首先打开一个特定的文件,这里只是举例,以当前目录下的 testApp.pid 文件作为特定文件, 以 O_WRONLY | O_CREAT 方式打开,如果文件不存在则创建该文件;打开文件之后使用 flock 尝试获取文 件锁,调用 flock()时指定了互斥锁标志 LOCK_NB,意味着同时只能有一个进程拥有该锁,如果获取锁失败, 表示该程序已经启动了,无需再次执行,然后退出;如果获取锁成功,将进程的 PID 写入到该文件中,当程 序退出时,会自动解锁、关闭文件