当前位置: 首页 > news >正文

初识Linux 进程:进程创建、终止与进程地址空间

目录

0.写在前面

1.进程创建

fork():

exec():

2.进程地址空间

3.进程终止

正常终止(主动退出)

异常终止(被动终止)


0.写在前面

        本文将对Linux环境下的进程:包括进程创建、终止与进程等待、替换进行讲解,作者使用XShell连接配置为CentOs 7.6的主机进行演示,希望能帮助你更好理解操作系统的运行原理!

1.进程创建

        什么是进程创建?怎么来创建进程?和别的进程有什么关系?别着急,这些问题需要慢慢回答。首先是进程创建的概念:

       进程创建的本质是为程序的运行构建独立的执行环境,包括:分配内存空间;创建进程控制块(PCB)......

        重点在于进程如何创建!:Linux 中进程创建过两个关键系统调用完成:

fork():

        先来看看man-pages关于fork的介绍:

        fork()的作用是创建父进程的副本(子进程)。内核会为子进程分配新的PCB (task_struct),并复制父进程的大部分资源(如内存页表、文件描述符、信号处理等),并且采用写时拷贝优化:

写时复制子进程与父进程初始时共享同一块物理内存(仅复制页表,不复制代码和数据)。只有当父进程或子进程尝试修改内存时,才会为修改的部分分配新的物理内存并复制数据。这大幅减少了内存占用和创建时间。

        再来看看返回值:

         如果成功创建子进程会在父进程返回PID,子进程本身返回0,如果创建失败,父进程返回-1,子进程没有,errno出现( 父子进程/多个子进程的运行顺序由调度器决定,不同的平台下不同)

        了解了返回值,我们可以根据pid_t类型的fork返回值区分出哪一个是父进程哪一个是子进程,但是既然说创建子进程会直接沿用父进程的代码和数据,并进行写时拷贝,这里能衍生出四个问题:

        1. 为什么fork要给子进程返回0,给父进程返回子进程pid? 
         - 对父进程而言,一个进程可能有多个子进程,没有函数能直接获取所有子进程的PID,fork返回子进程PID便于父进程识别和管理子进程。  
         - 对子进程而言,它可通过getpid()获取自身PID,fork返回0使其能轻松与父进程区分(进程ID 0由系统特殊使用,子进程PID不可能为0)。  

        2. fork干了什么?
        fork创建一个新进程(子进程),子进程是父进程的副本:  
        - 共享代码段,也就是说代码是共享的,但子进程拥有自己的PCB,且操作系统会拷贝父数据中子进程要修改的部分,(代码一般是共享的,页表和虚拟地址空间同样写时拷贝)父子进程互不干扰。(这属于数据层面的写时拷贝)  

        - 为什么?进程之间是独立的,数据可能被修改,父子进程不能共用一份数据,所以数据必须拷贝一份
        - 两进程从fork返回处继续执行,子进程继承父进程打开的文件描述符等资源。  

        3. fork是如何返回两次的?
        fork执行时,系统复制父进程的堆栈段,父进程和子进程都停留在fork函数中等待返回。         - 父进程中,fork返回子进程的PID
        - 子进程中,fork返回0
补充:父子进程共用代码段,return语句在父子进程中都会执行

        4. 一个变量为什么会有不同的内容?
        fork后,子进程获得父进程数据空间的独立副本,两者数据空间互不共享。  本质上发生了写时拷贝,操作系统重新开辟空间给子进程进行修改。看完前面内容,你可能觉得这个问题有点多余,但如果你去揪出两个进程中返回值的地址,你会发现是相同的。这就关乎
进程地址空间了。

exec():

        由于这一类系统调用的特殊,作者放到了进程替换中详细介绍~

2.进程地址空间

        通常在编程时,提到的栈区堆区常量区之类的,并不是物理地址!如果是物理地址,那么地址相同的变量不可能对应不同值!那么这里可以得出的是栈区堆区保存在虚拟地址空间中,先让我们了解一下虚拟地址空间的结构,这里可以通过一个简单的程序验证一下:

int g_val_1;                                                                           
int g_val_2=100;void test1()
{printf("code addr: %p\n",test1);//验证字符串常量区const char* str="hello bit";printf("read only string addr: %p\n",str);//验证已初始化的全局变量printf("init global value addr: %p\n",&g_val_2);//验证未初始化的全局变量printf("uninit global value addr: %p\n",&g_val_1);//验证堆区的地址char* mem=(char*)malloc(100);printf("heap addr: %p\n",mem);printf("stack addr: %p\n",&str);printf("stack addr: %p\n",&mem);//验证栈区的地址int a;int b;int c;printf("stack addr: %p\n",&a);printf("stack addr: %p\n",&b);printf("stack addr: %p\n",&c);
}

 通过简单的编译命令,我们执行可执行文件查看一下执行结果:

啊哈,这就验证了栈区向下增长,堆区向上增长的结论!直接给出虚拟地址空间的架构:

 虚拟地址空间通过页表来对应真实的物理地址:

        

下面我们来一睹mm_struct的真容:mmstruct部分代码:

