吃透 Linux 进程:从基础概念到实战,一篇打通完整脉络
目录

一、冯诺依曼体系结构
我们常见的计算机(笔记本)和不常见的计算机(服务器)大部分都遵守冯诺依曼体系。

我们首先要搞清楚外设分为输入设备和输出设备
输入设备:键盘,鼠标,话筒,摄像头,网卡,磁盘......
输出设备:显示器,磁盘,网卡,打印机......
其中网卡和磁盘等既是输入又是输出,中央处理器(CPU):含有运算器和控制器等。
不考虑缓存情况,这里的CPU能且只能对内存进行读写,在数据层面不能访问外设(输入或输出设备),外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取,一句话总结,CPU不和外设打交道,所有设备都只能直接和内存打打交道。
理解:计算机为什么要有内存这要逻辑?

理解数据流动过程:
从你打开窗口,开始给他发消息(文件),到他的到消息(文件)之后的数据流动过程

注意:
- 现代 CPU 会集成多级缓存(
L1、L2、L3),但缓存本质是内存的 “加速延伸”,核心数据交互仍遵循冯诺依曼规则; - 存储层次关系(从快到慢、成本从高到低):
CPU 寄存器 → 高速缓存 → 内存 → 本地磁盘 → 远程存储。

二、操作系统
2.1概念
一个基本的程序集合称为操作系统(OS),操作系统是一款进行软硬件管理的软件。
操作系统包括:
内核(进程管理,内存管理,文件管理,驱动管理)
其他程序(例如函数库,shell程序等等)

我们之前常说安卓是Linux操作系统是因为它使用了Linux内核
2.2设计OS的目的
对上,为用户程序(应用程序)提供⼀个良好的执行环境
对下,与硬件交互,管理所有的软硬件资源

注意:
- 软硬件体系结构是层状结构,本质是在软件工程上体现高内聚低耦合,高内聚是把相同功能相同逻辑的代码放在同一层内部,低耦合是层和层之间只使用一些接口的方式进行调用,为了后期的维护和修改。
- 访问操作系统必须使用系统调用,其实就是函数,只不过是系统提供的。
- 我们的程序只要你判断出它访问了硬件,他就必须贯穿整个软硬件体系结构,比如printf的本质是把数据写到了硬件(显示屏),但不可能是我们绕过操作系统直接打印
- 库在底层封装了系统调用
2.3核心功能
在整个计算机软硬件架构中,操作系统的定位是:⼀款纯正的“搞管理”的软件
2.4理解管理

OS 的管理逻辑和现实场景(如学校校长管理学生)完全一致。核心分为两步,这也是Linux内核的设计思路:
- 描述被管理对象:用结构体(struct)记录资源信息
- 组织被管理对象:用高效数据结构(链表,红黑树等)组织结构体
- 总结:先描述,再组织(下面的图要结合之前的图一起理解)

2.5系统调用和库函数概念
在开发角度,操作系统对外会表现为⼀个整体,但是会暴露自己的部分接口,供上层开发使用, 这部分由操作系统提供的接口,叫做系统调用。
系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部 分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行⼆次开 发。
一句话说明:
• 系统调用:内核提供,能碰硬件
• 库函数:用户态代码,不能碰硬件
• 关系:很多库函数内部封装了系统调用,让你用起来更方便

三、进程
3.1基本概念与基本操作
3.1.1前提了解
- 程序:硬盘上的代码文件(.exe、.out、.elf 等),静态的,躺平不动。
- 进程:运行起来的程序,是动态的,占用内存、被 CPU 执行。
-
OS 是进程的管理者,负责:
1. 把程序从硬盘加载到内存,创建进程
2. 给进程分配内存、CPU 时间
3. 调度:让多个进程轮流使用 CPU(并发/并行)
4. 提供系统调用、文件、网络、硬件等服务
5. 进程结束时回收资源
3.1.2基本概念
课本概念:程序的⼀个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体。
当前:进程=内核数据结构(task_struct)+自己的程序代码和数据

