知识扩展:
1.冯诺依曼体系
我们常见的计算机,如笔记本;不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。
我们所认识的计算机,由一个个硬件组件组成
输入设备:键盘、鼠标、扫描仪
中央处理器(CPU):含有运算器和控制器等
输出设备:显示器、打印机等
注:关于冯诺依曼
这里的存储器是内存
不考虑缓存的情况下,CPU只能对内存进行读写,不能访问内设;外设要输入输出数据,也只能操作内存。因此,所有设备都只能直接和内存交互
2.操作系统
2.1 概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。宏观上看,操作系统包括:内核(进程管理、内存管理、文件管理、驱动管理),其他程序(函数库、shekk程序等)
2.2 设计操作系统目的
对上:为用户程序提供良好的运行环境
对下:与硬件交互,管理所有软硬件资源
2.3 核心功能
在计算机软硬件架构中,操作系统定位是:一套纯管理的软件
2.4 如何理解管理
先描述 -》再组织
描述: 把目标对象的属性抽象到struct中,并且建立 struct结构体《-》实体目标对象 一对一的映射关系。这样把对目标对象的管理转化成对struct结构体的管理
组织:用链表或者其他数据结构将这些结构体组织起来。把对这些目标对象的管理转换成对链表等数据结构的管理
2.5系统调用和库函数
在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
3.进程
3.1 概念
从教学角度:进程是程序的一个运行实例,即一个正在运行的程序
从内核角度:分配系统资源(CPU、内存)的实体
从当前学习角度:进程=内核数据结构+程序代码和数据
3.2进程PCB&task_struct
基于先描述后组织的原则,OS为了完成对进程的管理
描述:将进程属性信息抽象提取保存在PCB(process control block)这个结构体中。Linux下的PCB是:task_struct,即task_struct是PCB的一种。
task_struct:
- 标示符:描述本进程的唯一标示符,用来区别其他进程。
- 状态:任务状态,退出代码,退出信号等。
- 优先级:相对于其他进程的优先级。
- 程序计数器:程序中即将被执行的下一条指令的地址。
- 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据:进程执行时处理器的寄存器中的数据 [休学例子,要加图 CPU,寄存器]。
- I/O 状态信息:包括显示的 I/O 请求,分配给进程的 I/O 设备和被进程使用的文件列表。
- 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
组织:所有运行在系统里的进程,对应的task_truct都以双链表的形式保存在内核里
一.进程状态

1.进程状态定义
一个进程可以有几个状态,这些状态在kernel源代码中:
cpp
*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 */
};
1.1 僵尸状态
当子进程退出并且父进程没有读取子进程的返回代码,子进程变为僵尸进程
子进程退出,销毁进程地址空间和页表、只保留PCB,等待父进程获取返回值,父进程不获取。此时需要一直维护退出状态,退出状态也属于进程基本信息,包含在PCB中,因此会一直维护PCB。那么当父进程创建很多很多子进程不回收,就会造成资源泄露,因为PCB数据结构本身也占据内存资源。
cpp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 1;
}
else if(id == 0){//child
printf("I am child, pid : %d\n", getpid());
sleep(10);
}else{//parent
printf("I am parent, pid: %d\n", getpid());
sleep(3);
exit(0);
}
return 0;
}
1.2 孤儿进程
父进程先退出,子进程变成孤儿进程。会被1号init/systemd进程领养。运行与一般进程一致,执行完会被1号进程回收。
2.进程状态查看
ps(process status)
ps aux / ps axj 命令
a:显⽰⼀个终端所有的进程,包括其他⽤⼾的进程。
x:显⽰没有控制终端的进程,例如后台运⾏的守护进程。
j:显⽰进程归属的进程组ID、会话ID、⽗进程ID,以及与作业控制相关的信息
u:以⽤⼾为中⼼的格式显⽰进程信息,提供进程的详细信息,如⽤⼾、CPU和内存使⽤情况等

ps -eo
-e:选择所有进程
-o:指定输出字段即你要显示的列(如pid,stat,comm)
示例:
bash
while :; do ps -eo pid=进程ID,stat=状态,comm=程序名 | grep -E 'test|进程ID'| grep -v grep; sleep 1; echo "###################"; done

