【Linux】进程

知识扩展:

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之前,还做了其他⼯作:

  1. 执⾏⽤⼾通过 atexit或on_exit定义的清理函数。

  2. 关闭所有打开的流,所有的缓存数据均被写⼊

  3. 调⽤_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 #include pid_t wait(int* status); 返回值: 成功返回被等待进程pid,失败返回-1。 参数: 输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL ``` ```cpp 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。 ``` > 阻塞等待: > > 若子进程已经退出,使用wait/waitpid时会立刻返回,并回收资源 > > 若子进程未完成,父进程会阻塞在此处等待子进程完成 > 非阻塞轮询: > WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程 > > 是否是正常退出)检查status0-7位退出码 > > WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程 > > 的退出码)提取status8-15位退出码 > waitpid使用示例: ```cpp int main() { int id=fork(); if(id==0) { //child printf("i am child:%d\n",getpid()); exit(10); } else { //parent int status=0; pid_t rid=waitpid(id,&status,0); if(rid>0) { if(WIFEXITED(status)) { printf("wait successs,rid:%d,exit code:%d,exit signal:%d\n",rid,WEXITSTATUS(status),status&0x7F); } else { printf("子进程退出异常!\n"); } } else { printf("wait failed:%d:%s\n",errno,strerror(errno)); } } return 0; } ``` ![](https://i-blog.csdnimg.cn/direct/9c9ac892c7cc411b8bea3fdb4cb57788.png) 非阻塞轮询示例: ```cpp int main() { int id=fork(); if(id==0) { //child while(1) { printf("i am child:%d\n",getpid()); sleep(2); } } else { while(1) { int status = 0; pid_t rid = waitpid(id, &status, WNOHANG); if(rid > 0) { if(WIFEXITED(status)) { printf("wait successs,rid:%d,exit code:%d,exit signal:%d\n",rid,WEXITSTATUS(status),status&0x7F); } else { printf("子进程退出异常!,exit signal:%d\n",status&0x7F); } } else if(rid == 0) { // 函数指针进行回调处理 int i = 0; printf("子进程没有退出\n"); sleep(1); } else { printf("等待失败\n"); break; } } } return 0; } ``` 结果:父进程每隔一秒等待一次子进程,子进程未结束,父进程返回正常执行。 ![](https://i-blog.csdnimg.cn/direct/76df487c85e14999924a75988bb106f8.png) 向子进程发送kill -9指令 2.2.2 获取⼦进程status wait和waitpid,都有⼀个status参数,该参数是⼀个输出型参数,代表子进程状态,由操作系统填充。 > 如果传递NULL,表⽰不关⼼⼦进程的退出状态信息。 > 否则,操作系统会根据该参数,将⼦进程对应的退出信息反馈给⽗进程。 使用exit(10)退出程序,通过echo $?打印退出码 ![](https://i-blog.csdnimg.cn/direct/3acdd1e5d55e45e1a6b440a60a8b1447.png) 那么status是否也为10呢? ```cpp int main() { int id=fork(); if(id==0) { //child printf("i am child:%d\n",getpid()); exit(10); } else { //parent int status=0; pid_t rid=waitpid(id,&status,0); if(rid>0) { if(WIFEXITED(status)) { printf("status:%d\n",status); } else { printf("子进程退出异常!\n"); } } else { printf("wait failed:%d:%s\n",errno,strerror(errno)); } } return 0; } ``` ![](https://i-blog.csdnimg.cn/direct/3cd55b20412b43a29c4e2784e8163f67.png) status好像并不简单等于退出码,那么到底是什么? status结构如下,可当做位图: ![](https://i-blog.csdnimg.cn/direct/51517f20fa7341bcb3fbe671a1d3e299.png) > 低15-8位,代表退出码;0-7位,代表退出信号。 > > 若0-7位为0,说明进程未收到信号,代码正常执行完毕,退出码有参考价值-----\>对应进程退出情况①② > > 若0-7为非零,说明进程收到信号,异常终止,代码执行终止,退出码无意义-------\>对应情况③ 子进程退出后,只保留了task_struct,等待父进程回收,我们推理可得进程执行的退出码与退出信号也保存在这里。父进程通过系统调用waitpid(),将exit_code\& exit_signal一起保存在status中。 ![](https://i-blog.csdnimg.cn/direct/9954c0f0d0454cbeab3cfb75931b6b4c.png) #### 3.进程程序替换 我们在bash输入一串指令,bash是如何执行呢?其实是bash创建子进程,对子进程进行程序替换,让子进程帮我们完成任务,下面我们引入进程替换 1.定义 程序替换是通过特定的接⼝,加载磁盘上的⼀个全新的程序(代码和数据),加载到调⽤进程的地址空间中! 2.原理 ⽤fork创建⼦进程后执⾏的是和⽗进程相同的程序(但有可能执⾏不同的代码分⽀),⼦进程通过调⽤⼀种 exec 函数以执⾏另⼀个程序。当进程调⽤⼀种 exec 函数时,该进程的用户空间代码和数据完全被 新程序替换,从新程序的启动例程开始执⾏。调⽤ exec 并不创建新进程,所以调⽤ exec 前后该进程 的 id 并未改变(见示例2)。 ![](https://i-blog.csdnimg.cn/direct/8f1dff3c3fb34c6cb88852b15ac39587.png) 示例1:建议使用进程替换完成TOP功能 ```cpp int main() { printf("我的程序要运行了!\n"); //execl("/usr/bin/ls", "ls", "-l", "-a", NULL); execl("/usr/bin/top", "top", NULL); printf("我的程序运行完毕了\n"); return 0; } ``` 结果:exec后的printf函数并未打印,说明进程替换成功后新的代码会覆盖原代码 ![](https://i-blog.csdnimg.cn/direct/41683e464f94451aa767d3d8b201b77b.png) 示例2:结合fork()使用进程替换执行ls功能 ```cpp int main() { printf("before exec!\n"); int id=fork(); if(id==0) { //child int rid=execl("/usr/bin/ls","ls","-al",NULL); if(rid==-1) { printf("exec fail!"); exit(1); } } else { //parent waitpid(-1,NULL,0); printf("after exec!\n"); } return 0; } ``` 结果: ![](https://i-blog.csdnimg.cn/direct/c76c2cfa679c4a33aa2e0bced50a651f.png) 实例3:DOE验证进程替换后有没有创建新的进程 main.c ```cpp int main() { printf("我的程序要运行了!\n"); printf("My pid is:%d\n",getpid()); //execl("/usr/bin/ls", "ls", "-l", "-a", NULL); //execl("/usr/bin/top", "top", NULL); execl("./other", "other", NULL); printf("我的程序运行完毕了\n"); return 0; } ``` other.cc ```cpp int main() { std::cout<<"hello C++,My pid is:"< 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[]); ``` > 这些函数执行成功会加载新的目标程序覆盖当前进程代码和数据,不会返回 > > 调用失败返回-1 > > 调用成功执行新的代码,不存在调用成功的返回值 4.2 命名解释 l(list) : 表⽰参数采⽤列表 v(vector) : 参数⽤数组 p(path) : 有 p ⾃动搜索环境变量 PATH e(env) : 表⽰⾃⼰维护环境变量 ![](https://i-blog.csdnimg.cn/direct/de56f53556e242feb90b7206aba4d133.png) ```cpp #include int main() { char *const argv[] = {"ps", "-ef", NULL}; char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL}; //以list形式传递参数 execl("/bin/ps", "ps", "-ef", NULL); // 带p的,可以使⽤环境变量PATH,⽆需写全路径 execlp("ps", "ps", "-ef", NULL); // 带e的,需要⾃⼰组装环境变量 execle("ps", "ps", "-ef", NULL, envp); //以vector形式传递参数 execv("/bin/ps", argv); // 带p的,可以使⽤环境变量PATH,⽆需写全路径 execvp("ps", argv); // 带e的,需要⾃⼰组装环境变量 execve("/bin/ps", argv, envp); return 0; } ``` 事实上,只有 execve 是真正的系统调⽤,其它五个函数最终都调⽤ execve ,所以 execve 在 man ⼿册 第2节,其它函数在 man ⼿册第3节。这些函数之间的关系如下图所⽰。 ![](https://i-blog.csdnimg.cn/direct/e4659a2535324f80abddc41c91ad23f0.png) 4.3 环境变量加载的两种方式 不调用带e的 exec函数-》不会清空当前的环境变量表-》putenv() 调用带e的exec函数-》会清空当前的环境变量表-》putenv()+传入environ 正常情况下other进程: ![](https://i-blog.csdnimg.cn/direct/fb526fe33cfc4c26ae48663cf4ab5b4c.png) ### 七、实现一个自定义shell shell逻辑: 1.打印命令行提示符 2.从标准输入中获取指令 3.解析指令并保存在argv数组中未来传给exec函数 4.判断是否为内建命令,若为内建,自己执行;若不是内建,创建子进程执行 main.cc: ```cpp #include "util.hpp" bool cd(); bool echo(); //获取用户 const char* GetUser() { const char* User=getenv("USER"); return User==NULL?"None":User; } //获取主机名 const char* GetHostName() { const char* HostName=getenv("HOSTNAME"); return HostName==NULL?"None":HostName; } //获取当前工作目录 const char* GetPwd() { //真正的工作目录-》CWD PWD只是shell基于CWD更新的一个环境变量 //const char* HostName=getenv("PWD"); //1.获取cwd const char* HostName=getcwd(cwd,sizeof(cwd)); //2.更新env表中pwd if(HostName!=NULL) { snprintf(cwdenv,sizeof(cwdenv),"PWD=%s",cwd); putenv(cwdenv); } return HostName==NULL?"None":HostName; } // eg:/home/cyh/study/0311 std::string DirName(const char* pwd) { #define SLASH "/" std::string dir=pwd; if(dir==SLASH) return dir; //反向寻找最后一个'/' auto pos=dir.rfind(SLASH); //std::cout<<"pos:"< #include #include #include #include #include #include #define COMMAND_SIZE 1024 #define FORMAT "[%s@%s %s]->>" char* argv[COMMAND_SIZE]; int argc=0; char cwd[1024]; char cwdenv[1024]; int lastcode=0; bool BuiltinResult; bool cd() { //cd 回到家目录 if(argc==1) { char* path=getenv("HOME"); //char* path=NULL; for test if(chdir(path)==-1) { std::cout<<"cd fail"<

相关推荐
半个俗人2 小时前
07.Linux vi编辑器
linux·运维·编辑器
淼淼爱喝水2 小时前
OpenEuler 操作系统期末复习:核心命令汇总
linux·考试·openeuler
HealthScience2 小时前
Linux在一个容器中创建一个子用户
linux·运维·服务器
jiay22 小时前
[ubuntu] 2404安装cuda13-0
linux·windows·ubuntu
忘了ʷºᵇₐ8 小时前
在IDEA 2024.1版本中如何打开Remote Host及连接linux
linux·运维·服务器
零K沁雪9 小时前
Linux 内核中与网络地址相关的函数
linux·内核
steins_甲乙11 小时前
# 从 0 做一个小型内存泄漏检测器:开篇与架构设计
linux
蒸蒸yyyyzwd12 小时前
后端学习笔记 day4
linux·笔记·学习
upp12 小时前
[最新版本centos 10系统制作与安装]
linux·运维·centos