PCB:
进程信息被放在⼀个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
课本上称之为PCB(process control block), Linux 操作系统下的 PCB 是: task_struct
task_struct :
在 Linux 中描述进程的结构体叫做 task_struct 。
task_struct 是 Linux 内核的⼀种数据结构类型,它会被装载到RAM(内存)里并且包含着进程的信息。
3.1.3task_struct
内容分类:
标识符:描述本进程的唯一标识符,用来区别其他进程。
进程状态:任务状态,退出代码,退出信号等。
程序计时器(PC):程序中即将被执行的下⼀条指令的地址。
内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
优先级:相对于其他进程的优先级。
上下文数据:进程执行时处理器的寄存器中的数据。
I/O 状态信息:包括显示的 I/O 请求,分配给进程的 I/O 设备和被进程使用的文件列表。
记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他资源:比如进程占用的 CPU 时间,打开的文件描述符等。
更加具体的信息后面还会再慢慢讲到的,这里大家先理解即可。
3.1.4查看进程
1.进程的信息可以通过 /proc 系统文件夹查看
2.进程信息也可以使用top和ps这些用户级工具来获取


3.1.5通过系统调用获取进程标识符

3.1.6通过系统调用创建进程-fork初识
理解下面的图文:


3.2进程状态
理解下面的图文:

3.2.1Linux内核源码
LInux内核中struct list_head的设计结构:

为什么要反过来这么做?
所有设计决策,都是针对内核场景的刚需,解决传统链表的致命缺陷:
1. 解决C语言无泛型的痛点,实现一套接口适配所有类型
C语言没有C++/Java的泛型机制,传统链表无法做到通用——每新增一种数据类型,就要重写一套增删改查代码,内核里有上百种数据结构都要用链表,会产生海量冗余代码,同时带来无数重复的bug。
而list_head的设计,让所有链表操作都只针对通用的list_head节点,内核只需要实现一套标准化接口(list_add/list_del/list_for_each等),不管你把它嵌到什么数据结构里,都能直接复用这套接口,彻底消除冗余。
2. 支持一个数据结构同时加入多个独立链表,完美适配内核复杂场景
这是内核最刚需的能力,传统链表完全做不到。内核里的核心对象(比如进程描述符task_struct),需要同时被多个维度管理:
• 要链入全局进程链表,遍历所有进程;
• 要链入对应CPU的就绪调度队列;
• 要链入等待某个事件的阻塞等待队列;
• 要链入兄弟进程链表,管理父子进程关系。
用list_head,只需要在task_struct里加多个独立的list_head成员,就能把同一个进程对象同时加入多个链表,所有链表都复用同一套操作接口。而传统链表要实现这个,就得给每个链表加一组prev/next,再写多套完全重复的操作代码,维护成本极高。
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。⼀个进程可以有几个状 态(在Linux内核⾥,进程有时候也叫做任务)。
源码:
/*---------------------------------------------------
*The task state array is a strange "bitmap" of
*reasons to sleep. Thus "running" is zero, and
*you can test for combinations of others with
*simple bit tests.
-----------------------------------------------------*/
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信号让进程继续运行。
X死亡状态(dead):这个状态只是⼀个返回状态,你不会在任务列表里看到这个状态。
3.2.2进程状态查看
查看R:末尾的&是Linux shell的后台运行指令,作用是把程序放到后台执行,不占用终端的输入权限,所以你能看到对应进程的STAT是S(无+号),而前台运行的进程状态是S+/R+。

查看S:可中断休眠,浅睡眠

查看T:

查看D:不可中断休眠,深度睡眠
进程正在内核态等待硬件IO完成,为了保护硬件交互过程的完整性,因为如果这个过程允许被信号打断,就可能出现“硬件还在传输数据,进程已经被终止”的情况,导致内核驱动与硬件状态不一致,甚至出现数据损坏、内核崩溃。所以进程不响应任何异步信号,只能等待IO完成后由内核主动唤醒。