二、进程切换
上下文:进程被CPU执行时,CPU寄存器内的数据称为该进程的上下文。CPU内寄存器只有一套,但上下文可以有很多份,分别对应不同的进程。
CPU上下文切换:实际含义是任务切换。当多任务内核准备切换任务时,保存当前正在运行进程的数据,即此时寄存器内数据。这些内容保存任务进程自己堆栈中。入栈完成后将下一个任务的运行状况从他自己的堆栈中取出重新装入CPU,并开始下一个任务的运行。

进程切换示意:

时间片:当代计算机大部分为分时操作系统,每个进程都有适合的时间片(就是一个计数器)。时间片到达,进程就被从CPU上剥离下来。
三、LinuxO(1)调度队列
优先级
进程获取CPU资源的先后顺序,即为进程优先级。
优先权⾼的进程有优先执⾏权利。配置进程优先权对多任务环境的linux很有⽤,可以改善系统性 能。
还可以把进程运⾏到指定的CPU上,这样⼀来,把不重要的进程安排到某个CPU,可以⼤ 改善 系统整体性能。
问题:一个CPU一定会有很多的进程要执行,而这些CPU又有不同的优先级,CPU是如何组织这些进程呢?引入下面:
1.概念引入
一个CPU拥有一个runqueue,如下图所示

2.活动队列
时间片没有结束的进程都按优先级放在活动队列中。
nr_active:当前有多少个运行状态的进程
进程处于运行状态,不一定正在被 CPU 物理执行。运行状态(Linux 中为 TASK_RUNNING)表示进程具备 CPU 执行资格,包含两类:一类是真正在 CPU 上执行的进程(已获得 CPU 使用权、分配了时间片),另一类是在 runqueue(运行队列)中排队、等待调度的就绪进程。
queue[140]:⼀个元素就是⼀个进程队列,相同优先级的进程按照FIFO规则进⾏排队调度,所以, 数组下标就是优先级!
问题:从该结构中,选择⼀个最合适的进程,过程是怎么的呢?
1.从0下表开始遍历queue[140]
2.找到第⼀个⾮空队列,该队列必定为优先级最⾼的队列
3.拿到选中队列的第⼀个进程,开始运⾏,调度完成!
4.遍历queue[140]时间复杂度是常数!但还是太低效了!
• bitmap[5]:⼀共140个优先级,⼀共140个进程队列,为了提⾼查找⾮空队列的效率,就可以⽤ 5*32个⽐特位表⽰队列是否为空,这样,便可以⼤提⾼查找效率!

3.过期队列
• 过期队列和活动队列结构⼀模⼀样
• 过期队列上放置的进程,都是时间⽚耗尽的进程
• 当活动队列上的进程都被处理完毕之后,对过期队列的进程进⾏时间⽚重新计算
4.active指针和expired指针
• active指针永远指向活动队列
• expired指针永远指向过期队列
• 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间⽚到期时⼀直 都存在的。
• 没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了⼀批 新的活动进程!
在系统当中查找⼀个最合适调度的进程的时间复杂度是⼀个常数,不随着进程增多⽽导致时间成 本增加,我们称之为进程调度O(1)算法!
cpp
struct rq {
spinlock_t lock;
/*
* nr_running and cpu_load should be in the same cacheline because
* remote CPUs use both these fields when doing load calculation.
*/
unsigned long nr_running;
unsigned long raw_weighted_load;
#ifdef CONFIG_SMP
unsigned long cpu_load[3];
#endif
unsigned long long nr_switches;
/*
* This is part of a global counter where only the total sum
* over all CPUs matters. A task can increase this counter on
* one CPU and if it got migrated afterwards it may decrease
* it on another CPU. Always updated under the runqueue lock:
*/
unsigned long nr_uninterruptible;
unsigned long expired_timestamp;
unsigned long long timestamp_last_tick;
struct task_struct *curr, *idle;
struct mm_struct *prev_mm;
struct prio_array *active, *expired, arrays[2];
int best_expired_prio;
atomic_t nr_iowait;
#ifdef CONFIG_SMP
struct sched_domain *sd;
/* For active balancing */
int active_balance;
int push_cpu;
struct task_struct *migration_thread;
struct list_head migration_queue;
#endif
#ifdef CONFIG_SCHEDSTATS
/* latency stats */
struct sched_info rq_sched_info;
/* sys_sched_yield() stats */
unsigned long yld_exp_empty;
unsigned long yld_act_empty;
unsigned long yld_both_empty;
unsigned long yld_cnt;
/* schedule() stats */
unsigned long sched_switch;
unsigned long sched_cnt;
unsigned long sched_goidle;
/* try_to_wake_up() stats */
unsigned long ttwu_cnt;
unsigned long ttwu_local;
#endif
struct lock_class_key rq_lock_key;
};
/*
* These are the runqueue data structures:
*/
struct prio_array {
unsigned int nr_active;
DECLARE_BITMAP(bitmap, MAX_PRIO+1); /* include 1 bit for delimiter */
struct list_head queue[MAX_PRIO];
}
四、环境变量
1.概念
环境变量⼀般是指在操作系统在运行中所需要的一些参数
• 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪 ⾥,但是照样可以链接成功,⽣成可执⾏程序,原因就是有相关环境变量帮助编译器进⾏查找。
思考:
1.我们编写代码时,链接时候,不知道动静态库具体在哪,但是照样链接成功,生成可执行程序
1.1命令行参数
示例:

