漫话linux:进程

#AI模型:追求全能还是专精?#

1.一个运行中的程序,叫做进程

使用ps ajx来查看所有进程

 来看看我的测试程序

[root@VM-4-13-centos ~]# mkdir lessson
[root@VM-4-13-centos ~]# cd lessson
[root@VM-4-13-centos lessson]# ll
total 0
[root@VM-4-13-centos lessson]# touch myprocess.c
[root@VM-4-13-centos lessson]# vim Makefile
[root@VM-4-13-centos lessson]# vim myprocess.c
[root@VM-4-13-centos lessson]# make
gcc -o myprocessmyprocess.c
gcc: fatal error: no input files
compilation terminated.
make: *** [myprocess] Error 4
[root@VM-4-13-centos lessson]# ^C
[root@VM-4-13-centos lessson]# ^C
[root@VM-4-13-centos lessson]# vim Makefile
[root@VM-4-13-centos lessson]# vim myprocess.c
[root@VM-4-13-centos lessson]# vim Makefile
[root@VM-4-13-centos lessson]# make
gcc -o myprocess myprocess.c
[root@VM-4-13-centos lessson]# 

运行一下

grep myprocess来查看进程(或者ps aux)

 这个进程,是由./myprocess产生的

进程至少是一部分在内存中才能被cpu读取,操作系统的代码和数据都在内存中

根据冯诺依曼体系,要运行一个程序,需要先从磁盘中读取数据(二进制文件)到内存中,进入操作系统,代码给运算器,数据给控制器(两个共同构成cpu),在操作系统中,系统可以同时有多个进程,就要进行进程管理,管理的方法是先描述再组织,任何一个进程,在加载成真正的进程时,操作系统要先创建进程的结构体对象--PCB(进程编号,进程状态(运行,休眠等),优先级),根据进程的PCB类型,为该进程创建对应的PCB对象,对进程的管理,就是对于进程PCB构成链表的增删查改

在linux下,task_struct是PCB,是一种内核结构,并在内存中储存着进程的信息,使用双链表来组织,但是PCB不全在双链表里

内容分类:

1.标识符,描述本进程的唯一标识符,用于区别于其他进程

2.状态,表示进程的任务状态,如退出代码,退出信号等

3.优先级:相较于其他进程的优先级

4.程序计数器:程序中即将被执行的下一条命令的地址

5.内存指针:程序代码,进程数据,与其他进程共享内存块的指针

6.上下文数据:进程执行过程中处理器的寄存器中的数据

7.I/O状态信息:显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表

8.记账信息:处理器时间总和时钟数总和,时间限制等

9.其他信息

冷门的查看进程方法:ls /proc,生成该目录也是进程的一部分,所以你也可以在目录里面找到它自己的进程

进程的路径就是进程所在的文件

来写一个测试程序

 在不退出底行的情况下,!man 手册编号 函数名来查询函数的头文件

那么问题来了,直接!man 函数名就能搞定的事情为啥非要加个手册啊(小白偶感)

之前应该是可以查到头文件的,但现在只能在那一栏查到用法,很奇怪,可能更新了 

一点原码(忘了是运行依赖方法了,错了一点)

[root@VM-4-13-centos ~]# mkdir lesson11
[root@VM-4-13-centos ~]# cd lesson11
[root@VM-4-13-centos lesson11]# touch proc.c
[root@VM-4-13-centos lesson11]# vim Makefile
[root@VM-4-13-centos lesson11]# vim proc.c
[root@VM-4-13-centos lesson11]# make
gcc -o proc proc.c
[root@VM-4-13-centos lesson11]# ./proc.c
-bash: ./proc.c: Permission denied
[root@VM-4-13-centos lesson11]# ./lesson11
-bash: ./lesson11: No such file or directory
[root@VM-4-13-centos lesson11]# ./proc
你好李玟璟
你好李玟璟

使用这个命令来显示即将打印的进程的部分属性

ps axj 管道 head -1 

ps axj | grep proc显示proc目录里面的进程,proc的位置可以被PID等替代

一起来 (任何指令都是进程)

终止进程 :kill -9 PID

获取自己的PID:getpid()

 

PPID父进程,使用getppid()来获取

bash进程,每次进入xshell系统会给你重新分配一个进程,用于控制命令行,它的PPID就是当前命令行的进程,当PPID出现问题时,不会影响PID本身,这就是PPID存在的意义,也就是说,你命令行报错了不会改变你的代码,所有进程的PPID都是bash进程