查看Z(僵尸进程):
僵死状态(Zombies)是⼀个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。0
僵死进程会以终止状态保持在进程表中,并且会⼀直在等待父进程读取退出状态代码。 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
如果父进程一直不管,不回收,不获取子进程的提出信息,那么Z会一直存在。这就是内存泄漏!!!
注意:进程退出了,内存泄漏问题自动结束了。常驻内存的进程(一旦启动不退出),如果有内存泄漏问题最麻烦。

孤儿进程:
父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?父进程先退出,子进程就称之为“孤儿进程”。孤儿进程被1号init/systemd进程领养,当然要有init/systemd进程回收喽。
为什么要被领养???
如果不被领养就会进入僵尸,造成内存泄漏


3.3进程优先级
3.3.1基本概念
cpu资源分配的先后顺序,就是指进程的优先权(priority)。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性 能。还可以把进程运行到指定的CPU上,这样⼀来,把不重要的进程安排到某个CPU,可以大改善系统整体性能。
优先级也是一种数字值越低优先级越高,基于时间片的分时操作系统,考虑公平性所以优先级可能变化但变化幅度不太大。
3.3.2查看系统进程
了解几个重要信息:
UID:代表执行者的身份
PID:代表这个进程的代号
PPID:代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI:代表这个进程可被执行的优先级,其值越小越早被执行
NI:代表这个进程的nice值

3.3.3查看进程优先级的命令
用top命令更改已存在进程的nice:


还有其他更改方法可以自行在网络上搜索。
优先级的极值问题:

3.3.4补充概念-竞争、独立、并行、并发
竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为 了高效完成任务,更合理竞争相关资源,便具有了优先级
独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行
并发:多个进程在⼀个CPU下采用进程切换的方式,在⼀段时间之内,让多个进程都得以推进,称 之为并发
3.4进程切换
我们先理解下面几个问题:
死循环进程如何运行?
- 一旦一个进程占有CPU不会把自己的代码跑完,只会跑一个时间切片的(一段时间)
- 死循环进程不会卡死系统,不会一直占有CPU(我们在使用一些软件时,即使这个软件卡死了我们还可以打开其他软件使用并找机会关闭这个程序,说明CPU并不是一直在执行这个程序)
时间片:当代计算机都是分时操作系统,没有进程都有它合适的时间片(其实就是⼀个计数器)。时间片到达,进程就被操作系统从CPU中剥离下来。
进程切换(上下文切换)是指 CPU 从一个进程切换到另一个进程执行的过程,是 Linux 实现 “单 CPU 并发多任务” 的核心机制。
CPU上下文切换:其实际含义是任务切换,或者 CPU 寄存器切换。当多任务内核决定运行另外的任务时,它保存正在运行任务的当前状态,也就是 CPU 寄存器中的全部内容。这些内容被保存在任务自己的堆栈中,入栈工作完成后就把下一个将要运行的任务的当前状况从该任务的栈中重新装入 CPU 寄存器,并开始下一个任务的运行,这一过程就是 context switch。

参考Linux内科0.11代码

3.5Linux2.6内核进程O(1)调度队列
过期队列和活动队列结构⼀模⼀样, 过期队列上放置的进程,都是时间片耗尽的进程。当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算。
active指针永远指向活动队列,expired指针永远指向过期队列。可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时⼀直都存在的。在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了⼀批新的活动进程!

四、命令行参数和环境变量
4.1命令行参数
命令行参数是程序运行时通过终端传入的参数,比如ls -l /home中,-l和/home都是ls命令的命令行参数。它让程序无需修改代码,就能根据外部输入调整行为。
其实在C语言中命令行参数通过main函数的参数接收
argc=4:包含程序名./myproc,实际传入的参数是 3 个;argv[0]固定为程序名,argv[1]~argv[argc-1]是用户传入的参数。

我们可以把使用的命令理解成一个个main函数,通过类似这种方法实现命令行及参数
4.2环境变量基本概念
环境变量(environment variables)⼀般是指在操作系统中用来指定操作系统运行环境的⼀些参数,如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪 里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
4.3常见环境变量
PATH:指定命令的搜索路径
HOME:指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
SHELL:当前Shell,它的值通常是/bin/bash
USER:当前登录用户名
LD_LIBRARY_PATH:动态库的搜索路径
4.4查看环境变量方法