struct mm_struct
{    unsigned long total_vm, locked_vm, shared_vm, exec_vm;unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;unsigned long start_code, end_code, start_data, end_data;unsigned long start_brk, brk, start_stack; ......
}

存在虚拟地址空间的意义:
        1.让进程以统一的视角看待内存,不用直接管理物理内存上的存储空间与地址;
        2.增加进程虚拟地址空间可以让我们访问内存的时候,增加一个转换的过程,
可以对寻址请求审查,拦截异常访问,保护物理内存;
        3.因为有地址空间和页表的存在,将进程管理模块,和内存管理模块进行解耦合;

        再来补充一下页表:

        保存在哪里?页表的物理地址(其实是页表的真实地址)临时存储在页目录基址寄存器(如 x86 的 CR3)上面,当进程切换时会被当作上下文保存,页表记录虚拟地址对应的物理地址,有权限标志位参与权限管理,在每一个虚拟地址对应物理地址的同时加上读写权限,那么尝试写入常量区时会被页表发现,还有标志位来判定代码和数据是否以已经被加载到内存中,由于操作系统对大文件采用惰性加载的方式:推迟非必要资源的加载或初始化,直到实际需要时再执行,数据并不会全部加载到内存,此时有了虚拟地址但物理地址是空着的,标识为0,触发缺页中断,从磁盘中将数据加载到内存有了物理地址再补全映射关系。

3.进程终止

        这里我们就把写出来的一个程序当作进程,进程终止的触发场景可分为正常终止异常终止两类:

正常终止(主动退出)

进程因完成任务或主动请求结束,常见方式包括:

  • main() 函数返回:程序执行到main()函数末尾,返回一个整数值(如return 0),等价于调用 exit(返回值);
  • 调用exit()函数:标准库函数exit(返回码)会执行清理操作如刷新 IO 缓冲区)
  • 调用_exit()系统调用:直接终止进程(不执行清理,不刷新缓冲区),将退出状态传递给内核(exit()最终会调用_exit())

但是正常终止分为两种情况:1.代码执行完毕,结果正确;2.代码执行完毕,结果错误;

        父进程会根据子进程main函数的退出码(return的返回值)来判断代码的运行情况:

        bash就是父进程,我们的程序运行时就相当于子进程,那么程序中main函数的返回码对bash来说至关重要!

        1.返回码为0,运行成功;
        2.返回码不为0,不同的返回码对应不同的情况;

| | 让我们来看看C语言中不同的返回码都可以携带哪些信息:

int test2()
{int i=0;for(i=0;i<60;i++){printf("%s\n",strerror(i));//退出码和退出码的描述具有对应关系,再通过strerror来将错误打印出来。//在C语言中,有errno这样的全局变量来保存最近一次的错误码,//可以用perror/strerror来打印错误信息}return 2;//这里故意返回的2,来看看返回码的运用}


补充:如何来查看退出码呢?在命令行界面,用echo $?就可以查看上一个程序的退出码;在程序中可以使用wait/waitpid来获取子进程的退出码(由exit返回),作者放到进程等待讲解!

哈哈,return 2的作用在这里! 

异常终止(被动终止)

进程因外部干预或内部错误被迫终止,常见场景包括:

  • 收到终止信号:如用户通过 kill -9 命令发送 SIGKILL,这里的kill命令本质上就是一种信号!

相关文章:

  • 软考软件评测师——基于风险的测试技术
  • protobuf原理和使用
  • 网络基础知识
  • vue2实现【瀑布流布局】
  • 推一帧,通一气:跨平台RTMP推流的内家功夫
  • Mysql面经
  • SpringBoot3+Vue3(1)-后端 请求头校验,jwt退出登录,mybaits实现数据库用户校验
  • SAGE:基于SAM进行二级蒸馏的多模态图像融合新方法,CVPR2025
  • 如何处理 collation 导致的索引失效 | OceanBase SQL调优实践
  • 信奥赛-刷题笔记-栈篇-T3-P4387验证栈序列0520
  • 13 分钟讲解所有知名 Python 库/模块
  • Linux探秘:驾驭开源,解锁高效能——基础指令
  • 数据仓库是什么?常见问题解答
  • 彭博社聚焦Coinbase数据泄露,CertiK联创顾荣辉警示私钥风险与物理攻击
  • Java从入门到精通 - 案例专题
  • 【RK3588嵌入式图形编程】-Cairo-形状与填充
  • 瑞萨单片机笔记
  • JS 中 var、let、const 的区别联系
  • 奥威BI:打破AI数据分析伪场景,赋能企业真实决策价值
  • CesiumEarth v1.15 更新
  • 央行行长潘功胜主持召开金融支持实体经济座谈会
  • 破题“省会担当”,南京如何走好自己的路?
  • 习近平:坚定信心推动高质量发展高效能治理,奋力谱写中原大地推进中国式现代化新篇章
  • 中国首次当选联合国教科文组织1970年《公约》缔约国大会主席国
  • 习近平在河南洛阳市考察调研
  • 上海青少年书法学习园开园:少年以巨笔书写《祖国万岁》