解释:一段指令会被Bash以空格为分隔符划分为一个个字符串,存入agrv数组,argc代表个数。(这个表属于bash还是指令进程?)
Bash执行指令简易流程:
1.bash解析输入指令串,按空格划分,存入argv数组
2.bash先判断是否为内置命令(ls等),若是直接调用函数执行;若为外部命令,①在PATH变量指令路径中寻找对应文件;②此时是我们自己编写的可执行文件,指定路径./,在当前路径找到文件。
3.bash找到对应指令文件,使用fork()创建子进程,子进程会拥有Bash的进程地址空间+代码+argv[]
4.子进程拿到对应指令可执行文件后,调用exec()系列函数进行进程替换
5.子进程执行,此时程序已替换为指定文件
6.父进程进程等待,收到子进程返回值后,恢复运行,打印命令行提示符等待用户输入
核心逻辑:Bash 处理「解析命令、查找路径」,子进程处理「执行程序」
总结:main的命令行参数,是程序实现不同子功能的方法!
2.环境变量 PATH
问题:我们自己写的程序,执行时需要输入./xxx,而ls这类系统指令则不需要
2.1要执行一个程序,首先要找到他
bash会在环境变量PATH指定路径找
2.2系统中存在环境变量,帮助系统找到目标二进制文件
bash会从家目录下.bash_profile文件内,获取环境变量
一个bash中会有两个表,argv存储命令行参数,ENV存储系统中的环境变量(从系统配置文件内加载)。
系统默认从PATH指定路径查找对应文件,若我们自己创建的文件,则需为bash指定路径寻找。
3.其他环境变量

3.1USER:使用者
su(不带-)被称为「非登录式 shell」:它只是切换了用户的 UID(权限),但不会读取目标用户的登录配置文件(如.bash_profile、.profile),所以大部分环境变量会继承当前会话的设置。su -(带-,也可以写su --login)被称为「登录式 shell」:它会模拟目标用户从控制台登录的过程,清空原有环境,重新加载目标用户的所有登录配置,环境变量完全替换为目标用户的默认值。
普通用户 → root
su:USER 不变(保留原用户),只给权限
su -:USER 变 root,全新环境
root → 普通
su 和 su -:USER 都会变
因为降权必须重置身份
eg:使用USER实现一个指定用户才可以执行的程序
cpp
int main(int argc,char* argv[])
{
//获取当前用户信息
const char* user=getenv("USER");
if(user==NULL)
return 1;
if(strcmp(user,"cyh")==0)
{
//从argv获取指令
if(strcmp(argv[1],"-a")==0)
{
printf("这是功能1\n");
}
else if(strcmp(argv[1],"-b")==0)
{
printf("这是功能2\n");
}
else
{
printf("指令无法识别!\n");
printf("Usage:%s [-a|-b]\n",argv[0]);
}
}
else{
printf("Only cyh can do!");
}
return 0;
}
4.环境变量获取方法
4.1 操作
export:设置一个新的环境变量
bash
[cyh@iZuf6ba7plevoziooe3s2pZ 0314]$ export MYENV=112233

env:显示所有环境变量
bash
[cyh@iZuf6ba7plevoziooe3s2pZ 0314]$ env

echo $XXX:显示指定环境变量值

unset:清除环境变量
bash
[cyh@iZuf6ba7plevoziooe3s2pZ 0314]$ unset MYENV

set:显示环境变量和本地定义的shell变量
bash
[cyh@iZuf6ba7plevoziooe3s2pZ 0314]$ set