4.5获取环境变量的方法
和环境变量相关的命令:
- echo:显示某个环境变量值
- export:设置⼀个新的环境变量
- env:显示所有环境变量
- unset:清除环境变量
- set:显示本地定义的shell变量和环境变量

这个本地变量不会被子进程继承只在bash内部使用。

export命令叫做内建命令(built-in command),它不需要创建子进程而是让bash自己执行,由bash自己调用函数或者系统调用完成。
五、程序地址空间
5.1研究平台
kernel 2.6.32
32位平台
5.2程序地址空间回顾

注意:程序地址空间并不是物理意义的内存而是进程地址空间(虚拟地址空间)
5.3虚拟地址

OS必须负责将 虚拟地址 转化成 物理地址
5.4进程地址空间
总结:
一个进程,一个虚拟地址空间
一个进程,一套页表
页表的用来做虚拟地址和物理地址映射的


5.5虚拟内存管理
虚拟地址空间就是结构体变量,里面是进行区域划分的变量


为什么要有虚拟地址空间:
- 程序在物理内存中是乱序的,虚拟地址空间的存在可以将地址从无序变有序。
- 在地址转换的过程中也可以对你的地址和操作进行合法性的判断保护,进而保护物理内存。
- 虚拟地址空间还可以让进程管理和内存管理一定程度解耦合。

注意一些细节问题:
我们可以不加载代码,只有task_struct,mm_struct,页表
创建进程是先有task_struct,mm_struct等再加载代码和数据
我们现在理解挂起是内存空间严重不足,保留虚拟地址部分,将内存部分清除移动到磁盘中。
我们还要思考一个问题:那既然每⼀个进程都会有自己独立的 mm_struct ,操作系统肯定是要将这么多进程的 mm_struct 组织起来的
- 当虚拟区较少时采取单链表,由mmap指针指向这个链表;
- 当虚拟区间多时采取红黑树进行管理,由mm_rb指向这棵树。
linux内核使用 vm_area_struct 结构来表示⼀个独立的虚拟内存区域(VMA),由于每个不同质的虚 拟内存区域功能和内部机制都不同,因此⼀个进程使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。上⾯提到的两种组织方式使用的就是vm_area_struct结构来连接各个VMA,方便进程快速访问。
struct vm_area_struct {
unsigned long vm_start; //虚存区起始
unsigned long vm_end; //虚存区结束
struct vm_area_struct *vm_next, *vm_prev; //前后指针
struct rb_node vm_rb; //红⿊树中的位置
unsigned long rb_subtree_gap;
struct mm_struct *vm_mm; //所属的 mm_struct
pgprot_t vm_page_prot;
unsigned long vm_flags; //标志位
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
struct list_head anon_vma_chain;
struct anon_vma *anon_vma;
const struct vm_operations_struct *vm_ops; //vma对应的实际操作
nsigned long vm_pgoff; //⽂件映射偏移量
struct file * vm_file; //映射的⽂件
void * vm_private_data; //私有数据
atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;


六、进程的创建
6.1fork()
前面我们已经简单了解过了fork()函数,这里我们再次学习
进程调用 fork ,当控制转移到内核中的 fork 代码后,内核会做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork 返回,开始调度器调度

6.2fork函数返回值
子进程返回0
父进程返回的是子进程的pid
6.3写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自⼀份副本。具体见下图:

为什么要写时拷贝?
- 延迟分配内存,提高整机内存利用率(若子进程仅读取数据,无需额外分配内存);
- 加速
fork调用速度(避免大量数据拷贝); - 保证进程独立性(写入时自动分离,互不干扰)。
6.4fork常规用法
- 父子进程执行不同代码段(如父进程创建数据,子进程进行备份);
- 子进程通过exec函数替换为新程序。
6.5fork调用失败的原因
- 系统进程数达到上限(可通过
ulimit -u查看进程数限制); - 实际用户的进程数超过系统配置上限。
七、进程终止
进程终止的本质是释放进程占用的系统资源(内核数据结构 、内存、文件描述符等),并将退出状态反馈给父进程。
7.1进程退出场景
- 正常终止:代码运行完毕,结果正确(main函数的返回值通常表明你的程序执行的情况,如main函数返回
0); -
正常终止但结果错误:代码运行完毕,但逻辑错误(如
main函数返回1/2/3......,不同的值代表不同的错误原因); - 异常终止:代码运行中遇到错误(如除零、段错误、信号终止)。
7.2进程常见的退出方法
正常终止(可以通过 echo $? 查看进程退出码):
- 从main返回
- 调用exit
- _exit
异常退出:
- ctrl+c,信号终止

7.2.1退出码
退出码(退出状态)可以告诉我们最后⼀次执行的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码0 时表示执行成功,没有问题。 代码1 或0 以外的任何代码都被视为不成功。

错误码和错误码对应的描述我们可以打印出来观察一下。

这个时候如果我们想知道一个程序退出的具体原因可以这样


注意:异常退出时,退出码没有意义。进程出现异常一般是进程收到了信号。
7.2.2exit函数
exit 会在终止进程前执行清理工作,最终调用_exit系统调用(_exit 不会主动刷新缓冲区)。
#include <unistd.h>
void exit(int status);
·在任何地方调用exit表示进程结束,返回给父进程bash,子进程的退出码

7.2.3_exit函数
_exit 直接终止进程,不执行任何清理工作,速度更快。
#include <unistd.h>
void _exit(int status);
exit(C语言库函数) :_exit(系统)
库函数是系统的调用,所以exit调用_exit系统调用。
我们讨论的这个缓冲区是库缓冲区。

7.2.4return退出
return是⼀种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做exit的参数。
八、进程等待
8.1进程等待必要性
之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。 另外,进程⼀旦变成僵尸状态,那就刀枪不入,“杀⼈不眨眼”的kill-9也无能为力,因为谁也 没有办法杀死⼀个已经死去的进程。最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
8.2进程等待的方法
8.2.1wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL

8.2.2waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的⼦进程的进程ID;
如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0;
如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在;
参数:
pid:
Pid=-1,等待任⼀个⼦进程。与wait等效。
Pid>0.等待其进程ID与pid相等的⼦进程。
status: 输出型参数
WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的退出码)
options:默认为0,表⽰阻塞等待
WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该⼦进程的ID。
![]()
失败情况是:


