目录

1.进程创建

1.1fork函数

1.2写时拷贝

1.3fork常见用法

1.4fork调用失败原因

2.进程终止

2.1进程退出场景 

2.2进程常见退出方法

正常终止(3种)

(1)从main函数返回

退出码

进程运行时错误码

区别

联系

(2)调用_exit函数退出

(3)调用_exit函数退出

异常终止


1.进程创建

1.1fork函数

特性 说明
功能 创建当前进程的一个子进程副本
头文件 #include <unistd.h>
函数原型 pid_t fork(void);
返回值 成功时
• 父进程返回子进程的PID(>0)
• 子进程返回0
失败时:返回-1
执行特点 调用一次,返回两次(分别在父、子进程中)
内存空间 子进程获得父进程地址空间的完整副本(写时复制)
继承内容 代码段、数据段、堆栈、文件描述符、信号处理等
不继承内容 进程ID、父进程ID、运行时间、文件锁、未决信号等
执行顺序 父进程与子进程的执行顺序不确定,由系统调度决定
典型用途 1. 创建并发进程
2. 配合exec()执行新程序
3. 实现多任务处理
fork函数从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程
进程调用fork,当控制转移到内核中的fork代码后,内核做:
  • 分配内核数据结构(task_struct、mm_struct、页表等)给子进程(新的内存块依靠写时拷贝)
  • 将父进程部分数据结构内容(task_struct)拷贝至子进程,子进程task_struct的部分内容依据子进程实际情况进行修正,例如子进程的PID、PPID等,子进程的mm_struct、页表中的内容初始时与父进程保持一致,触发写时拷贝后,发生独立的变化
  • 添加子进程到系统进程列表当中
  • fork返回(fork返回两次,在返回之前子进程已经存在,即父子进程各返回依次),开始调度器调度

fork 之前父进程独立执行, fork 之后,父子两个执行流分别执行
注意, fork 之后,谁先执行完全由调度器决定
父子兄弟进程的运行顺序都只与调度器有关

更多fork细节参考我的博客 

Linux 之 【进程的概念、task_struct、查看进程(proc、ps、kill)、fork创建进程】

https://blog.csdn.net/zl_dfq/article/details/154742314?sharetype=blogdetail&sharerId=154742314&sharerefer=PC&sharesource=zl_dfq&spm=1011.2480.3001.8118

1.2写时拷贝

  • 对于父进程的数据,操作系统完全可以拷贝一份给子进程,但这并不高效,因为子进程完全有可能不修改或者只修改几个数据
  • 为此,创建子进程之初,父子共享同一份物理内存数据,页表都标志为只读,当父或子任意一方尝试修改共享数据时,触发缺页中断机制,操作系统这才为修改方分配新的物理内存并将原数据复制到新内存中,然后再进行写入操作,并同时修改对应的页表映射关系
  • 上述行为就叫做写时拷贝
  • 这种“先共享,后按需复制”的延迟复制策略,就是写时拷贝的核心思想
通常,父子代码共享,父子不再写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图 :

创建子进程之后,父子页表中所有可写都标志为只读,并设置写时拷贝标志,当进程试图修改共享数据时,就会触发缺页中断,发生写时拷贝

代码段的数据本身就是只读的,除执行exec()等进程替换操作外,用户不可修改

1.3fork常见用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

1.4fork调用失败原因

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

2.进程终止

2.1进程退出场景 

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

2.2进程常见退出方法

  • 正常终止(3种)

(1)从main函数返回
退出码

return之后的数字,exit、_exit的参数值叫作退出码

标准规定,0表示代码运行完毕,结果正确,非0表示代码运行完毕,结果不正确

即结果的正确性统一通过退出码进行判定

 标准退出码及其隐含描述
0    成功(Success)
1    一般错误(General error)
2    Shell/builtin命令误用(Misuse of shell builtins)
126  命令不可执行(Command invoked cannot execute)
127  命令未找到(Command not found)
128  无效退出参数(Invalid argument to exit)
128+N  被信号N终止(Fatal error signal "N")
130  被Ctrl+C终止(128+2=SIGINT)
255  退出状态超出范围(Exit status out of range)

退出码用于计算机存储识别,退出描述就让用户知晓当前程序的运行结果,从而执行下一步决策,实际过程中,我们也可以建立一套自己的<退出码,退出描述>体系(字符串数组,下标即退出码,对应字符串即为退出描述)

echo $?  //该语句查看最近一个进程的退出码
  • 进程运行时错误码

进程运行时错误码(通常指 errno)是操作系统为每个进程(或线程)维护的一个全局变量,用于记录最近一次失败的系统调用或库函数的具体错误原因

全称 Error Number
本质 全局整型变量(实际是线程局部存储)
头文件 #include <errno.h>
功能 记录最近一次失败的系统调用或库函数的错误原因

strerror将错误编号(errno值)转换为可读的错误描述字符串

