目录

 

进程的本质

进程在内核中的描述——task_struct

创建进程

进程的状态

进程优先级

进程切换

进程存在的意义

结语


进程的本质

进程是程序的执行实例,是资源分配的基本单位。这么说太官方了,实际上,跑起来的程序就叫做进程。每个进程都有自己的内存和资源,互不打扰,各行其是。比如在电脑上同时打开浏览器和VSCode,它们就是两个不同的两个进程。

更具体的说明就是,通过命令ps aux查到的全是进程。

进程在内核中的描述——task_struct

通过以上对进程的简单理解,不难发现计算机中的进程是很多的。这里提出两个问题,第一个问题:那么多的进程,操作系统要不要将它们管理起来?第二个问题:怎么管理?

不管是从操作系统的安全性还是系统性能上来讲,那么多的进程肯定是要被管理起来的。管理的方法论基本上是固定的——先描述,再组织。也就是说,先把每一个进程的相关信息保存到一个结构体中,然后再用链表把这些结构体组织起来,这样,操作系统就可以对系统中的所有进程进行管理了。保存进程相关信息的结构体叫做进程控制块PCB(process control block),在Linux中,这个进程控制块叫做task_struct

//task_struct部分源码
pid_t pid;                   // 进程的唯一标识符
pid_t tgid;                  // 线程组 ID
volatile long state;         // 任务状态
int exit_state;              // 退出状态
int exit_code;               // 退出代码
int exit_signal;             // 退出信号
int prio;                    // 动态优先级
int static_prio;             // 静态优先级
struct mm_struct *mm;        // 内存管理结构
struct vm_area_struct *mmap; // 虚拟内存区域链表
struct files_struct *files;  // 文件结构体
struct fdtable *fdt;         // 文件描述符表
//...

创建进程

系统提供了创建进程的接口fork()。

pid_t fork(void);

fork函数值得一讲的就是它的返回值。如果子进程创建成功,父进程接收到的返回值是子进程的pid,子进程接收到的返回值是0.如果子进程创建失败,那么父进程接收到的返回值是-1。下面演示一下如何创建进程。

#include <stdio.h>
#include <unistd.h>

int main()
{
    pid_t pid = fork();
    if (pid == -1) {
        // fork failed
        perror("fork");
    }else if (pid == 0) {
        // child process
        printf("Child process:%d\n", getpid());  
    }else{
        // parent process
        printf("Parent process:%d\n", getpid());
    }

    return 0;
}

//执行结果:
Parent process:7483
Child process:7484

在创建完子进程后,父进程和子进程会从fork()的下一行开始执行,所以一般用 if 来进行分流执行。

进程的状态

在Linux内核中,进程的状态分为以下几种。

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运行状态(running): 并不意味着进程⼀定在运行中,它表明进程要么是在运行中,要么在运行队列里。
• S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
(interruptible sleep))。
• D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
• T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。其他进程可以通过发送 SIGCONT 信号让这个被暂停的进程继续运行。
• Z状态(zombie):父进程没有读取子进程的退出信息就会出现僵尸进程。

下面,介绍一个查看进程状态的命令:ps aux / ps ajx

a:表示显示所有进程。

u:以用户友好的格式显示进程信息。通常包括进程的用户、PID、CPU 和内存使用率等信息。

x:显示没有控制终端的进程。

j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息
 

下面介绍两个特殊进程:僵尸进程和孤儿进程。

先来说说僵尸进程,它的成因是子进程退出后,父进程没有读取子进程的退出信息,导致子进程成为僵尸进程。下面写一段代码演示僵尸进程的产生。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork");
    }else if (pid == 0){
        printf("Child process[pid:%d]\n", getpid());
        sleep(3);
        exit(EXIT_SUCCESS);
    }else {
        printf("Parent process[pid:%d]\n", getpid());
        sleep(30);
    }
    return 0;
}

//运行结果:
Parent process[pid:9072]
Child process[pid:9073]

为什么我们在写程序的时候要避免僵尸进程?

因为僵尸进程会导致系统内核资源泄露,比如进程的pid,pid的数量是有限的,用一个就少一个,僵尸进程占着pid,但却什么都不做,这就是一种浪费。

如何避免产生僵尸进程?

父进程调用wait() / waitpid()函数来读取子进程的退出信息即可。

 pid_t wait(int *_Nullable wstatus);
 pid_t waitpid(pid_t pid, int *_Nullable wstatus, int options);

 wait()  函数使调用它的父进程暂停执行,直到它的一个子进程结束(即调用  exit()  或  _exit() ),并返回该子进程的进程 ID(PID)。如果调用成功,还可以通过指针参数  wstatus  获取子进程的退出状态。成功时,返回终止的子进程的PID,失败时返回-1。

waitpid()  函数提供了更多的控制,它允许父进程等待特定的子进程结束。与  wait()  不同, waitpid()  允许指定要等待的子进程 PID,以及一些选项来控制等待的行为。

> pid:为正,表示要等待的子进程的pid,为-1,表示等待任意子进程,为0,表示等待同进程组的所有子进程。

> wstatus:状态信息指针,可以获取子进程的退出状态。

> options:指定等待的行为选项,比如 WNOHANG :如果没有任何已结束的子进程可等待, waitpid()  不会挂起调用进程的执行,而是立即返回。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork");
    }else if (pid == 0){
        printf("Child process[pid:%d]\n", getpid());
        exit(EXIT_SUCCESS);
    }else {
        printf("Parent process[pid:%d]\n", getpid());
        wait(NULL); // 等待子进程结束
    }
    return 0;
}

下面介绍孤儿进程。

