一.进程创建
1.fork函数
在linux中fork函数是⾮常重要的函数,它从已存在进程中创建⼀个新进程。新进程为⼦进程,⽽原进 程为⽗进程。
- #include <unistd.h>
- pid_t fork(void);
- 返回值:⾃进程中返回0,⽗进程返回⼦进程id,出错返回-1
进程调⽤fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给⼦进程
- 将⽗进程部分数据结构内容拷⻉⾄⼦进程
- 添加⼦进程到系统进程列表当中
- fork返回,开始调度器调度
- fork之后,谁先执⾏完 全由调度器决定
2.写时拷贝
当父进程创建子进程时fork,父进程首先要做的是,直接将父进程代码和数据对应页表项的权限,全部改成只读权限,然后子进程继承下来的页表也全部是只读权限,当子进程尝试通过代码对某些数据进行修改的时候,那么页表就立马识别到当前正在对只读区域进行写入操作,就会触发系统错误,触发系统错误的时候,系统就要进行判断,是真的错了,还是要进行写时拷贝,因为毕竟也会有野指针异常访问空间,系统错误后,就会触发缺页中断(后面学习会讲解,这里只需要了解就行),让系统去做检测,如果发现写入的区域是代码区,直接进行杀进程,但一旦发现写入的区域是数据区,就判定成发生写时拷贝,然后系统向物理内存申请空间,进行拷贝,修改页表映射,恢复权限。
要是写入时,这个内存空间不在物理内存里呢?
也是触发错误,发生缺页中断,系统检测,发生页表置换,从磁盘调入物理内存
写时拷贝就是时间换空间的做法!!!
为什么还要做一次拷贝呢?不能直接开辟空间吗?
因为你的写入操作!=对目标区域进行覆盖,也有可能会用到原有数据,比如:count++。
二.进程终止
main函数的返回值,是返回给父进程或者是系统,最终表示程序对还是不对!!
回顾一下查看上一次进程退出码的指令:echo $?。查看上次进程的退出信息,命令行中,最近一次程序的退出码。退出码表示错误原因。
一般0表示成功,非0 表示错误,用不同的数字,约定或表明出错的原因,系统提供了一批错误码,也可以自己约定错误码。
errno表示获取错误码,而strerror则是将该错误码转换成字符串
数字是给系统看的,字符串是给用户看到
看下面代码,如果不存在该文件,打开一定是失败的,则会返回错误码errno,然后通过strerror将数字转换成字符串。
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>
int main()
{
printf("before: errno: %d,errstring: %s\n", errno, strerror(errno));
FILE *fp = fopen("./log.txt", "r");
if (fp == nullptr)
{
printf("after: errno: %d,errstring: %s\n", errno, strerror(errno));
return errno;
}
return 0;
}
所有再用echo $?查看也是与errno一样。
在Linux下系统错误码有0-133,Windows下有0-140.
在两种系统分别执行下面代码查看:
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>
int main()
{
for(int i = 0;i<200;i++)
{
std::cout<<"code: "<<i<<", errstring: "<<strerror(i)<<std::endl;
}
return 0;
}
1.进程退出的时候,main函数结束代表进程退出,mian函数返回值代表进程所对应的退出码。
2.进程退出码可以由系统默认的错误码来提供,也可以约定自己的退出码。
1.进程终止的方式
1.main函数return
2.进程调用exit,exit在代码任何地方,表示进程结束,非mian函数的return,只表示函数结束,exit表进程结束
3._exit,系统接口,也是终止进程
补充:系统级头文件都是.h.语言级头文件,比如<stdio.h>,在c++中可以写成<cstdio>,但系统级头文件不行。
观察下面代码结果:
int main()
{
printf("进程运行结束!");
sleep(2);
exit(23);
sleep(2);
return 0;
}
然后对比_exit,看看区别:
int main()
{
printf("进程运行结束!");
sleep(2);
_exit(23);
sleep(2);
return 0;
}
结论:语言级exit会把打印从缓冲区刷新出来,再结束进程,而系统级_exit则不会把打印从缓冲区刷新出来,直接结束进程。
1.刷新缓冲区的区别
2.上下层的区别
我们知道语言级函数,往往是封装系统调用接口,更接近上层,
我们试想一下我们之前认为的缓冲区在哪个位置?
这个缓冲区一定不在OS内部,因为如果在OS内部那么printf打印的信息也一定在OS中,所不管调用哪个函数,都会刷新缓冲区,所有这个缓冲区一定不在OS中。
结论:这个缓冲区是语言级缓冲区,由C/C++提供的!!!
调用exit,fflush从语言层把缓冲区内容刷新到OS中,在刷新到屏幕上。
调用_exit,则直接会杀死进程,数据还在缓冲区内,没机会刷新。
三.进程等待
对于我们创建出来的子进程,作为父进程,必须等待这个子进程,因为是父进程创建的,就必须对子进程负责,对子进程进行回收,如果不回收,根据进程状态那一节内容,子进程就是变成僵尸进程,如果忘了可以进行回顾:进程的状态。
我们看下面代码,如果不进行回收,子进程就会变成僵尸:
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>
#include <cstdio>
#include<unistd.h>
int main()
{
pid_t id = fork();
if(id<0)
{
printf("errno: %d,errstring: %s\n",errno,strerror(errno));
return errno;
}
else if(id == 0)
{
int cnt = 10;
while(cnt)
{
printf("子进程运行中,pid: %d\n",getpid());
sleep(1);
cnt--;
}
exit(0);
}
else
{
while (true)
{
printf("我是父进程,pid: %d\n",getpid());
sleep(1);
}
}
return 0;
}
1.wait
学习两个回收子进程函数:
1.先看wait,一般而言,父进程创建子进程就要等待子进程,直到结束
wait
1.回收子进程的僵尸状态
2.等待的时候子进程,如果不退,父进程就要一直阻塞在wait内部,类似scanf。
3.返回值,大于0,表示成功回收子进程,小于0,表示回收失败
4.等待期间,父进程阻塞式等待,等待成功一般返回子进程pid
5.作用:等待任意一个子进程退出
#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id<0)
{
printf("errno: %d,errstring: %s\n",errno,strerror(errno));
return errno;
}
else if(id == 0)
{
int cnt = 5;
while(cnt)
{
printf("子进程运行中,pid: %d\n",getpid());
sleep(1);
cnt--;
}
exit(0);
}
else
{
sleep(10);
pid_t rid = wait(nullptr);
if(rid > 0)
{
printf("wait sub processon,rid: %d\n",rid);
}
while (true)
{
printf("我是父