本篇文章进行操作系统中进程控制的学习!!!
fork函数初识:
在Linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程
新进程为子进程,而原进程为父进程
#include
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程的pid,出错(创建子进程失败)返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做了以下工作:
分配新的内存块和内核数据结构给子进程(已知task_struct、mm_struct和页表)
将父进程部分数据结构内容拷贝至子进程
添加子进程到系统进程列表(运行队列)当中
fork返回时,开始调度器调度
进程 = 内核的进程数据结构 + 进程的代码和数据
- 创建子进程的内核数据结构(struct task_struct + struct mm_struct + 页表)+ 代码继承父进程,数据通过写时拷贝,来实现父子进程之间的共享和独立!!!
[lyh_sky@localhost lesson14]$ ls
process2 process2.c
[lyh_sky@localhost lesson14]$ cat process2.c
#include
#include int main()
{printf("我是一个进程: %d\n", getpid());fork();printf("我依旧是一个进程: %d\n", getpid());return 0;
}
[lyh_sky@localhost lesson14]$ ./process2
我是一个进程: 8968 // 父进程
我依旧是一个进程: 8968 // 父进程
我依旧是一个进程: 8969 // 子进程
fork之后子进程是从哪里开始执行的呢?
fork之前父进程独立执行,fork之后就有两个进程了,父子两个执行流分别执行
fork之后,父子进程共享所有的代码,而不是拷贝fork之后的代码!!!
但是子进程执行的后续代码不等于共享的所有代码,只不过子进程只能从这里开始执行
CPU中有一个EIP寄存器:它是一个程序计数器,作用是保存当前正在执行代码的下一条代码
EIP寄存器会拷贝给子进程,子进程便从EIP所指向的代码处开始执行!!!
这里子进程没有执行Before这条打印,因为EIP寄存器的原因!!!
[lyh_sky@localhost lesson14]$ ls
process2 process2.c
[lyh_sky@localhost lesson14]$ cat process2.c
#include
#include int main()
{printf("Before PID: %d\n", getpid());fork();printf("After PID: %d\n", getpid());return 0;
}
[lyh_sky@localhost lesson14]$ ./process2
Before PID: 9751
After PID: 9751
After PID: 9752
通过if else分流的方式来分别执行父子进程代码,各自完成自己的工作!!!
#include
#include
using namespace std;int main()
{pid_t id = fork();if (id == 0){while (1){cout << "我是子进程, 我的pid: " << getpid()<< ", 我的父进程是: " << getppid() << endl;sleep(3);}}else{while (1){cout << "我是父进程, 我的pid: " << getpid()<< ", 我的父进程是: " << getppid() << endl; sleep(3);}}return 0;
}
当fork一个子进程后:
通常,父子代码共享,父子再不写入时,数据也是共享的
当任意一方试图写入,便以写时拷贝的方式各自一份副本(独立性)
写时拷贝本身是由OS中内存管理模块管理的!!!