4.2 获取环境变量
func1:使用env[ ]
func2:getenv()
func3:extern char**environ;
5.环境变量特性
5.1 环境变量具有全局特性
5.2 概念引入
a.本地变量:bash在运行时所需要的变量,不会被子进程继承,只在bash内部被使用。使bash自己也可以执行脚本
b.export内建命令:不需要创建子进程执行,bash自己调用函数/系统调用执行
五.进程地址空间
1.结合C语言回顾
在学习C语言时,老师为我们画出这样的空间布局图

结合代码验证:
cpp
//未初始化全局变量
int g_unval;
//初始化全局变量
int g_val=1;
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",&g_unval);
static int test=0;
printf("test static addr:%p\n",&test);
//堆区
char *heap_mem = (char*)malloc(10);
char *heap_mem1 = (char*)malloc(10);
char *heap_mem2 = (char*)malloc(10);
char *heap_mem3 = (char*)malloc(10);
printf("heap addr: %p\n", heap_mem); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem1); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem2); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\n", heap_mem3); //heap_mem(0), &heap_mem(1)
//栈区
printf("stack addr: %p\n", &heap_mem); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem1); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem2); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem3); //heap_mem(0), &heap_mem(1)
//代码区
const char* str="hello world";
printf("read only string addr: %p\n", str);
for(int i = 0 ;i < argc; i++)
{
printf("argv[%d]: %p\n", i, argv[i]);
}
for(int i = 0; env[i]; i++)
{
printf("env[%d]: %p\n", i, env[i]);
}
return 0;
}

2.虚拟地址
DOE1:
cpp
#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;
}
结果:
parent[2995]: 0 : 0x80497d8
child[2996]: 0 : 0x80497d8
原因:
创建子进程时,子进程会继承会继承父进程的代码和数据
DOE2:
cpp
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;
}
结果:我们发现父子进程的输出地址一样,但是变量内容不一样
分析:
变量内容不一样-》指向的并不是一个变量地址值一样-》该地址值不是物理地址!
在Linux地址下,这种地址叫做 虚拟地址
我们在⽤C/C++语⾔所看到的地址,全部都是虚拟地址!物理地址,⽤⼾⼀概看不到,由OS统⼀ 管理
3.进程地址空间
引入进程地址空间,便可以解释上述问题。父进程创建子进程,子进程将父进程的地址空间和页表拷贝,在g_val未改变时,父子进程将其保存在同一虚拟地址,经页表映射到相同的物理地址,如同浅拷贝一样;在fork()尝试改变g_val时,发生写时拷贝,在物理地址中为子进程拷贝一份g_val到新的地址,并建立与子进程页表映射关系

虚拟地址空间:操作系统为每个正在运行的进程一个承诺,给予他所有的内存(但实际并未完全给他),承诺的多了,也需要管理起来,使用先描述后组织,虚拟地址空间便是描述的一个数据结构
区域划分:指将整片区域等分为一个个小单位,对于不同功能区,保存其开始地址和结束地址,便将地址划分为不同功能区。
3.1 虚拟地址空间实质:并不真正保存数据,只有对虚拟地址的划分

3.2 代码加载过程:

(1)按需求在虚拟地址空间中申请指定大小空间
(2)将程序加载到物理内存
(3)在页表中建立虚拟地址与物理地址的映射
注:在真实的代码加载过程中,以2G数据+代码为例,OS/进程在虚拟地址空间按区域申请2GB空间并存入页表,物理内存不会一次加载所有数据,可能一次加载500MB到内存,并建立虚拟地址-物理地址的映射。当操作系统访问页表,出现只有虚拟地址没有物理地址的情况,发生缺页中断,继续从磁盘加载500MB数据到内存,并建立虚拟地址-物理地址的映射,依次..
虚拟地址空间的存在原因:
1.将地址从无序变为有序
对于物理内存来说,一次申请很大的连续空间,利用率低,如果不用连续分别随机申请,利用率高;对于用户or进程来说,更希望申请的空间地址连续有逻辑,方便访问。为了兼顾两方,OS会自动做虚拟地址-》物理地址转换,虚拟地址提供给上层用户,数据真正保存于物理内存。
2.地址转换过程中,可以与访问的地址和操作进行合法性判断,进而保存物理内存
页表实际上除了保存虚拟和物理地址,还会保存操作权限。避免对内存的非法访问与操作,保护内存安全。
3.让进程管理与内存管理,进行一定程度的解耦
进程只需要访问操作虚拟地址空间,至于磁盘怎么样向内存加载数据or一次加载多少or若内存剩余空间不够怎么办,交给内存管理。
问题:tast_struct用来描述进程;mm_struct用来描述虚拟地址空间,在mm_struct中保存各区域start&end地址用来区域划分。但是以堆举例,每次我们malloc都会返回不同的堆地址,但是mm_struct中只保存堆的整体划分,我们怎么管理那些独立的堆空间?
linux内核使⽤ vm_area_struct 结构来表⽰⼀个独⽴的虚拟内存区域(VMA),由于每个不同的虚
拟内存区域功能和内部机制都不同,因此⼀个进程使⽤多个vm_area_struct结构来分别表⽰不同类型 的虚拟内存区域。 mm_struct中有一个struct vm_area_struct *mmap指向该进程的vm_area_struct;所有区域的vm_area_struct使用链表链接。
cpp
struct mm_struct
{
/*...*/
struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */
struct rb_root mm_rb; /* red_black树 */
unsigned long task_size; /*具有该结构体的进程的虚拟地址空间的⼤⼩*/
/*...*/
// 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
};
cpp
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对应的实际操作
unsigned 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;