8.2.3获取子进程status
wait和waitpid,都有⼀个status参数,该参数是⼀个输出型参数,由操作系统填充。如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status并非普通整数,而是一个 16 位的位图,核心有效位为低 16 位,解析规则如下:
- 低 7 位:存储子进程的终止信号(若为非 0,说明子进程异常终止);
- 高 8 位:存储子进程的退出码(仅当低 7 位为 0 时有效,即正常终止);

8.2.4阻塞与非阻塞等待
1.阻塞等待(options=0)
父进程暂停执行,直到有子进程退出,适合不需要并发处理其他任务的场景;
代码简单,无需循环检测。
2.非阻塞等待(options=WNOHANG)
父进程发起等待后立即返回,若无子进程退出则返回 0,不会阻塞;
适合父进程需要并发处理其他任务的场景(如服务器同时处理多个客户端请求);
需通过循环持续检测,直到回收子进程。

非阻塞代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if (pid == -1)
{
perror("fork failed");
return 1;
}
else if (pid == 0)
{ // 子进程
sleep(5);
exit(1);
}
else
{ // 父进程非阻塞等待
int status;
pid_t ret;
do {
ret = waitpid(pid, &status, WNOHANG); // 非阻塞
if (ret == 0)
{
printf("子进程仍在运行,父进程可处理其他任务...\n");
sleep(1); // 模拟父进程其他工作
}
} while (ret == 0); // 直到回收成功或失败
if (WIFEXITED(status))
{
printf("子进程退出,退出码:%d\n", WEXITSTATUS(status));
}
}
return 0;
}
模拟现实工作:
//// 函数指针类型
//typedef void (*func_t)();
//
//#define NUM 5
//func_t handlers[NUM+1];
//
//// 如下是任务
//void DownLoad()
//{
// printf("我是一个下载的任务...\n");
//}
//void Flush()
//{
// printf("我是一个刷新的任务...\n");
//}
//void Log()
//{
// printf("我是一个记录日志的任务...\n");
//}
//
//
//
//// 注册
//
//void registerHandler(func_t h[], func_t f)
//{
// int i = 0;
// for(; i < NUM; i++)
// {
// if(h[i] == NULL)break;
// }
// if(i == NUM) return;
// h[i] = f;
// h[i+1] = NULL;
//}
//
//int main()
//{
// registerHandler(handlers, DownLoad);
// registerHandler(handlers, Flush);
// registerHandler(handlers, Log);
//
//
// pid_t id = fork();
// if(id == 0)
// {
// //子进程
// int cnt = 3;
// while(1)
// {
// sleep(3);
// printf("我是一个子进程, pid : %d, ppid : %d\n", getpid(), getppid());
// sleep(1);
// cnt--;
// //int *p = 0;
// //*p= 100;
// // int a = 10;
// // a /= 0;
// }
// exit(10);
// }
//
// // 父进程
// while(1)
// {
// int status = 0;
// pid_t rid = waitpid(id, &status, WNOHANG);
// if(rid > 0)
// {
// printf("wait success, rid: %d, exit code: %d, exit signal: %d\n", rid, (status>>8)&0xFF, status&0x7F) ; // rid
// break;
// }
// else if(rid == 0)
// {
// // 函数指针进行回调处理
// int i = 0;
// for(; handlers[i]; i++)
// {
// handlers[i]();
// }
// printf("本轮调用结束,子进程没有退出\n");
// sleep(1);
// }
// else
// {
// printf("等待失败\n");
// break;
// }
//
// }
//
// return 0;
//}
九、进程程序替换
这个代码居然可以执行我们的命令,这个现象叫做进程程序替换。
程序替换是通过特定的接口,加载磁盘上的⼀个全新的程序(代码和数据),加载到调用进程的地址空间中!