创建子进程

fork函数的作用是创建一个新的进程,如果成功给父进程返回子进程,给子进程返回0,如果失败给子进程返回-1 ,此时系统有两个执行流

由于给父进程返回pid>0,==0则是子进程,可以分类讨论来给两个进程分别运行代码

使用fork()函数,子进程会继承父进程的部分代码,但不会继承数据,继承原则为一父可能有多子,一子只有一父

举个例子:三个人组队打boss,第一个人是父进程,然后使用两个fork来增加两个执行流(一父多子),第二个人穿了个甲,第三个人少拿一把刀,虽然打boss的动作是一样的(继承代码),但是结果可能不同(数据不一样)

进程状态分为三种:

1.就绪状态:已具备运行的一切条件,只要等待cpu的分配就可以运行,通常会加入到就绪队列,等待调度器分配cpu资源

2.运行状态:表示进程正在被CPU执行。处于运行状态的进程正在使用CPU进行计算或其他操作

3.阻塞状态:表示进程因为某些原因暂时无法继续执行,需要等待一些特定条件的解除之后才能继续运行。例如,当进程等待I/O操作完成或者等待某个资源可用时,会转入阻塞状态。进程在阻塞状态时,通常会被移动到阻塞队列中,等待条件的满足

阻塞状态的更多解释:一个进程要运行,不仅仅需要cpu资源,还需要磁盘,网卡,显示器资源等,如果我们申请 CPU 资源无法暂时无法得到满足,这就需要排队的 "运行队列" 。那么如果我们申请其他慢设备的资源呢?是需要排队的(task_struct 在进程排队)

当访问某些资源(磁盘,网卡等),如果该资源暂时没有准备好,或者正在给其他进程提供服务,那么此时:

1. 当前进程要从 runqueue 中逐出。
2. 将当前进程放入对应设备的描述结构体中的waitqueue 

进程状态:看PCB在哪个状态队列

内存不足了,操作系统就会把 该进程的代码和数据置换到磁盘上,进行进程挂起

进程状态转换

#include <stdio.h>
#include <unistd.h>
 
int main() {
    while (1) {
        printf("进程[%d]正在运行...\n", getpid());
        sleep(1); // 模拟阻塞状态
    }
    return 0;
}

 

进程状态用整数表示,这些整数存储在进程的task_struct结构体中。常见的进程状态包括:运行(R)、睡眠(S)、磁盘睡眠(D)、停止(T)、死亡(X)、僵尸(Z)和孤儿进程

查看进程状态

ps aux
ps axj

STAT表示了字段的状态

cpu太快了,print显示器等待的时间在他看来就是在sleep了

深度睡眠状态:磁盘读写时的进程,不可被kill -9杀掉且不可被唤醒

暂停状态:我们调试程序,让程序打断点之后让程序运行起来,程序在打断点处停住的时候是将进程暂停了,所以你在gdb 调试或在 VS 下调试时你会发现程序会停下来,这就是暂停,是进程挂起的一种

gdb下的调试状态

$ gdb process  # 进入gdb调试
(gdb) l        # 查看代码
(gdb) b 9      # 打断点
q + 回车       # 退出

僵尸状态:当一个 Linux 中的进程退出的时候,一般不会直接进入 X  状态(死亡,资源可以立马被回收),而是进入 Z 状态

原因:进程为 Z 状态,就是为了维护退出信息,可以让父进程或者 OS 读取记录的,退出信息会写入 test_struct

代码示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
int main() {
    pid_t id = fork();
    if (id < 0) {
        perror("fork");
        return 1;
    } else if (id == 0) { // 子进程
        printf("子进程[%d]开始运行...\n", getpid());
        sleep(5);
        printf("子进程[%d]退出...\n", getpid());
        exit(0);
    } else { // 父进程
        printf("父进程[%d]正在睡眠...\n", getpid());
        sleep(30); // 父进程延迟回收子进程
    }
    return 0;
}

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

僵尸进程的危害:僵尸进程虽然不再运行,但它们仍然占用系统资源(如进程控制块task_struct)。如果父进程不及时回收子进程,会导致系统资源浪费,甚至内存泄漏

