前言:前面了解完前面的Linux进程基础概念后,我们算是解决了Linux进程中的一大麻烦,现在我们准备更深入的了解Linux进程——Linux进程控制!
我们主要介绍的Linux进程控制内容包括:进程终止,进程等待与替换,进程程序替换!
本篇主要内容:
重识进程创建
进程退出场景
错误码和退出码
1. 重识进程创建
1.1 fork函数
在我们之前提到过,创建进程使用的是fork
函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h> pid_t fork(void); // 返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
fork函数返回值:
- 子进程返回0,
- 父进程返回的是子进程的pid
这些我们在之前都提到过,我们复习一下,今天我们重点要讲的是fork
的写时拷贝
1.2 写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
当父进程形成子进程之后,子进程写入,发生写时拷贝,重新申请空间,进行拷贝,修改页表(OS)
但是,我们怎么知道发生了写时拷贝呢?写时拷贝的内容都是由操作系统来完成的
- 其实父进程创建子进程的时候首先将自己的读写权限,改成只读,然后再创建子进程,此时是操作系统在做,用户并不知道,而且用户可能会对某一数据进行写入,这时页表转换就会出现问题,操作系统就会介入,就触发了我们重新申请内存拷贝内容的策略机制
1.3 fork创建进程与进程终止
接下来我们看一下这段代码:
#include<stdio.h> #include<unistd.h> #include<stdlib.h> #define N 10 typedef void (*callback_t)(); // 函数指针 void Worker() { int cnt = 10; while(cnt) { printf("i am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt); sleep(1); cnt--; } } void createSubProcess(int n, callback_t cb) { int i = 0; for(i = 0; i < n; i++) { sleep(1); pid_t id = fork(); if(id == 0) { printf("create child process success: %d\n", i); // child cb(); exit(0); } } } int main() { createSubProcess(N, Worker); // 只有父进程走到这里 sleep(100); return 0; }
而代码中的exit函数可以让进程终止
这段代码涵盖了fork创建进程与进程终止,让我们来执行以下
fork创建进程与进程的终止
下面将让我们真正来了解进程终止!
2. 进程终止
进程退出场景:
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
前面二者都是正常退出,而最后则是异常退出
2.1 main的退出码
我们之前写代码中,main函数只能return 0吗?答案是肯定不是!
在多进程环境中,我们创建子进程的目的就是协助父进程办事,但是父进程怎么知道子进程把事情办得怎么样?所以父进程要知道子进程办的怎么样,就有了退出码,而main函数的返回值,就是进程的退出码!
返回 0 就表示成功,其他数字就表示进程失败的原因,每个不同的数字代表不同的原因!
我们可以通过函数来直接查看每个数字代表的意义:strerror
它可以返回描述错误码的字符串
#include<stdio.h> #include<string.h> int main() { for(int i = 0; i < 200; i++) { printf("%d: %s\n", i, strerror(i)); } return 0; }
退出码 0 正好对应的是成功!
当我们134位置处时,发现已经没有错误信息了。
注意:错误码我们可以自己自定义!
main函数的退出码是可以被父进程获取的,用来判断子进程的运行结果
我们可以直接用指令查看进程的退出码:
指令:
echo $?
int main() { return 31; }
我们在执行完,这段代码后,再用指令查看退出码
我们可以发现指令:echo $?
返回的是上一个进程的错误码
2.2 C语言的错误码
错误码和退出码:
- 错误码通常是衡量一个库函数或者是一个系统调用一个函数的调用情况
- 退出码通常是一个进程退出的时候,他的退出结果
- 二者都是当失败的时候,用来衡量函数、进程出错是的出错详细原因
我们用代码来测试一下:
#include<stdio.h> #include<errno.h> int main() { // errno 可以返回最近一次错误的错误码 int ret = 0; printf("before: %d\n", errno); FILE *fp = fopen("csdn.txt", "r"); if(fp == NULL) { printf("after: %d, error string: %s\n", errno, strerror(errno)); ret = errno; } return ret; }
一旦出现错误,错误码errno就会被系统自动赋值
可以通过错误码来判断代码运行的情况
2.3 进程异常终止
当我们的代码出异常了,也就是进程异常终止了,退出码还有意义吗?
// 当我们在运行这样的代码时 int a = 100; a /= 0; ...... int *p = NULL; *p = 100; ......
第一种情况: Floating point exception
第二种情况: Segmentation fault
当然不止这两个情况,但是它们都会让程序进程异常终止!
其实一旦程序出现了异常,操作系统就是通过信号
的方式来杀掉这个进程!
而我们的前面两种情况正好对应了kill -8
和 kill -11
,
我们来试试,通过使用信号能不能还原之前这几种错误情况
while(1) { printf("i am a process, pid: %d\n", getpid()); sleep(1); }
通过信号终止进程
进程出异常本质是收到了对应的信号,因此一个进程是否出异常,我们只要看有没有收到信号即可
3. 总结拓展
拓展:exit与_exit的区别
在之前讲到正常退出时,我们有3种情况。
正常终止
- 从main返回
- 调用exit
- _exit
而exit与_exit两个有什么区别和联系呢?
首先他们二者都可以让进程终止,并且使用方法也一样,那他们到底有什么区别呢?我们用代码来一探究竟!
//代码一: int main() { printf("Hello"); exit(0); } ...... //代码二: int main() { printf("Hello"); _exit(0); }
代码一:
代码二:
为什么会出现这种情况呢?
exit最后也会调用exit, 但在调用exit之前,还做了其他工作:
- 执行用户通过 atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写入
- 调用_exit
printf打印如果不使用\n换行的话,数据会被存储到缓冲区里,,exit函数会帮助我们
刷新缓冲区的数据,然而_exit函数不会。因为exit函数在调用exit之前将所有缓存数据都写入了,所以在终止进程时,会将数据打印在屏幕上!
综上:
- 1.exit 是库函数,exit是系统调用
- 2.exit终止进程的时候,会自动刷新缓冲区。 exit终止进程的时候,不会自动刷新缓冲区
总结
进程终止相对于其他的进程控制来说,是很好理解的。我们主要介绍了进程终止,在明白错误码和退出码之后,相信能理解父进程是怎么得到子进程信息的。让我们期待下一节——进程等待与替换!
谢谢大家支持本篇到这里就结束了