进程和PCB
基本介绍
在这之前,我们大多是听过程序,但是这里我们要说的却是进程,迄今为止并没有对进程有一个全面而详细的描述,但是进程和程序的主要区别如下:
- 程序是永存的;进程是暂时的,是程序在数据集上的一次执行,有创建有撤销,存在是暂时的;
- 程序是静态的观念,进程是动态的观念;
- 进程具有并发性,而程序没有;
- 进程是竞争计算机资源的基本单位,程序不是。
- 进程和程序不是一一对应的: 一个程序可对应多个进程即多个进程可执行同一程序; 一个进程可以执行一个或几个程序
由此我们可以给出进程的定义:一个执行中程序的实例,即一个正在执行的程序。
如果站在内核的角度来看:进程是分配系统资源的单位。
PCB
这里的PCB可不指的是电路版,而是进程控制块,进程中的信息就被放在了一个叫做进程控制块(PCB)的结构体中。
在不同的操作系统下进程控制块的名称不同,在Linux操作系统中PCB的具体名称是:task_struct
。
当一个程序被加载到内存中要开始执行的时候,操作系统同时会给该进程分配一个PCB,在Linux中就是 task_struct
这里面包含了所有关于进程的数据信息。所以CPU对 task_struct
进行管理就相当于对进程进行管理。
task_struct
task_struct包含以下内容:
- 标识符:与进程相关的唯一标识符,用来区别其他进程
- 状态:进程会有不同的状态,如运行,停止等等
- 优先级:相对于其他进程的优先顺序
- 程序计数器:程序中即将执行的下一条指令的地址
- 内存指针:包括程序代码和进程相关数据的是很
- 上下文信息:进程执行时CPU的寄存器中的数据
- IO状态信息: 包括显示的I/O请求,分配给进程的I/O设备和正在被进程使用的文件列表。
- 记账信息:可能包括处理器时间总和,使用的时钟总数,时间限制,记账号等
进程标示符: 描述本进程的唯一标示符,用来区别其他进程
就是进程的PID,PID是操作系统中唯一标识的进程号。
有两个获得进程PID的方式:
int main() { //下面两个函数分别获取进程PID和父进程PID printf(“pid=%d, ppid=%d\n”, getpid(), getppid()); return 0; }
`进程的状态`
| 状态 | 中文名称 | 描述 |
| --- | --- | --- |
| R(TASK_RUNNING) | 可执行状态 | 运行或者即将运行的状态 |
| S(TASK_INTERRUPTIBLE) | 可中断的睡眠状态 | 被阻断而等待,可以被一个信号激活 |
| D(TASK_UNINTERRUPTIBLE) | 不可中断的睡眠状态 | 被阻断而等待,不可以被信号激活 |
| T(TASK_STOPPED ) | 暂停状态或跟踪状态 | 由于任务的控制或者外部跟踪而被终止 |
| Z(TASK_ZOMBIE) | 退出状态 | 进程成为僵尸进程,但是它的父进程还没调用wait函数 |
| X(TASK_DEAD) | 死亡状态 | 这个状态永远看不到 |
`优先级`
因为CPU资源有限,而进程却有很多个,所以需要优先级这个属性去决定了进程拿到资源的顺序。
`程序计数器`
**程序中即将被执行的下一条指令的地址**
CPU有三个工作:取指令,分析指令和执行指令。CPU中的指令寄存器每一次都会保存下一条指令的地址,以此来进行指令判断。
`内存指针`
**包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。**
`上下文数据`
通常操作系统内核使用一种叫做**上下文切换**的方式来实现控制流。
实行这种机制是因为CPU只有一个,同一时间只能执行一个程序(也就是说,CPU只有一套寄存器)所以只能有将一个进程的存储数据放入寄存器中计算,但是同时有多个进程的时候,操作系统为了使得CPU的利用率最高,所以会让进程之间来回的切换,从而形成了上下文数据。
一般进程切换有两种情况:
1. 并发运行:每个进程执行它的控制流的那一段时间叫做 **时间片**,也就是每个任务都由最多运行时间的限制,如果超出了最多运行时间限制(也就是时间片用完),那么就会自动让出CPU资源。
2. 抢占运行:当操作系统内核,发现一个优先级更高的进程的时候,该优先级更高的进程就会”抢占“当前进程的位置,然后执行优先级更高的进程。等到该进程执行完后,在执行“被 抢占”的进程。这种决策方式叫做 **调度**。
以上两种情况,都会使得进程莫名其妙的退出CPU的执行,但是下次CPU还想接着上一次执行的地方继续执行那个莫名其妙退出的进程,所以就需要在进程退出之前,在 `task_struct`中保留下上一次执行的数据,方便下一次再被执行。
`I/O信息`
**显示I/O请求,分配给进程的I/O设备和被进程使用的文件列表。**
我们首先写一个简单的程序,程序就不放出来了,程序内部就是一个死循环,然后执行这个程序。
接着我们再打开一个终端,执行以下命令:
![](https://info.xinmouren.cn//blog-article-image/202111052048500.png#crop=0&crop=0&crop=1&crop=1&id=sDdny&originHeight=382&originWidth=1528&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
<a name="a65275d4"></a>
### 组织进程
所有运行在系统里的进程都以 `task_struct`双链表链表的形式存在内核里。如果在情况复杂的情况下,双链表中的节点也有可能存在在其他的数据结构中,例如队列等。
<a name="014fad3c"></a>
### 查看进程
查看进程由三种方式,下面来介绍这三种方式。
<a name="5fc2a2f6"></a>
#### 通过系统目录
在 `/proc`这个目录下保存着所有进程的信息,每个进行都会分配一个目录,用来存放该进程的信息。
![](https://info.xinmouren.cn//blog-article-image/202111052052900.png#crop=0&crop=0&crop=1&crop=1&id=oJ1LX&originHeight=343&originWidth=1468&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
上图中蓝色的就是各个进程的PID号,当你创建一个进程的时候,就会在这个目录下找到对应的PID。
<a name="29bcf4d7"></a>
#### 通过PS命令
```bash
ps aux # 查看系统中所有的进程信息
ps axj # 可以查看进程的父进程号
ps命令查看到的各个参数如下:
- USER: 进程拥有者
- PID: pid
- %CPU: 占用的 CPU 使用率
- %MEM: 占用的记忆体使用率
- VSZ: 占用的虚拟记忆体大小
- RSS: 占用的记忆体大小
- TTY: 终端的次要装置号码 (minor device number of tty)
- STAT: 该进程的状态:
- D: 无法中断的休眠状态 (通常 IO 的进程)
- R: 正在执行中
- S: 静止状态
- T: 暂停执行
- Z: 不存在但暂时无法消除
- W: 没有足够的记忆体分页可分配
- <: 高优先序的行程
- N: 低优先序的行程
- L: 有记忆体分页分配并锁在记忆体内 (实时系统或捱A I/O)
- START: 行程开始时间
- TIME: 执行的时间
- COMMAND:所执行的指令
通过top命令
top # 动态的查看进程的信息,其中的信息默认3秒回更新
进程的状态
查看进程的状态
查看进程的状态可以使用以下命令:
ps aux
或者
ps axj
进程状态详解
在内核源码中有如下定义:
static const char *const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
R(运行态)
处于R状态的进程有的在cpu中执行,但是很多都是在运行队列中等待运行,也就是该进程允许被调度。
实现:
可以运行任意一个可运行的程序,即可出现R状态。
运行下面的代码:
#include <stdio.h>
int main(void)
{
printf("i am running\n");
while (1)
{
}
return 0;
}
另外打开一个终端,然后使用 ps
命令查看进程的状态:
S (睡眠态)
这种状态是一种浅度睡眠,此时的进程是在被阻塞的状态中,等待着条件的满足过后进程才可以运行。在这种状态下可以被信号激活,也可以被信号杀死。
实现:
可以使用 sleep()
使得一个进程睡眠。
运行下面的程序:
#include <stdio.h>
int main(void)
{
printf("i am running\n");
while (1)
{
printf("i am sleeping\n");
sleep(100); // 睡眠100秒
}
return 0;
}
另外打开一个终端,然后使用 ps
命令查看进程的状态:
D(磁盘休眠状态)
这种状态是一种深度休眠的状态,在这种状态下即使是操作系统发送信号也不可以杀死进程,只能等待进程自动唤醒才可以。
这种情况没法模拟实现,一般都是一个进程正在对IO这样的外设写入或者读取的时候,为了防止操作系统不小心杀掉这个进程,所以特地创建出一个状态保护这种进程。
T(停止状态)
可以通过发送 SIGSTOP
信号给进程来停止进程。这个被暂停的进程可以通过发送 SIGCONT
信号让进程继续运行。
kill -SIGSTOP PID // 停止进程
kill -SIGCONT PID // 继续进程
实现:
运行如下代码:
#include <stdio.h>
int main(void)
{
printf("i am running\n");
while (1)
{
}
return 0;
}
然后另外打开一个终端:
在暂停进程后,可以在第一个终端中看到:
{% tip warning %}在停止进程后,可以看到第一个终端中的程序好像退出了,但是当我们查看的时候发现进程依旧存在,也就是我们无法再用 ctrl+c 来结束这个进程了{% endtip %}
X(死亡状态)
进程停止执行,进程不能再次投入运行(与停止进程区分)。通常这种状态发生在接受到 SIGSTOP
、SIGTSTP
、SIGTTIN
、SIGOUT
等信号的时候。
实现:
可以使用 kill -9 PID
即可杀死一个进程
接下来我们使用上述命令来杀死上面实验中遗留的进程:
Z(僵死状态)
后面会详细说明这种情况。
孤儿进程
如果父进程比子进程先退出,那么此时子进程就叫做孤儿进程。(也就是没有爸了,成孤儿了)而操作系统不会让这个子进程孤苦伶仃的运行在操作系统中,所以此时孤儿进程会被 init
进程(也就是1号进程,即所有进程的祖先,上面讲解fork的时候遇到过)领养,从此以后孤儿进程的状态和最后的PCB空间释放都是由 init
进程负责了。
运行下面这段程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
pid_t pid = fork();
if (pid == 0)
{ // 子进程一直执行,即死循环
while (1)
{
printf("I am a child, pid=%d, ppid=%d\n", getpid(), getppid());
sleep(1);
}
}
else
{
int count = 2; // 父进程执行2次
while (count--)
{
printf("I am a father, pid=%d, ppid=%d\n", getpid(), getppid());
sleep(1);
}
//执行2次之后退出
exit(1);
}
return 0;
}
然后运行这段程序,可以看到如下的输出情况:
可以看到,父进程运行两次之后,退出了,而我们的子进程一直在运行,当父进程退出之后,子进程被 init
进程收养,返回的父进程号为 1 。
⚠ 此时你会发现 ctrl+c 依旧无法杀死这个孤儿,所以我们需要使用杀死命令的进程:
僵尸进程
为什么会出现僵尸进程
进程的作用是为了给操作系统提供信息的,所以在进程调用结束之后,应该将该进程完成的任务情况汇报(**eixt code**
)给操作系统(也就是让操作系统知道进程已经结束了)但是进程在执行完之后已经结束了,所以此时进程的状态就是僵尸状态。
僵尸进程的概念
僵尸进程:即进程已经结束了,但是父进程没有使用 wait()
系统调用,此时父进程不能读取到子进程退出返回的信息,此时就该进程就进入僵死状态。
僵尸进程的危害
进程已经结束了,但是进程控制块PCB却还是没有被释放,这时就会浪费这一块资源空间。所以会导致操作系统的内存泄漏。
如何消灭僵尸进程
僵死状态需要父进程发出 wait()
系统调用终止进程,如果父进程不终止进程,那么此时要消灭僵尸进程只能通过找到僵尸进程的父进程,然后 **kill**
掉这个父进程,然后僵尸进程就会成为孤儿进程,此时由 **init**
进程领养这个进程然后杀死这个僵尸进程。
实现:
我们稍微修改一下上面的例程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if (pid == 0)
{
int count = 2; // 子进程运行2次
while (count--)
{
printf("I am a child, pid=%d, ppid=%d\n", getpid(), getppid());
sleep(1);
}
exit(0);
}
else
{ // 父进程一直运行
while (1)
{
printf("I am a father, pid=%d, ppid=%d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
运行之后可以看到,当子进程结束后,使用ps命令依旧可以看到这个进程,此时进程处于僵死状态:
进程的优先级
进程优先级的概念
进程优先级为进程获取 cpu
资源分配的先后顺序,即进程的优先权,优先级高的进程可以有优先执行的权力。
之所以会存在进程优先级,是因为 cpu
本身的资源分配是有限的,一个 cpu
一次只能运行一个进程,但是一个操作系统中可能会有成千上百的进程,所以需要存在进程优先级来确定每一个进程获得 cpu
资源分配的顺序。
查看进程的优先级
用 ps –al
或者 ps -l
命令则会类似输出以下几个内容:
其中:
- UID:执行者的身份,用户标识符
- PID:进程的编号
- PPID:进程的父进程的编号
- PRI:进程可被执行的优先级,PRI越小代表优先级越高
- NI:进程的nice值,代表进程优先级的修改数值
PRI和NI
PRI
和 NI
是一组对应的概念。NI
的取值会影响到 PRI
的最终值。
PRI
代表进程被 CPU
执行的先后顺序,并且 **PRI**
越小进程的优先级越高。NI
代表 nice
值,表示进程的优先级的修改数值。所以两者之间有一个计算的公式:**(new)PRI = (old)PRI + NI**
。
{% tip warning %}
1.PRI在系统中默认初始化为80。
2.NI的取值范围为-20 ~ 19,一共40个级别。
3.当NI为正值,PRI增大,进程优先级变高。当NI为负值,PRI变小,进程优先级变小。
{% endtip %}
所以在 **Linux**
环境下,我们一般说调整进程的优先级,就是在调整 **nice**
值。**nice**
值决定性的影响到进程优先级。
更改NI值
通过 top
命令更改 nice
值
- 使用
top
命令后,按r
键,要求你输入需要更改进程优先级的进程PID - 输入需要更改进程优先级的进程PID
- 输入你想要更改后的
nice
值
通过 renice
命令更改 nice
值
语法格式:
renice nice值 进程PID
进程的创建
fork()
常用的创建进程的方式有两种:
- 直接执行某个可执行文件,例如:
./a.out
,此时被运行的程序就会自动创建一个进程来运行。 - 使用系统接口函数:fork()
fork()的基本使用
当时用 fork()
函数之后,就在原来的进程中创建了一个子进程,在 fork()
之前的代码只被父进程执行,在 fork()
之后的代码有父子进程一起执行。
创建的子进程和父进程几乎一模一样,子进程可以获得父进程中所有的文件,只有PID是父子进程最大的不同。
接下来我们使用 fork()
来创建一个进程:
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if (pid < 0)
{
printf("error");
}
if (pid == 0)
{
printf("i am a child process\n");
}
else
{
printf("i am a father process\n");
}
return 0;
}
然后我们执行上述代码,运行结果如下:
在语句 pid=fork()
之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的几乎完全相同
{% p blue, fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值: %}
{% p blue, 1)在父进程中,fork返回新创建子进程的进程ID; %}
{% p blue, 2)在子进程中,fork返回0; %}
{% p blue, 3)如果出现错误,fork返回一个负值;%}
在 fork
函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork
函数返回0,在父进程中,fork
返回新创建子进程的进程ID。我们可以通过 fork
返回的值来判断当前进程是子进程还是父进程。
引用一位网友的话来解释fpid的值为什么在父子进程中不同。“其实就相当于链表,进程形成了链表,父进程的fpid(p 意味point)指向子进程的进程id,因为子进程没有子进程,所以其fpid为0
而 fork
有很多特点:
- fork函数调用一次,返回两次
- 并发执行
父子进程是两个并发运行的独立程序,父子进程谁先被调度是不能确定的。 - 相同但是独立的地址空间
两个进程其实地址空间是一样的,但是它们都有自己私有的地址空间,所以父子进程的运行都是独立的,一个进程中的内存不会影响另一个进程中的内存。 - 共享文件
子进程继承了父进程所有打开的文件,所以父进程调用fork的时候,stdout
文件是打开的,所以子进程中执行的内容也可以输出到屏幕上
fork()更加复杂的案例
下面有一份更加复杂的代码:
#include <unistd.h>
#include <stdio.h>
int main(void)
{
//ppid指当前进程的父进程pid
//pid指当前进程的pid,
//fpid指fork返回给当前进程的值
int i = 0;
printf("i\tson/par\tppid\tpid\tfpid\n");
for (i = 0; i < 2; i++)
{
pid_t fpid = fork();
if (fpid == 0)
printf("%d\tchild\t%4d\t%4d\t%4d\n", i, getppid(), getpid(), fpid);
else
printf("%d\tparent\t%4d\t%4d\t%4d\n", i, getppid(), getpid(), fpid);
}
return 0;
}
上面代码的运行结果如下:
下面来分析一下这份代码:
首先,在执行程序后,进行一次输出,随即进入我们的循环当中。
在循环中,执行第一次fork(),此时PID为 19720 进程进行一次fork(),本次fork()得到一个新的进程,进程PID为19722。
紧接着PID为 19720 的进程继续运行,迎来了第二次fork(),这次fork生成了PID为 197223的进程,然后,这个进程就退出了,注意此时这个进程已经完成,退出函数了。
此时,i = 1的时候,PID为 19720 的进程再一次创建了一个PID为19723 的进程,所以在输出中我们看到前两个均为同一个进程在运行,先后创建了两个进程
然后,第二次fork出的进程,也就是 PID 为19723 的进程,因为 i=1 ,所以循环退出,直接就结束了,这就是上图中第三行的输出,子进程 19723 运行完毕退出。
接着,第一次fork出的进程 19722 执行fork操作,注意,由于此时的 19720 进程已经退出,所以这个时候的 19722 进程是一个孤儿进程,所以看到第四行和第五行的输出中,19722 进程的父进程号为1。第四行为 19722 刚被创建的时候,此时作为子进程运行,第五行为 19722 执行fork操作,此时作为父进程,创建了 19727 这个进程,然后运行完毕退出。
最后,19727 运行完毕,此时因为 19722 已经退出,所以它也是一个孤儿进程。
exec函数族
exec函数族介绍
对于exec函数族来说,它的作用通俗来说就是使另一个可执行程序替换当前的进程,当我们在执行一个进程的过程中,通过exec函数使得另一个可执行程序A的数据段、代码段和堆栈段取代当前进程B的数据段、代码段和堆栈段,那么当前的进程就开始执行A中的内容,这一过程中不会创建新的进程,而且PID也没有改变。
✅ 也就是让父子进程执行不同的内容
{% tip bell %}
一般exec函数族的用途有以下两种:
- 当进程不需要再往下继续运行时,调用exec函数族中的函数让自己得以延续下去。
- 如果当一个进程想执行另一个可执行程序时,可以使用fork函数先创建一个子进程,然后过子进程来调用exec函数从而实现可执行程序的功能。
{% endtip %}
函数原型
通过 man
命令可以看到exec函数族的原型:
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
参数说明:
- path:要执行的程序路径。可以是绝对路径或者是相对路径。在execv、execve、execl和execle这4个函数中,使用带路径名的文件名作为参数。
- file:要执行的程序名称。如果该参数中包含“/”字符,则视为路径名直接执行;否则视为单独的文件名,系统将根据PATH环境变量指定的路径顺序搜索指定的文件。
- argv:命令行参数的矢量数组。
- envp:带有该参数的exec函数可以在调用时指定一个环境变量数组。其他不带该参数的exec函数则使用调用进程的环境变量。
- arg:程序的第0个参数,即程序名自身。相当于argv[0]。
- …:命令行参数列表。调用相应程序时有多少命令行参数,就需要有多少个输入参数项。注意:在使用此类函数时,在所有命令行参数的最后应该增加一个空的参数项(NULL),表明命令行参数结束。
- 返回值:一1表明调用exec失败,无返回表明调用成功。
它们都是以exec为前缀,那么不同的是后面的一些字符,l表示命令行参数列表、p表示PATH环境变量、v表示使用参数数组、e使用环境变量数组
举例说明
首先我们来看看 execl
函数,运行下面的例子:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
execl("/bin/ls", "ls", "-l", NULL);
perror("execl");
exit(1);
}
上面的例子输出如下:
这个程序很简单,就是用当前的进程调用 ls
这个可执行程序,并添加了 -l
参数。
由于 execl
成功调用后这个进程的代码段都被替换了,自然下面的代码就不会再执行了,所以也就没有返回值了,但是当调用失败后就会返回 -1
并设置 errno
值。那么在成功调用后实际上这个进程就变成了 ls
,然后执行 ls -l
的命令,因为我们用的是 execl
函数,所以第一个参数就需要用 ls
的所在目录,第二个参数其实没有实际意义,因为已经指定了 **ls**
的所在位置,所以第二个参数随便设置就可以但是不可以没有,第三个参数就是你所需要的功能,也就是你需要的命令参数,最后用NULL表示结束。
如果 execlp
,那么第一个参数就可以不用加ls的路径了,直接是ls就可以了,因为系统会去PATH中查找
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
execlp("ls", "ls", "-l", NULL);
perror("execl");
exit(1);
}
运行结果与上面相同。
如果是 execv
的话,后面的参数就要是一个指针数组的形式,可以看下面的代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
char *argv[] = {"ls", "-l", NULL};
execv("/bin/ls", argv);
perror("execl");
exit(1);
}
错误返回
一般的exec函数族的错误原因:
- 找不到文件或者路径,此时
errno
为ENOENT
。 - 数组
argv
和envp
(环境变量数组)没有以NULL结尾,此时errno
为EFAULT
。 - 没有对应可执行文件的运行权限,此时
errno
为EACCES
。
守护进程
什么是守护进程
守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
守护进程是个特殊的孤儿进程,这种进程脱离终端,为什么要脱离终端呢?
之所以脱离于终端是为了避免进程被任何终端所产生的信息所打断,其在执行过程中的信息也不在任何终端上显示。由于在 Linux 中,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。
{% tip bell %}
✅ Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。
✅守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。
✅一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。
✅守护进程的名称通常以d结尾,比如sshd、xinetd、crond等
{% endtip %}
查看守护进程
使用命令:
ps ajx
- a 表示不仅列当前用户的进程,也列出所有其他用户的进程
- x 表示不仅列有控制终端的进程,也列出所有无控制终端的进程
- j 表示列出与作业控制相关的信息
从上面我们可以看出:
- 守护进程基本上都是以超级用户启动( UID 为 0 )
- 没有控制终端( TTY 为 ?)
- 终端进程组 ID 为 -1 ( TPGID 表示终端进程组 ID)
创建守护进程步骤
进程组与会话期
Linux以会话(session)、进程组的方式管理进程。
进程组 :
- 每个进程都属于一个进程组
- 每个进程组都有一个进程组号,该号等于该进程组组长的PID号 .
- 一个进程只能为它自己或子进程设置进程组ID号
会话期:
- 会话期(session)是一个或多个进程组的集合。
setsid()函数
setsid()函数可以建立一个会话期。
如果,调用setsid的进程不是一个进程组的组长,此函数创建一个新的会话期。并具有以下特性:
- 此进程变成该会话期的首进程
- 此进程变成一个新进程组的组长进程
- 此进程没有控制终端,如果在调用setsid前,该进程有控制终端,那么与该终端的联系被解除。
- 如果该进程是一个进程组的组长,此函数返回错误。
- 为了保证这一点,我们先调用fork()然后exit(),此时只有子进程在运行,也就是让子进程成为孤儿进程
创建守护进程
编写守护进程的一般步骤步骤:
- 在父进程中执行fork并exit推出;
- 在子进程中调用setsid函数创建新的会话;
- 在子进程中调用chdir函数,让根目录 ”/” 成为子进程的工作目录;
- 在子进程中调用umask函数,设置进程的umask为0;
- 在子进程中关闭任何不需要的文件描述符
详细说明:
{% p red, 1. 在后台运行 %}
创建子进程,父进程退出。
if (fork() > 0)
{
exit(0);
}
此时子进程变成孤儿进程,被init进程收养。
{% p red, 2. 脱离控制终端,登录会话和进程组 %}
子进程创建新会话。
if (setsid() < 0)
{
exit(-1);
}
当进程是会话组长时setsid()调用失败。但第一点已经保证进程不是会话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。
{% tip bolt %}
Linux中的进程与控制终端,登录会话和进程组之间的关系:
- 进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。
- 登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。
- 控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。
{% endtip %}
{% p red, 3. 禁止进程重新打开控制终端 %}
现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端:
if(fork() > 0)
{
exit(0);
}
结束第一子进程,第二子进程继续(第二子进程不再是会话组长)。
{% p red, 4.改变当前工作目录 %}
进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。
chdir("/");
chdir("/tmp");
对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如 /tmp
{% p red, 5.重设文件权限掩码 %}
进程从创建它的父进程那里继承了文件权限掩码。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件权限掩码清除。
if (umask(0) < 0)
{
exit(-1);
}
{% tip bolt %}
umask是用来指定”目前用户在新建文件或者目录时候的权限默认值”
{% endtip %}
{% p red, 6.关闭打开的文件描述符 %}
进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误
int i;
for(i=0; i<getdtablesize(); i++)
{
close(i);
}
实例
下面创建一个定时(1s)将系统时间写入文件的守护进程:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
int main()
{
pid_t pid;
FILE *fp;
time_t t;
int i;
//捕捉错误
if ((pid = fork()) < 0)
{
perror("fork");
exit(-1);
}
//父进程退出
else if (pid > 0)
{
exit(0);
}
//fork之后,父进程退出,以下代码只有子进程执行
//创建一个会话
setsid();
//重置文件权限掩码
umask(0);
//切换工作目录
chdir("/tmp");
//关闭所有的文件描述符
for (i = 0; i < getdtablesize(); i++)
{
close(i);
}
//创建新的文件
if ((fp = fopen("time.log", "a")) == NULL)
{
perror("fopen");
exit(-1);
}
while (1)
{
time(&t);
fprintf(fp, "%s", ctime(&t));
fflush(fp);
sleep(1);
}
}
运行结果如下:
进程的回收
wait函数
wait函数:父进程调用,回收子进程。
函数原型:
pid_t wait(int *status);
函数作用:
- 阻塞并等待子进程退出
- 回收子进程残留资源
- 获取子进程结束状态(退出原因)。
返回值:
- 成功:清理掉的子进程ID;
- 失败:-1 (没有子进程)
status参数:可使用 wait 函数传出参数 status 来保存进程的退出状态,借助宏函数来进一步判断进程终止的具体原因
- WIFEXITED(status) :判断子进程是否正常结束,返回值>0即为正常结束
- WEXITSTATUS(status):获取子进程返回值
- WIFSIGNALED(status):判断子进程是否(非正常结束)被信号约束,返回值>0即为异常结束
- WTERMSIG(status):获取结束子进程的信号类型
下面举例来说明:
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
pid_t pid, wpid;
int status;
pid = fork();
if (pid == -1)
{
perror("fork");
exit(1);
}
else if (pid == 0)
{
printf("I am child,my parent pid = %d\ngoing to sleep 3s\n", getppid());
sleep(3);
printf("-------------child die------------------\n");
return 0;
}
else
{
// 回收子进程,避免出现僵尸进程,注意这里是阻塞运行,也就是父进程会一直等到子进程结束
wpid = wait(&status);
if (wpid == -1)
{
perror("wait error:");
exit(1);
}
// 正常退出
if (WIFEXITED(status))
{
printf("child exit normal,return: %d\n", WEXITSTATUS(status));
}
// 异常退出
if (WIFSIGNALED(status))
{
printf("child exit error,return: %d\n", WTERMSIG(status));
}
while (1)
{
printf("I am parent,my pid = %d,my son = %d\n", getpid(), pid);
sleep(1);
}
}
return 0;
}
运行结果如下:
waitpid函数
函数原型:
pid_t waitpid(pid_t pid,int *status,int option)
参数说明:
- pid:可用于指定回收哪个子进程或者任意子进程(-1)
- status:指定用于保存子进程返回值和结束方式的地址
- option:指定回收方式
- 0 :堵塞,子进程结束后返回
- WNOHANG :非堵塞,若未结束,则直接返回0
返回值:
- 成功:回收的子进程pid或0
- 失败:返回EOF
status参数:可使用 wait 函数传出参数 status 来保存进程的退出状态,借助宏函数来进一步判断进程终止的具体原因
- WIFEXITED(status) :判断子进程是否正常结束,返回值>0即为正常结束
- WEXITSTATUS(status):获取子进程返回值
- WIFSIGNALED(status):判断子进程是否(非正常结束)被信号约束,返回值>0即为异常结束
- WTERMSIG(status):获取结束子进程的信号类型