可以通过以下方法避免僵尸进程:父进程及时调用wait()waitpid()回收子进程,使用信号处理机制,在子进程退出时通知父进程进行回收

孤儿进程:父进程先退出了,子的父就变成1 号进程了,相当于被os领养了

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
 
int main(void) {
    pid_t id = fork();
    if (id == 0) {
        // child
        int cnt = 5;
        while (1) {  // 死循环,孩子进程就不退了
            printf("我是子进程,我还剩下 %ds\n", cnt--);
            sleep(1);
        }
        printf("我是子进程,我已经变僵尸了,等待被检测\n");
        exit(0);
    }
    else {
        // father
        int cnt = 3;
        while (cnt) {
            printf("我是父进程,我: %d\n", cnt--);
            sleep(1);
        }
        exit(0);
    }
}

父进程退出,为什么父进程没有变成僵尸?我们怎么没有看到父进程 为Z?

那是因为父进程的父进程是bash ,它会自动回收它的子进程,也就是这里的父进程。这里之所以没有看到父进程变成僵尸,是因为被 bash 回收了, z->x 的状态很快,所以你没看到

那为什么刚才我自己代码中的父进程创建的子进程,父进程没有回收子进程呢?那是因为你的代码压根就没有写回收,所以你的子进程就没有回收

那我们怎么暂停呢,ctrl+c 只能干掉前台进程,所以只能用kill -9

进程优先级:cpu资源分配的先后顺序,优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能

查看系统进程:使用ps -1命令查看系统进程的相关信息,包括UID、PID、PPID、PRI和NI等。PRI代表进程的优先级,NI代表进程的nice值。PRI值越小,进程的优先级越高

修改系统进程:修改进程优先级主要是通过修改nice值实现的,nice值范围为-20至19,数值越小,优先级越高。可以使用nicerenice命令或通过top命令进行修改

$ sudo top
# 在top命令界面按“r”键,输入进程PID和新的nice值

优先级调度原理:开散列哈希存储PCB,使时间复杂度为O(1),然后交换运行队列和等待队列的元素,本质:让pcb嵌入队列,在结构体中调整位置

进程的切换:

1.竞争与独立:由于系统资源有限,所以进程存在竞争性,多进程运行期间,每个进程需要独享资源,不会互相干扰,这体现了进程的独立性

2.并行与并发:并行是指多个进程在多个CPU下同时运行,并发是指多个进程在单个CPU下通过进程切换的方式,在一段时间内推进多个进程的执行

进程的抢占:抢占式内核允许优先级高的进程抢占CPU资源。当一个低优先级进程正在运行时,如果来了一个高优先级的进程,调度器会将低优先级进程剥离CPU,切换到高优先级进程

实现切换:当进程在被执行的过程中,一定会存在大量的临时数据,会暂存在 CPU 内的寄存器中

进程在运行中产生的各种寄存器数据,叫进程的硬件上下文数据,当进程被剥离:需要保存上下文数据,当进程恢复时:需要将曾经保存的上下文数据恢复到寄存器中,调度器根据保存的进程上下文,就可以实现进程切换

环境变量:环境变量是操作系统中用来指定运行环境的一些参数。它们通常具有全局特性,可以影响系统和应用程序的行为

为什么我们的代码运行要带路径,而系统的指令不用带路径?

系统中是存在相关的 环境变量,保存了程序的搜索路径的! 

为什么我们的代码运行要带路径,而系统的指令不用带?其本质是由环境变量 PATH 引起的

常见的环境变量:

我们可以通过 env 指令查看环境变量

PATH: 指定命令的搜索路径

 如何查看环境变量的内容?我们可以使用 echo  PATH去显示

环境变量 PATH 中会承载多种路径,中间用冒号 ( : ) 作为分隔符

HOME: 指定用户的主工作目录

SOME: 指定用户的主工作目录

设置环境变量:

可以使用echo命令查看环境变量的路径,使用export命令($用法)改变环境变量的路径,注意shell命令的$修饰避免其命令指向一个叫PATH的字符串,而是环境变量

echo $PATH
export PATH=$PATH:/my/custom/path

为了使自定义的可执行程序不带路径也能执行,可以将程序所在路径(你的文件路径)加入到PATH环境变量中

export PATH=$PATH:/path/to/your/program

创建与删除

export MYENV="hello world"
unset MYENV

进程地址空间