9.1替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种 exec 函数以执行另⼀个程序。当进程调用⼀种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec 并不创建新进程,所以调用 exec 前后该进程 的 id 并未改变。

9.2替换函数
其实有六种以exec开头的函数,统称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[]);
9.2.1函数解释

事实上,只有 execve 是真正的系统调用,其它五个函数最终都调用 execve(封装) ,所以 execve 在 man ⼿册第2节,其它函数在 man ⼿册第3节。这些函数之间的关系如下图所⽰。

9.2.1命名理解
l(list) :表示参数采用列表
v(vector) :参数用数组
p(path) :有 p 自动搜索环境变量 PATH
e(env) :表示自己维护环境变量

十、自主Shell命令行解释器
10.1目标
- 要能处理普通命令
- 要能处理内建命令
- 要能帮助我们理解内建命令/本地变量/环境变量这些概念
- 要能帮助我们理解shell的允许原理
10.2实现原理
[root@localhost epoll]# ls
client.cpp readme.md server.cpp utility.h
[root@localhost epoll]# ps
PID TTY TIME CMD
3451 pts/0 00:00:00 bash
3514 pts/0 00:00:00 ps
这种情况,用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立⼀个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。

10.3实现一个shell
大纲:

具体实现还需要很多细节需要注意
10.4总结
我们学习的exec*/exit->retrun
⼀个C程序有很多函数组成。⼀个函数可以调用另外⼀个函数,同时传递给它⼀些参数。被调用的函数执行⼀定的操作,然后返回⼀个值。每个函数都有他的局部变量,不同的函数通过call/return系统进行通信。
这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux⿎励将这种应用于程序之内的模式扩展到程序之间。如下图

一个C程序可以fork/exec另⼀个程序,并传给它⼀些参数。这个被调用的程序执行⼀定的操作,然后通过exit(n)来返回值。调用它的进程可以通过wait(&ret)来获取exit的返回值。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)