为什么要有写时拷贝???
写时拷贝:如果有多个调用者(callers)同时要求相同资源(如内存或磁盘上的数据存储),他们会同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用都是透明的(transparently)
提高效率和节省内存
创建子进程的时候,直接把数据分开不行吗???
【浪费内存资源】父进程的数据,子进程不一定使用,即便使用,也不一定全部进行写入
【效率低】如果fork的时候,就无脑的拷贝数据给子进程,会增加fork的成本(内存和时间)
采用写时拷贝的好处:
只会拷贝父子进程中其中一个数据修改的,变相的,就是拷贝数据的最小成本
写时拷贝采用延时拷贝策略,只有当你真正使用的时候,才进行拷贝
你想要,但是你不立马使用该空间,那先不给你,就意味着这段空间可以给别人使用,变相的提高内存的使用效率!!!
验证:系统有太多进程导致内存资源不足创建失败!!!
[lyh_sky@localhost lesson14]$ ls
test test.c
[lyh_sky@localhost lesson14]$ cat test.c
#include
#include
#include
#include int count = 0;int main()
{while (1){pid_t id = fork();if (id < 0){printf("创建子进程失败!!!\n");break;}else if (id == 0){printf("我是一个子进程: %d\n", getpid());// 让这个进程存在30秒后结束它sleep(30);// 结束该进程,错误码返回0exit(0);}++count;}printf("一共创建了%d个子进程\n", count);return 0;
}
关于进程终止的认识:
- C/C++中main函数的return 0,是给谁return的呢?
很多人说,是给操作系统返回的0,答案是不准确的
进程会维护一个退出码,表征进程退出的信息,让父进程进行读取
bash进程运行一个子进程时,子进程执行结束,会返回一个退出码给bash进程
echo $?:打印bash进程最近执行完毕的子进程的退出码,第二次会变回0
[lyh_sky@localhost lesson14]$ ls
test test.c
[lyh_sky@localhost lesson14]$ cat test.c
#include int main()
{return 20;
}
// 在bash进程中运行一个子进程
[lyh_sky@localhost lesson14]$ ./test
[lyh_sky@localhost lesson14]$ echo $?
20
[lyh_sky@localhost lesson14]$ echo $?
0
- 为什么return 0,可以return其他值吗?
常见的进程退出方式:
代码已经全部执行,结果是正确的
代码已经全部执行,结果是错误的
代码没有全部执行,程序出现异常
进程代码跑完,结果是否正确,返回0代表是成功的,非0代表是失败的
我们需要找到失败的原因,所以需要不同的错误码来标识不同的失败原因
一般来说失败时,非0值可以自定义,错误退出码 可以对应不同的错误原因,方便定位问题
C语言中的错误码
[lyh_sky@localhost lesson14]$ ls
errno errno_code.c
[lyh_sky@localhost lesson14]$ cat errno_code.c
#include
#include
#include
#include int main()
{// 错误码没有0,只有1~124个错误码for (int i = 1; i <= 124; ++i){printf("Erron code %d: %s\n", i, strerror(i));}return 0;
}
[lyh_sky@localhost lesson14]$ ./errno

关于进程终止的常见做法:
main函数return,非main返回不行,非main函数return代表该函数已经执行完毕
void exit(int status),调用exit
void _exit(int status),调用_exit
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255
exit 和 _exit的区别是什么??
exit终止进程,刷新缓冲区!!!
_exit直接终止进程,不会有任何的刷新操作!!!
exit函数执行过程:

exit
[lyh_sky@localhost lesson14]$ cat test.c
#include
#include
#include int main()
{printf("这是一个测试代码!!!");exit(0);return 0;
}
[lyh_sky@localhost lesson14]$ ./test
这是一个测试代码!!![lyh_sky@localhost lesson14]$
_exit
[lyh_sky@localhost lesson14]$ cat test.c
#include
#include
#include int main()
{printf("这是一个测试代码!!!");_exit(0);return 0;
}
[lyh_sky@localhost lesson14]$ ./test
[lyh_sky@localhost lesson14]$
我们都知道进程由内核数据结构、进程代码和数据组成
OS可能不会释放该进程的内核数据结构,比如:task_struct 和 mm_struct
OS会将它们放到内核数据结构缓冲池,“slab分配器”
slab是内存管理模块的数据结构,变相的可以提高内存的利用率!!!
前面的篇章说过,子进程退出,父进程如果不管它,子进程就会从S/R状态变为Z状态(僵尸),进而造成内存泄漏的问题!
进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程,就算是OS也无能为力
最后就是,父进程交给子进程的任务完成的如何,我们需要知道,如:子进程运行完成,结果如何,是否正常退出
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
#include
#include
pid_t wait(int*status);
pid_t:成功返回被等待进程pid,等待失败返回-1
status:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
wait函数可以解决回收子进程的Z(僵尸)状态,让子进程正常进入X(死亡)状态
举个例子:
[lyh_sky@localhost lesson14]$ ls
process2 process2.c
[lyh_sky@localhost lesson14]$ cat process2.c
#include
#include
#include
#include int count = 5;int main()
{pid_t id = fork();if (id == 0){while (1){printf("我是一个子进程, %d秒后退出\n", count);sleep(1);--count;if (count == 0){printf("子进程已退出,等待父进程回收!!!\n");break;}} }else{printf("我是一个父进程,等待子进程退出!!!\n");pid_t id = wait(NULL);printf("子进程已退出: %d\n", id);}return 0;
}
[lyh_sky@localhost lesson14]$ ./process2

#include
#include
pid_t waitpid(pid_t pid, int *status, int options);
当正常返回的时候,waitpid返回收集到的子进程的进程PID
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充
通过调用wait/waitpid函数可以从该函数内拿出特定的数据
status可以从子进程task_struct中拿出子进程的退出码和退出信号等等…
如果传递NULL,表示不关心子进程的退出状态信息
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究低16比特位):
int是四个字节,那么有32个比特位,这里面每八个比特位标识着一个状态

子进程代码执行完成,以退出码的方式返回给父进程
[lyh_sky@localhost lesson14]$ cat process.c
#include
#include
#include
#include
#include int cnt = 5;int main()
{pid_t id = fork();if (id == 0){while (1){printf("我是一个子进程: %d\n", getpid());sleep(1);// 循环执行五次后跳出cnt--;if (!cnt){break;}}printf("子进程已经执行完毕,即将退出!!!");exit(12);}else{int status;printf("我是一个父进程: %d, 等待子进程退出!!!\n", getpid());sleep(8);pid_t ret = waitpid(id, &status, 0);// 这里使用右移和按位与的方式获取退出码和终止信号printf ("子进程已经退出,pid: %d, 子进程退出码: %d, 进程退出信号: %d\n", ret, (status >> 8) & 0xFF, (status & 0x7F));}return 0;
}

子进程以异常的方式退出,父进程获取异常终止的信号
[lyh_sky@localhost lesson14]$ cat process.c
#include
#include
#include
#include
#include int cnt = 5;int main()
{pid_t id = fork();if (id == 0){while (1){printf("我是一个子进程: %d\n", getpid());sleep(1);cnt--;if (!cnt){break;}}// 空指针访问,段错误int* p = NULL;*p = 100;printf("子进程已经执行完毕,即将退出!!!");exit(12);}else{int status;printf("我是一个父进程: %d, 等待子进程退出!!!\n", getpid());sleep(8);pid_t ret = waitpid(id, &status, 0);printf ("子进程已经退出,pid: %d, 子进程退出码: %d, 进程退出信号: %d\n", ret, (status >> 8) & 0xFF, (status & 0x7F));}return 0;
}

退出码和终止信号先看谁呢?
一旦进程在执行时出现异常,进程会直接退出,我们只需要关注终止信号即可
退出码是进程执行完毕后,才会返回给父进程
上一篇:野孩子助醉舞,山鸟助唱
下一篇:双亲委派机制