验证父子进程空间的一致性

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
    pid_t id = fork();
    if(id < 0){
        perror("fork");
        return 0;
    }
    else if(id == 0){ //child
        printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
    }else{ //parent
        printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
    }
    sleep(1);
    return 0;
}

结论是父子进程具有相同的地址空间

验证变量修改后父子进程之间的差异

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
    pid_t id = fork();
    if(id < 0){
        perror("fork");
        return 0;
    }
    else if(id == 0){ //child
        g_val=100;
        printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
    }else{ //parent
        sleep(3);
        printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
    }
    sleep(1);
    return 0;
}

结论是父子进程具有相同的地址空间,但是值不一样

实验结论:上述实验表明,父子进程的变量地址相同但内容不同,说明地址为虚拟地址,且父子进程有各自独立的物理地址映射。这验证了虚拟地址的概念,即我们在C/C++中看到的地址是虚拟地址,由操作系统负责将其转化为物理地址

进程地址空间:程序地址空间实际上是进程地址空间的子集,是系统级的概念。进程地址空间通过虚拟地址映射实现内存独立性,确保进程间互不干扰

进程地址空间验证

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
 
int un_g_val;
int g_val = 100;
 
int main(int argc, char* argv[], char* env[])
{
    printf("code addr            : %p\n", main);
    printf("init global addr     : %p\n", &g_val);
    printf("uninit global addr   : %p\n", &un_g_val);
    
    char* m1 = (char*)malloc(100);
    printf("heap addr            : %p\n", m1);
    printf("stack addr           : %p\n", &m1);
    
    int i = 0;
    for (i = 0; i < argc; i++) {
        printf("argv addr        : %p\n", argv[i]);   
    }
    
    for (i = 0; env[i]; i++) {
        printf("env addr         : %p\n", env[i]);
    }
}

运行结果为地址整体依次增大,堆区向地址增大方向增长,栈区向地址减少方向增长,验证了堆和栈的挤压式增长方向

验证静态局部变量

静态修饰的局部变量,编译的时候已经被编译到全局数据区,这一点可以通过以下代码验证

#include <stdio.h>
#include <stdlib.h>
void func() {
    static int static_var = 10;
    printf("static_var addr: %p\n", &static_var);
}
int main() {
    func();
    return 0;
}

这也说明了这些变量的地址在全局数据区,而不是局部栈区

区域与页表

进程地址空间通过 mm_struct 结构体来管理各个区域。每个区域的定义如下:

struct mm_struct {
    long code_start;
    long code_end;
    
    long init_start;
    long init_end;
    
    long uninit_start;
    long uninit_end;
    
    long heap_start;
    long heap_end;
    
    long stack_start;
    long stack_end;
    ...
}

用一个start 和end 就可以表示区域,每个区域都有一个 start 和 end,它们之间就有了地址,地址我们称之为虚拟地址,然后这些虚拟地址经过页表,就能映射到内存中了

父子进程全局变量共享与写时拷贝

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
 
