进程控制:地址空间、fork与进程异常结束
目录
引言
进程地址空间补充
fork
进程终止
编辑
strerror接口
errno接口
扩展:段错误(1.野指针 2.溢出、越界)
哪些错误可以编译报错,哪些可以运行报错
编译时错误:
运行时错误:
异常终止(代码没有跑完)
exit
_exit
编辑
原因
引言
在当今的计算世界中,Linux操作系统以其强大的稳定性和灵活性而著称,成为了服务器和开发领域的宠儿。在Linux的内核深处,进程控制机制是维持系统高效运行的核心力量。通过对进程的精确管理,Linux能够确保资源的合理分配,维护系统的稳定性和响应性。在本专题中,我们将深入探讨Linux进程控制的核心内容,包括进程的创建、执行、同步和终止。
本文的核心要点存在三个:fork创建子进程、进程地址空间补充、进程异常结束现象。
进程地址空间补充
进程地址空间自下而上分为:代码区、已初始化全局数据、未初始化全局数据、堆区、战栈区、命令行参数、环境变量
fork
fork会创建子进程。本节重点介绍写时拷贝
当只读数据需要写入时,不做异常处理,重新映射,修改权限。
子进程会发生写时拷贝,父进程也会发生!
在操作系统的进程管理中,写时拷贝(Copy-on-Write,简称COW)是一种优化策略,它主要发生在创建子进程的时候,但并不仅限于子进程。
当父进程通过系统调用如fork()创建子进程时,内核不会立即为子进程分配一份父进程内存空间的拷贝,而是让父子进程共享同一内存区域。只有当父进程或子进程尝试写入该共享内存区域时,内核才会为写入者分配一个新的页,并将原来的页标记为只读,这个过程就是写时拷贝。因此,写时拷贝可以发生在以下情况:
子进程写入:如果子进程尝试写入共享内存中的某个页面,将会触发写时拷贝,子进程获得自己的页面拷贝,而父进程的页面保持不变。
父进程写入:同理,如果父进程尝试写入共享内存中的某个页面,也会触发写时拷贝,父进程得到自己的页面拷贝,而子进程的页面保持不变。
写时拷贝技术可以显著减少不必要的内存分配和数据复制,提高fork()操作的效率。然而,需要注意的是,写时拷贝主要是在进程fork()之后,父子进程共享内存页时才会发生。如果不是在这种共享情况下,比如进程自己分配新的内存页并进行写入操作,就不会涉及写时拷贝机制。
所以,总结来说,写时拷贝既可能发生在子进程,也可能发生在父进程,前提是它们共享了内存页,并且其中一方尝试写入这些页。
创建子进程
if语句子进程进入,if语句以外父进程执行。
子进程与父进程谁先执行,有调度器决定。
进程终止
为什么代码结束需要return 0
return的这个数字是干嘛的?
这个return 0会被父进程(或者bash)拿到
?是一个表示进程退出码的变量。在echo输出时,$可以获取?的变量代表的信息。
后续的?变成0的原因是,上面的指令是echo指令,正常退出,所以?变成0。
退出码
strerror接口
可以打印错误码对应的错误信息。
打印的是错误码的描述。
2对应的就是找不到文件。
定制错误码:只需要返回对应的下标即可
errno接口
如果用ret接收errno
这样return ret时,就可以得到对应的错误码。
扩展:段错误(1.野指针 2.溢出、越界)
“段错误”通常是指计算机程序在执行过程中发生的一种错误,它表明程序尝试访问其内存地址空间中不允许访问的段或试图以无效的方式使用内存。在操作系统,特别是类Unix系统中,这通常会导致程序崩溃,并出现“段错误”(Segmentation Fault)的信息。
空指针解引用:尝试访问一个尚未初始化或已经设置为NULL的指针。
越界访问:数组或指针超出其分配的内存区域进行读写操作。
释放后使用:在释放内存之后仍然尝试访问或修改该内存。
非法指针运算:对指针进行非法的算术运算,例如向一个整数指针增加一个指针。
栈溢出:函数调用太深导致栈空间耗尽。
哪些错误可以编译报错,哪些可以运行报错
在C++编程中,错误可以分为几种类型,它们可能在编译时或运行时被检测到。以下是一些常见的错误类型及其发生阶段:
编译时错误:
-
语法错误:这些是最常见的编译时错误,它们发生在代码的语法不符合C++语言规范时。例如,遗漏分号、括号不匹配、关键字拼写错误等。
if (x > 0 // 编译错误:括号未闭合
-
类型错误:当变量、表达式或函数的使用与它们的类型不匹配时,会发生类型错误。例如,将字符串赋值给整数变量。
int x = "hello"; // 编译错误:不能将字符串字面量赋值给int类型
-
声明和定义错误:如果使用了未声明或未定义的变量或函数,编译器会报错。
int main() {printHello();// 编译错误:函数printHello未声明 }
-
模板错误:在使用模板时,如果模板实例化失败,会在编译时报错。
template <typename T> T max(T a, T b) {return a > b ? a : b; }int main() {max<string>(3, 4);// 编译错误:不能比较int和int以用于string类型 }
-
预处理错误:预处理指令的错误,如宏定义不当或条件编译指令错误。
#define MAX_SIZE 5+ // 编译错误:宏定义中的表达式无效
运行时错误:
-
逻辑错误:代码逻辑不正确,但语法无误,这样的错误通常不会导致编译失败,但会在运行时导致不正确的行为。
for (int i = 0; i > 10; ++i) {// 运行错误:循环条件错误,循环不会执行 }
-
空指针解引用:解引用一个未初始化或已删除的指针会导致运行时错误。
int* p = nullptr; *p = 10; // 运行错误:空指针解引用
-
数组越界:访问数组索引之外的元素会导致未定义行为。
int arr[5]; arr[10] = 100; // 运行错误:数组越界
-
资源管理错误:如忘记释放分配的内存(内存泄露)、文件未关闭等。
int* p = new int[10]; // 如果忘记后续的delete[] p,则会导致内存泄露
-
异常处理错误:如果在代码中抛出了异常但没有适当的try-catch块来捕获它,程序可能会异常终止。
throw std::runtime_error("Error occurred"); // 如果没有try-catch块,则会导致程序非正常终止
段错误是运行时的错误。
理解这些错误类型及其发生阶段对于编写健壮和高效的C++代码至关重要。编译时错误通常更容易诊断和修复,而运行时错误可能更难以追踪和修复。
异常终止(代码没有跑完)
程序的异常被转化为信号
18 19代表的是进程的终止与继续。
我们可以通过 kill -数字 进程pid的格式来控制进程。
exit
exit的作用是直接终止进程,不管是出现在函数中,还是main中。
而exit()中传入的参数就是进程退出时的退出码。
?中保存了退出码。
exit与return
exit在任何地方调用都表示进程的退出,但是return只有在主函数结束时,才表示进程的退出。
_exit
头文件为unistd.h
_exit也是终止程序。但是_exit在2号手册是一个系统调用。
区别
注意下面的代码没有\n(linux的stdout以\n为刷新缓冲区的标记)
发现先休眠1S,然后打印数据
当我们改成_exit时
换成_exit之后,缓冲区没有刷新
原因
这就是系统调用和库函数的区别。
因此我们的prinf、cat一定是先将数据打印到缓冲区,然后才刷新。
同时也可以得出结论:这个缓冲区绝对不在内核区(占用1G),而是在用户区(占用3G)