问题:为什么要引入写时拷贝?
子进程创建时拷贝父进程的地址空间和页表,虚拟地址-》物理地址映射关系完全一致。子进程运行时,如果修改数据的,要保证不影响父进程的数据,所以需要在物理地址上拷贝一份,那么拷贝多少呢?此时引入写时拷贝:
1.减少创建时间
2.减少内存浪费
创建子进程时,会拷贝父进程的mm_struct和页表,虚拟内存与物理内存映射关系完全一致。假设父进程存在50个变量,子进程可能只对其中10个做修改,那么我们只需要为这10个变量申请新的内存,其他40个可以共享。
写时拷贝过程
父进程原本在页表中,对于物理内存映射时的权限为读权限,子进程创建并拷贝父进程的mm_struct和页表,对于物理内存的访问权限也为只读,现在要对某一虚拟地址空间地址做写操作,OS在映射时会判断,此时权限只读但要做写操作,判断为写时拷贝,在内存创建新的变量,更改子进程页表对应位置的物理内存,并对这段映射关系的权限更改为读写。
六、进程控制
1.进程退出场景
①代码运行完毕,结果正确
②代码运行完毕,结果不正确
③代码异常终止
父进程为了执行特定任务创建子进程,子进程执行后需要将反馈给父进程,让父进程拿到任务执行情况。main()函数的返回值,即退出码,便代表程序执行的情况,存储到自己进程的task_struct中,可被父进程拿到。(只有代码正常执行完毕时所返回的退出码有效,异常终止的退出码没有参考价值,因此下面所讨论的退出码针对①②情景)
在linux中,为我们提供了一些常见错误以及对应的退出码:
当我们向命令行中输入指令时,bash创建子进程帮我们执行任务,子进程的退出码会返回给bash,我们可以通过在命令行输入echo $?查询最近一个进程的退出码
示例:打开一个不存在的文件
cpp
int main()
{
FILE *fp = fopen("log.txt", "r");
if(fp == NULL) return 13;
//// 读取
fclose(fp);
return 0;
}

对于代码正常执行完毕的情况,退出码由程序员自行维护,只要能对应上即可;对于代码异常中断时,退出码无效,系统会向进程发送信号,此时信号及对应的状况有操作系统规定
1.2 _exit()与exit()
1.2.1 _exit函数
cpp
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终⽌状态,⽗进程通过wait来获取该值
1.2.2 exit()函数
cpp
#include <unistd.h>
void exit(int status);
exit最后也会调⽤_exit, 但在调⽤_exit之前,还做了其他⼯作:
-
执⾏⽤⼾通过 atexit或on_exit定义的清理函数。
-
关闭所有打开的流,所有的缓存数据均被写⼊
-
调⽤_exit

验证:
cpp
int main()
{
printf("hello");
exit(0);
}
运⾏结果:
root@localhost linux\]# ./a.out
hello\[root@localhost linux\]#
```cpp
int main()
{
printf("hello");
_exit(0);
}
```
运⾏结果:
\[root@localhost linux\]# ./a.out
\[root@localhost linux\]#
#### 2.进程等待
2.1 原因:
原因1:子进程退出后,父进程不回收会变为僵尸进程,造成内存泄漏
原因2:子进程创建是为了执行任务,需要把任务完成结果返回给父进程
因此,父进程通过进程等待的方式,回收子进程资源 获取子进程退出信息
2.2 进程等待的两种方式
2.2.1 定义
```cpp
#include