int g_val = 100;
int main(void) 
{
    pid_t id = fork();
    if (id == 0) {
        // child
        int flag = 0;
        while (1) {
            printf("child: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
            sleep(1);
            flag++;
            if (flag == 5) {
                g_val = 200;
                printf("child modified g_val\n");
            }
        }
    }
    else {
        // father
        while (1) {
            printf("parent: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
            sleep(2);
        }
    }
}

在父子进程中,虚拟地址相同但值不同,验证了写时拷贝机制

写时拷贝机制:写时拷贝是指当父子进程有一方尝试修改变量时,操作系统会为修改方分配新的物理内存并拷贝数据,以确保独立性

fork()的两个返回值:

pid_t id 是属于父进程的栈空间中定义的

fork 内部 return 会被执行两次,return 的本质就是通过寄存器将返回值写入到接收返回值的变量中。当我们的 id = fork() 时,谁先返回,谁就要发生 写时拷贝。所以,同一个变量会有不同的返回值,本质是因为大家的虚拟地址是一样的,但大家的物理地址是不一样的

进程地址空间的意义:虚拟地址空间通过软硬结合层,保护内存并简化进程和程序的设计和实现,确保进程的独立性和安全性

进程地址空间区域划分

意义:

1.让进程以统一视角看待内存

2.访问内存加了一个转换的过程,可以保护物理空间

3.因为地址空间和页表,与内存管理模块进行解耦合

os 对大文件的分批加载是怎么实现的呢?

采用惰性加载的方式

存在 缺页中断 ,重新申请 填写页表

缺页中断:

当一个进程访问虚拟内存中的某一页时,操作系统会先检查该页是否当前已经被加载到物理内存中。如果这一页已经在物理内存中,CPU就可以直接访问它。但是,如果这一页并没有在物理内存中,就会发生缺页中断。

当发生缺页中断时,CPU会暂停当前的执行,并将控制权交给操作系统内核。操作系统内核会首先查找页表,寻找到相关的页面对应的磁盘地址。然后,操作系统会将磁盘上的内容读取到空闲的物理内存页中。

一旦内容被加载到物理内存中,操作系统会更新页表,将该页面的映射关系添加到页表中,然后将控制权交还给进程并重新开始执行。这样,进程可以继续访问所需的内存页面。

整个过程用于解决虚拟内存中的页面不在物理内存中的问题,使得系统看起来好像比它实际拥有的更多内存一样,从而使得多个进程能够共享有限的内存资源,提高内存利用率和系统的整体性能。

就达到分批加载的效果了

进程创建fork:

1.将给子进程分配新的内存块和内核数据结构(形成了新的页表映射)

2.将父进程部分数据结构内容拷贝至子进程

3.添加子进程到系统进程列表当中

4.fork 返回,开始调度器调度

这样就可以回答之前返回两个值?

发生了写实拷贝,形成了两个物理空间块

创建了一个子进程

一般来说子进程创建之后,会共享父进程的所有代码

eip 叫做 程序计数器,用来保存当前正在执行的指令的下一条指令。eip 程序计数器会拷贝给子进程,子进程便从该 eip 所指向的代码处开始执行。

我们再来重新思考一下 fork 之后操作系统会做什么:

" 进程 = 进程的数据结构 + 进程的代码和数据 "

创建子进程的内核数据结构:

(struct task_struct + struct mm_struct + 页表)+ 代码继承父进程,数据以写时拷贝的方式来进行共享或者独立

代码共享,写实拷贝确保了进程的独立性

写实拷贝:

当任意一方试图写入,就会按照写时拷贝的方式各自拷贝一份副本出来。写时拷贝本身由操作系统的内存管理模块完成的

选择暂时先不给你,等你什么时候要用什么时候再给。这就变相的提高了内存的使用情况

fork:

fork 之后利用 if-else 进行分流, 让父子执行不同的代码块。我们做网络写服务器的时候会经常采用这样的编码方式,例如父进程等待客户端请求,生成子进程来处理请求。

继承大纲后又有所区别

fork 肯定不是永远都成功的,fork 也是有可能调用失败的

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
 
int main(void)
{
    for (;;) {//写了一个死循环
        pid_t id = fork();
        if (id < 0) {
            printf("子进程创建失败!\n");
            break;
        }
        if (id == 0) {
            printf("I am a child... %d\n", getpid());
            sleep(2); // 给它活2秒后 exit
            exit(0);  // 成功就退出
        }
    }
 
    return 0;
}

创建失败,死循环创建太多进程了

进程终止exit

为什么要使用return?

代码运行完的结果只有三种:

1.运行完毕,结果正确

2.运行完毕,结果不正确

3.程序异常终止

在c语言时,我们就知道strerror函数如果检测到规定程序块出现问题,会返回错误码,一般从0开始,0为正确,其它为各种错误的原因,linux也具有相似的机制

在超长程序进行调试时,无脑return 0明显不适用

失败的非零值是可以自定义的,我们可以看看系统对于不同数字默认的 错误码 是什么含义。C 语言当中有个的 string.h 中有一个 strerror 接口

#define EPERM 1 /* 操作不被允许 */  
#define ENOENT 2 /* 没有这样的文件或目录 */  
#define ESRCH 3 /* 没有这样的进程 */  
#define EINTR 4 /* 系统调用被中断 */  
#define EIO 5 /* I/O 错误 */  
#define ENXIO 6 /* 没有这样的设备或地址 */  
#define E2BIG 7 /* 参数列表过长 */  
#define ENOEXEC 8 /* 执行格式错误 */  
#define EBADF 9 /* 错误的文件描述符 */

在linux命令中,$?表示获取上一个进程的状态,这个进程状态被存储在一个环境变量中,使用上文所提到的echo $?来获取上一个进程的状态,获取错误码,就可以先入为主的判断进程失败的原因

还有一种情况,代码异常报错,甚至没有运行到return的部分已经崩掉了,此时要发信号

比如返回错误码8和11,kill -8 出现野指针 -11 段错误

终止进程的做法:

#include<stdlib.h>

exit 的退出码 12

exit 在任意地方被调用,都表示调用进程直接退出

return 只表示当前函数的返回

#include <stdio.h>
#include <stdlib.h>
 
void func() {
    printf("hello func\n");
    exit(111);
}
 
int main(void)
{
    func();    
 
    return 10;
}

这个程序不会运行到return,到了exit就返回了错误码

注意,只有在 main 函数调 return 才叫做 进程退出,其他函数调 return 叫做 函数返回。

_exit 和 exit

exit 会清理缓冲区,关闭流等操作,而 _exit 什么都不干,直接终止

  1 #include<stdio.h>  
  2 #include<unistd.h>                                                                                                  
  3 #include<stdlib.h>                     
  4 void func() {                          
  5     printf("hello exit");              
  6     exit(0);                           
  7 }                                      
  8                                        
  9 int main(void)                         
 10 {                                      
 11     func();                            
 12     printf("hello _exit");             
 13     _exit(0);                          
 14 }         

exit打印出来了,_exit打印不出来,因为exit首先把缓冲区的打印数据给清理了,而_exit直接终止了

 缓冲区在这个背景下的概念是指一块预分配的内存,用来存储和重复利用特定类型的数据结构。这种做法可以显著减少频繁的内存分配和释放所带来的开销,提高系统性能,这也是 slab 分配器背后的核心思想

slab 分配器:根据合适的内存拿,不要就放回去

我们 printf 一定是把数据写入缓冲区中,合适的时候,在进行刷新

这个缓冲区绝对不在哪里?

绝对不在内核里,在用户空间,要不然一定会被刷新

进程等待:是指父进程暂停自己的执行,直到某个特定的子进程结束或发生某些特定的事件

原因:

1.僵尸进程无法杀死,存在内存泄漏

2.可以选择性了解分配给子进程的任务的完成情况

可以使用wait函数去实现进程等待

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
 
int main (
    void
    ) 
{
    pid_t id = fork();
    if (id == 0) {
        // child
        while (1) {
            printf("我是子进程,我正在运行... Pid: %d\n", getpid());
            sleep(1);
        }
    }
    else {
        printf("我是父进程: pid: %d,我将耐心地等待子进程!\n", getpid());
        sleep(20);   // 为了便于观察,我们让父进程休眠20s
 
        // 苏醒后,父进程执行 wait,耐心地等待子进程
        pid_t ret = wait(NULL);  // 暂且将status参数设置为NULL
        if (ret < 0) {
            printf("等待失败!\n");
        }
        else {
            printf("等待成功!\n");   // 此时 Z → X
        }
 
        sleep(20);  // 子进程退出后,再让父进程存在一段时间
    }
}

 

等到20秒后只剩父进程,子进程没了

为什么是交替型,不是一个执行完?//了解循环的细节

在fork创建子进程后,父进程会首先执行并打印这条信息

父进程在打印一次消息后休息20秒,等待观察子进程的活动,不进行其他操作

通过这种方式,父进程和子进程能够并发地执行各自的任务,并且我们通过延迟父进程的退出能观察到子进程的连续活动

 执行流程如下:

1.父进程打印:yamlCopy code
我是父进程: pid: 1234,我将耐心地等待子进程!

2.子进程每秒打印一次进程:yamlCopy code
我是子进程,我正在运行... Pid: 5678

此循环一直执行直到子进程终止

3.通过终端输入kill 5678终止子进程

4.父进程20秒后苏醒并等待子进程结束后,打印

textCopy code
等待成功!
之后,父进程再休眠20秒并继续存在一段时间后退出

如果去掉父进程中的wait()调用,程序的行为将发生显著变化。以下是分析:

  1. 子进程继续运行
    子进程将继续执行其无限循环,每隔一秒打印一条消息,表明它是子进程并显示其PID。由于wait()被移除,父进程不会等待子进程结束,因此子进程将独立地继续运行,直到被外部信号(如用户中断)或系统资源限制(如内存不足)终止。

  2. 父进程继续执行
    父进程在打印出“我是父进程: pid: [父进程PID],我将耐心地等待子进程!”和休眠20秒后,将不会等待子进程。相反,它将直接跳过原本wait()调用的位置,继续执行后面的代码(如果有的话)。在这个特定的例子中,父进程在休眠20秒后,将执行sleep(20);再次休眠20秒,然后程序结束。

  3. 僵尸进程
    由于父进程没有等待子进程结束,当子进程结束时(尽管在这个例子中它不会自然结束,但假设它因为某种原因结束了),子进程将变成一个僵尸进程。僵尸进程是已经终止但父进程尚未通过wait()或类似机制回收其资源的进程。这些进程仍然占用系统资源(主要是进程表中的一个条目),直到父进程调用wait()或父进程自身结束(此时子进程将由init进程接管并清理)。

  4. 资源泄露
    如果父进程创建了多个子进程并且没有适当地等待它们结束,那么系统上将积累大量的僵尸进程,这可能导致资源泄露。在极端情况下,这可能会耗尽系统资源,影响系统的稳定性和性能。

  5. 程序结束
    在这个特定的例子中,父进程在打印完“等待成功!”(实际上,由于移除了wait(),这条消息将不会打印)并再次休眠20秒后结束。然而,子进程将继续运行,直到被外部干预。

  6. 建议
    在实际应用中,如果父进程创建了子进程,并且父进程需要知道子进程的结束状态或避免僵尸进程,那么父进程应该使用wait()waitpid()或其他相关函数来等待子进程结束。如果父进程不关心子进程的结束状态,但仍然希望避免僵尸进程,可以考虑在父进程中设置对SIGCHLD信号的忽略处理(通过signal(SIGCHLD, SIG_IGN);),这样当子进程结束时,内核会自动清理其资源,而不会留下僵尸进程。

waitpid:刚才讲的 wait 并不是主角,因为其功能比较简单,在进程等待时用的更多的是 waitpid

waitpid 可以把 wait 完全包含,wait 是 waitpid 的一个子功能,waitpid(pid,status,options)

参数为pid,要等待的子进程的进程ID。根据传入的值可以指定等待任何子进程、特定进程ID的子进程、任何同一进程组的子进程或者任何同一会话的子进程

如果pid大于0,waitpid将等待指定进程ID的子进程结束

如果pid等于-1,waitpid将等待任意子进程结束,等于wait程序

status:一个指向整型的指针,是一个输出型参数,它将用于存储子进程的终止状态

options:用于指定等待行为的附加选项,传入0表示以默认行为等待子进程

如果waitpid成功,就会返回已终止子进程的进程ID

如果错误则返回-1,并使用errno变量来表示具体错误原因

Z状态,其本质上就是将自己的 task_struct 维护起来(代码可以释放,但是 task_struct 必须维护)。所谓的 wait/waitpid 的退出信息,实际上就是从子进程的 task_struct 中拿出来的,即 从子进程的 task_struct 中拿出子进程退出的退出码,拷贝到父进程中

status是一个 输出型参数 (即通过调用该函数,从函数内部拿出来特定的数据)。整数的低 16 位,其中又可以分为 最低八位 和 次低八位

 

次低八位,拿子进程退出码 

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
 
int main (
    void
    ) 
{
    pid_t id = fork();
    if (id == 0) {
        int cnt = 5;   // 循环5次
        // child
        while (1) {
            // 五秒之内运行状态
            printf("我是子进程,我正在运行... Pid: %d\n", getpid());
            sleep(1);
 
            // 五秒之后子进程终止
            cnt--;
            if (cnt == 0) {
                break; 
            }
        }
 
        exit(233);   // 方便辨识,退出码我们设置为233,这是我们的预期结果
    }
    else {
        printf("我是父进程: pid: %d,我将耐心地等待子进程!\n", getpid());
        
        // ***** 使用waitpid进行进程等待
        int status = 0;  // 接收 waitpid 的 status 参数
 
        pid_t ret = waitpid(id, &status, 0);
        if (ret > 0) {   // 等待成功
            printf (
                "等待成功,ret: %d, 我所等待的子进程退出码: %d\n", 
                ret,
                (status>>8)&0xFF
            );
        }
 
    }
}

 status 并不是整体使用的,而是区域性使用的,我们要取其次低八位。我们可以用位操作来完成,将 status右移八位再按位与上 0XFF,即 (status>>8)&0xFF ,就可以提取到 status 的次低八位了

waitpid 经过系统调用,来读取子进程的pcb(eg. task_st…),这是因为操作系统不相信任何人,父进程用户无法直接读取子进程的 pcb ,要通过系统调用的接口

core dump(核心转储)

它是操作系统在进程收到某些信号而终止运行时,将此时进程地址空间的内容以及有关进程状态的其他信息写出的一个磁盘文件。目前只需要知道,该信息是用于调试的

最低七位: 提取子进程的退出信号

刚才我们讲的 wait/waitpid 和次低八位的时侯,都是关于进程(exit) 的正常退出

现在看一下异常退出,写一个死循环,子进程一直跑,父进程一直等

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
 
int main (
    void
    ) 
{
    pid_t id = fork();
    if (id == 0) {
        // 子进程一直不退出,父进程会一直等待。
        // child
        while (1) {
            printf("我是子进程,我正在运行... Pid: %d\n", getpid());
            sleep(1);
        }
 
        exit(13);
    }
    else {
        printf("我是父进程: pid: %d,我将耐心地等待子进程!\n", getpid());
        
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if (ret > 0) {   // 等待成功
            printf(
                "等待成功,ret: %d, 我所等待的子进程退出码: %d\n, 退出信号是: %d", 
                ret, (status>>8)&0xFF, 
                status&0x7F
            );
        }
 
    }
}

现在我们直接 while(1) 死循环让子进程往死里跑,此时父进程由于调用了 waitpid,就会一直等待子进程,父进程就会持续阻塞。

父进程看到子进程kill了,终于可以不用等了,可以给子进程收尸了

 可以发现,使用-9号信号kill掉进程时,进程的退出信号就是9,然而当进程由于信号异常终止时,此时进程退出码是无意义的!

所以进程的等待可以理解为是 父进程在等给子进程退出记录

非阻塞轮询

options->阻塞方式

waitpid(pid,&status,WNOHANG)

WNOHANG就是wait no hang,hang也就是悬挂,也就是非阻塞,等待子进程死亡,若父进程执行到waitpid时,子进程还没退出,则函数返回0后接着运行下面的代码,若执行到waitpid后子进程已经退出则返回退出子进程的pid

假如要期末考试了,我想找小张去自习室帮我复习,小张自己也在复习,我打电话问他什么时候复习完了,可以出门,情况解释如下

WNOHANG 夯住,非阻塞+循环:我给小张间隔打电话寻求帮忙,自己干等在楼下

阻塞式调用:打电话一直不挂

非阻塞轮询+自己的事情(最高效):间隔打电话,自己也在复习

返回值 :ret_pid=0(_pid)–所等待的条件还没有就绪,>0成功返回退出码,<0失败

代码

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
    pid_t id = fork();
    if(id<0)
    {
        perror("fork");
        exit(1);
    }

    if(id==0)//子进程代码
    {
        int count = 5;
        while(count)
        {
            printf("[%d]我是子进程,我的pid是: %d\n",count,getpid());
            sleep(1);
            count--;
        }
        exit(55);//子进程执行完代码后退出
    }
    //父进程代码
    while(1)//循环访问子进程退出情况
    {
        int wait = waitpid(id,NULL,WNOHANG);
        if(wait>0)//子进程退出成功
        {
            printf("子进程退出成功,子进程pid: %d\n",wait);
            break;
        }
        else if(wait==0)//子进程还没退出,父进程干自己的事情
        {
            //此处简单模拟父进程干的事情
            printf("我是父进程,我现在要干一些别的事情\n");
        }
        else //等待子进程退出失败
        {
            perror("waitpid");
            exit(1);
        }
        sleep(1);
    }
    return 0;
}

 子进程每执行一次父进程就可以干一次自己的事情

进程退出的宏:为了增加可读性,定义了接口宏,来查找退出码WEXITSTATUS和WIFEXITED,在判断中,我们一般先关注退出信号,异常了再关注退出码

WEXITSTATUS 宏用于查看进程的退出码,若非 0,提取子进程退出码:WEXITSTATUS(status)
WIFEXITED 宏用于查看进程是否正常退出,如果是正常终止的子进程返回状态,则为真:WIFEXITED(status)

waitpid的意义:1.返回用于记录子进程的数据结构2.Z->X

  • 8
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值