至于孤儿进程,顾名思义,没有父进程的子进程就是孤儿进程。也就是说,父进程比子进程先退出了,此时的子进程就变成了孤儿进程。孤儿进程会由1号进程领养,在它退出时读取它的退出信息,避免成为僵尸进程。下面通过一段代码演示孤儿进程被1号进程领养:

 while : ; do ps axj | grep test | grep -v grep;sleep 1; echo "#################"; done

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork");
    }else if (pid == 0){
        printf("Child process[pid:%d]\n", getpid());
        sleep(15);
    }else {
        printf("Parent process[pid:%d]\n", getpid());
        sleep(3);//父进程比子进程先退出
        exit(1);
    }
    return 0;
}

可以看到,子进程的父进程由pid为13185的进程转变为1号进程。

这里做一个小总结:僵尸进程是子进程先退出,父进程未读取它的退出信息而导致该子进程变为僵尸进程。孤儿进程是父进程先退出,子进程还在执行,还在执行的子进程就成为了孤儿进程。这是僵尸进程和孤儿进程的一个区别,但是,它们之间也可能存在着联系,比如一个孤儿进程退出时,1号进程未能及时回收,那么这个孤儿进程就转变为了僵尸进程。

在了解了孤儿进程会被1号进程领养后,解决僵尸进程的方式就不止主动回收子进程一种了,还可以利用双fork()技术,脱离父子关系。请看代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

void do_work() {
    printf("Hello World!\n");
}
int main(int argc, char *argv[]) {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork");
    }else if (pid == 0){
        printf("Child process[pid:%d]\n", getpid());
        if (fork() == 0) {
            if (fork() == 0) {
                do_work();
                exit(0);
            }
            exit(0);
        }
        wait(NULL);
    }else {
        printf("Parent process[pid:%d]\n", getpid());
    }
    return 0;
}

如果对上面代码有疑惑,可以画图帮助理解!

进程优先级

进程优先级(Process Priority) 是操作系统调度器用于决定CPU资源分配顺序的关键数值指标。它定义了进程获取CPU时间片的相对权重,直接影响进程的响应速度和执行频率。

通过ps -l命令可以看到:

PRI:表示进程的优先级。

● 普通进程的PRI范围是100-139。

● 实时进程的PRI范围是0-99,不受nice值影响。

NI:进程的nice值,影响进程的优先级。

● nice值的范围在-20-19之间。

在Linux系统中,PRI(实际调度优先级)和nice值之间存在这样的数学关系:

PRI = 基础值 + NI,这个基础值可能因系统版本不同而不同,可能是120,也可能是80。最后需要明确的一点是:PRI的值越小,优先级越高;反之,优先级就越低。

查看进程优先级和nice值的命令:

第一个:ps -eo pid,pri,ni

● e:显示所有进程(every process)。

● o:自定义输出格式。

第二个:top -p pid(指定监视的进程pid)

进程切换

进程切换也叫做上下文切换,是指操作系统在CPU上停止执行当前进程转而执行另一个进程的过程。导致进程切换的原因有多种,最典型的就是当前进程的时间片耗尽,操作系统把它从CPU上剥离下来,还需要明确一点,上下文切换是有开销的。

指标 典型值 影响
切换开销 1-100微秒 CPU时间损耗
切换频率 100-1000次/秒 系统负载敏感
切换原因占比    
时间片耗尽 60-70% 公平调度
I/O阻塞 20-30% 资源等待
高优先级抢占 5-15% 实时性需求

进程存在的意义

进程存在的意义这个问题,它很基础,但说实话我感觉并不好回答。

看,没骗你们吧~但是,话又说回来,既然学了进程,如果不了解它存在的意义的话,是不是有点不可理喻啊?言归正传:

首先第一点——并发执行,允许单核处理器同时处理多个程序。当然,这里的同时实际上并不是真正意义上的同时,但由于计算机太快,给我们的感觉就是同时执行。

第二点——资源隔离,每个进程都有自己对应的上下文数据,可以确保执行状态独立。

第三点——程序抽象,前面说到跑起来的程序就是进程,仅仅一个“跑”字,程序和进程之间就存在着本质的区别,程序它就是存储在硬盘上的一些指令和数据,它是静态的,而进程不一样,它是动态的。那么,居然说进程是程序的抽象,抽象什么了?就是那一堆指令。用户不需要关心那一条条的指令是如何执行的,进程为程序的执行提供了一个统一的接口,无论是简单的脚本程序还是复杂的多线程应用程序,操作系统都通过进程这个抽象来管理它们。

第四点——状态管理,进程的状态管理是操作系统对进程调度和控制的核心机制,比如当一个进程处于阻塞状态时,操作系统就把它从CPU上切换下来,转而执行就绪的进程,可以提高系统的效率。

第五点——权限控制,比如有些进程只能在用户空间执行,而不能执行需要内核权限的操作,这样做有利于系统的安全。还有用户组这些概念,都是权限控制的意义所在。

第六点——可调度实体,进程是资源分配的基本单位,有明确的生命周期,标准化的接口和独立的运行环境,有助于实现并发执行。

第七点——错误隔离,进程之间通过地址隔离、资源隔离和执行环境隔离,确保一个进程的错误不会影响其他进程或者整个操作系统,这种隔离机制不仅保护了系统的稳定性,还保护了用户数据的安全性和完整性。

结语

在读完本篇博客后,希望提供的内容能对您有所帮助。若仍有疑问或需要进一步探讨,欢迎随时交流。感谢您的关注与支持,期待未来能有更多机会共同学习和成长。

祝一切顺利!


完~

 

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