函数原型 char *strerror(int errnum);
头文件 #include <string.h>
功能 将错误编号(errno值)转换为可读的错误描述字符串
参数 errnum - 错误编号,通常是全局变量errno的值
返回值 指向错误描述字符串的指针(静态缓冲区,不可修改)
  1 #include<stdio.h>
  2 #include<string.h>
  3 
  4 
  5 int main()
  6 {
  7     for(int i = 0; i < 200; ++i)
  8         printf("[%d]: %s\n", i, strerror(i));                                                                                                                          
  9 
 10     return 0;
 11 }

实际过程中我们也可以建立一套自己的<错误码,错误描述>体系(字符串数组,下标即错误码,对应字符串即为错误描述)

区别
维度 错误码(Error Code / errno) 退出码(Exit Code) 关键区别
英文名称 Error Number Exit Status / Exit Code -
核心本质 运行时错误诊断 程序终止状态 运行时 vs 结束时
作用时机 程序运行中 程序结束时 时机完全不同
作用对象 单次系统调用/库函数 整个进程/程序 粒度不同
设置者 操作系统内核(自动) 程序自身(手动) 自动 vs 手动
获取方式 全局变量 errno Shell: $?
C: wait() 返回值
访问方式不同
数值范围 1-133+(系统相关) 0-255(8位有效) 范围不同
0的含义 没有错误(但一般不检查0) 成功(标准约定) 语义不同
持久性 被覆盖(只记录最近错误) 最终确定(程序结束) 临时 vs 永久
线程特性 线程局部(各线程独立) 进程全局(所有线程共享) 作用域不同
标准函数 strerror()perror() 无标准转换函数 支持程度不同
联系


上面这种“直接复用系统错误码作为退出码”的方式,在 Linux 系统命令中很常见,能让开发者/用户通过退出码快速定位内部错误原因

  • 简单工具/脚本、仅处理系统级错误(如文件操作、权限问题):放心直接用  errno  作为退出码;
  • ​复杂应用、需自定义业务错误:建议对  errno  做映射(比如系统错误码 + 1000 偏移),避免冲突。
perror

此外perror函数也能够转换全局变量errno

函数原型 void perror(const char *s);
头文件 <stdio.h>
功能 errno的值转换为可读的错误描述并输出到stderr
参数 s - 自定义前缀字符串(可为NULL)
返回值 无(void函数)
输出格式 s: 错误描述\n
(2)调用_exit函数退出
(3)调用_exit函数退出
对比维度 exit() _exit() 
标准归属 C 标准库函数 系统级函数
• _exit()
头文件 <stdlib.h> (C)
<cstdlib> (C++)
• _exit()<unistd.h>
函数原型 void exit(int status); void _exit(int status); 
退出方式 "优雅"退出(执行清理操作) "粗暴"退出(立即终止)
清理操作 执行全面清理
1. 调用所有atexit()注册的函数
2. 刷新所有I/O缓冲区
3. 关闭所有文件流
4. 删除临时文件
不执行任何清理
1. 不调用atexit()注册的函数
2. 不刷新缓冲区
3. 不关闭文件流
4. 不删除临时文件
缓冲区处理 自动刷新输出缓冲区,确保数据完整写入 不刷新缓冲区,可能导致数据丢失
文件处理 关闭所有打开的FILE*文件流 不关闭FILE*流,但操作系统会关闭文件描述符



  • _exit函数是系统调用接口,直接作用于操作系统内部,使程序终止
  • exit函数先要执行用户定义的清理函数,冲刷I/O缓冲区,关闭相关流等工作,最后再终止程序
  • 事实上,exit与_exit是调用与被调用的上下层关系
  • 而我们也可以得出一个结论:所谓的I/O缓冲区一定不再系统内核中,不然_exit终止程序时也要维护缓冲区中的数据!!!
     
  • eixt/_exit与return的区别是,eixt/_exit函数一旦在程序任何处被调用,进程直接终止,main函数中的return n等同于执行exit(n)(调用main的运行时函数会将main的返
    回值当做 exit 的参数 ),用户其他函数中的return语句只用于当前函数返回

exit VS _exit VS abort

  • exit:执行完整的C库清理流程,包括刷新所有标准I/O缓冲区、关闭流并调用通过atexit注册的函数,但不会调用C++局部对象的析构函数(仅保证静态对象析构)。适用于程序正常结束或遇到可恢复的运行时错误时,需要确保日志落盘、临时文件删除等资源释放的场景
  • abort:引发SIGABRT信号强制异常终止程序,不执行任何清理工作——不刷新缓冲区、不关闭流、也不调用atexit函数,但通常允许系统生成core dump文件供事后调试。适用于检测到不可恢复的逻辑错误(如空指针解引用、断言失败),且开发者需要保留崩溃现场进行定位分析的场景
  • _exit:直接发起内核级系统调用终止进程,完全绕过标准C库的清理机制(包括不刷新stdio缓冲区、不调用atexit),同时也不产生任何信号(不会触发SIGABRT)。主要使用场景是fork()后的子进程在exec失败时立即退出,以避免执行父进程注册的清理函数或刷新共享的缓冲区造成数据错乱
  • 异常终止

程序异常终止,实际上是先收到了某种信号,然后再终止程序
下面是Linux系统中所有的信号

下面演示11号段错误信号(非法内存访问(最常见))

验证进程收到信号而终止

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