进程创建
进程创建的绝大部分内容在之前的学习中已经详细学习过了,这里就简单看一下,然后主要再看一下写时拷贝。
fork会在当前进程内部创建一个子进程,创建失败返回-1,创建成功会有两个返回值,对子进程返回0、父进程返回子进程的pid。
进程调用fork,当控制转移到内核中的 fork 代码后,内核会做:
1.子进程也需要一个内存块及自己的内核数据结构task_struct、mm_struct这些,由其分配。
2.然后继承父进程的PCB、页表、进程地址空间等信息,pid等信息进行修改。
3.把子进程添加到系统进程列表中。
写时拷贝
页表中除了虚拟地址和物理地址的映射关系外,还存在着权限(这里权限实际是体现一个4KB页的权限,这里就理解为每一个物理地址的权限)。
1.只有父进程的时候,通过虚拟地址在页表映射找到物理地址可以找到对应的代码和数据,此时的代码是只读的不能进行修改而数据是可写可读的,可以对数据进行修改。
2.子进程在被父进程创建之后会继承父进程的代码和数据,此时父子进程过虚拟地址在页表映射找到物理地址可以找到对应的代码和数据,但是此时的代码段对于父子进程都是只读的,都不能进行修改。当其中一方对数据进行修改的时候会发生写时拷贝,必须是数据才会发生写时拷贝 代码不可以(通过mm_struct可以知道)。
所以操作系统实现写时拷贝通过设置页表权限,当父子代码段权限出错的时候(并不是真的错误 只是一种识别方式),操作系统就对其进行写时拷贝。
为什么写时拷贝?
1.减少创建时间,子进程得到父进程的数据不需要额外的拷贝消耗
2.减少内存浪费,提高内存使用率。子进程不一定对父进程的所有变量都需要修改 可能只有部分做修改 需要修改的变量修改时写时拷贝
父进程创建子进程的目的?
1.执行自己后面代码
2.执行全新的功能 指如令
fork失败的原因?
1.内核数据结构创建失败 PCB页表内存没有申请出来
2.加载内存时候失败 如代码和数据
本质都是因为内存空间不够
进程中止
创建进程会占用资源,那么进程中止同样的需要释放资源,如释放PCB页表等。
进程退出 三种情况
1.代码运行结束,结果正确。
2.代码运行结束,结果错误。
3. 发生异常 如: /0、野指针问题等
根据这三种子进程的状态情况,父进程就知道子进程是否完成了自己的任务。
main函数也是被其他函数调用的,需要把返回值返回,而它的父进程通过它的返回值就知道了该函数的运行情况,通常就是前两种情况。
如下 我们一个简单打开文件hh.txt的程序,打开失败会返回1。
该进程是父进程bash的子进程,这个子进程的作用就是以只读方式打开文件(该文件不存在 最终会返回1),但是程序在运行之后就直接结束了,那么父进程怎么知道是否成功呢?
父进程判断该进程是否完成任务就是通过接收该子进程main函数的返回值,所以main函数的返回值就是该进程的退出码,退出码是0代表成功,其他的值表示出错的各种原因(会有多种),而这个退出码存储在当前子进程的task_struct中,该信息没有被父进程接收之前子进程是僵尸状态,在父进程回收之后子进程僵尸状态结束。
通过echo $?打印最近一个程序(进程)退出时的退出码
打印的结果就是该程序main函数的返回值,所以main函数的返回值就是当前进程退出码
而再一次使用后打印的结果是0,因为打印的是echo进程的退出码,上一次echo是正常结束退出码就是0。
但是数字代表的具体信息是什么呢?
strerror(i)可以打印i号错误对应的字符串信息
如下代码打印0到150号的信息,我们可以发现信息从0到133一共有134个,之后的打印的都是 Unknow ,也就是运行正确会返回0 ,错误会有从1到133一共有133种不同的错误信息。
errno在发生错误时自动设置errno为对应的错误编号
如下当打开一个不存在的文件 或者ls 对一个不存在文件使用时 errno就是2 就是对应文件不存在的错误信息。
这套134个退出码是系统自己设计的,我们也可以不遵守,自己设计退出码所对应的信息,根据退出码自己知道是什么错误。
这是上面两点程序正常情况结束的情况,那么第三点异常呢?
如果发生异常了,退出码没有意义!
如下代码发生 /0的错误 此时会发生异常 返回的结果并不是我们指定的666而是154
退出码方式
main函数结束就代表当前进程结束,所以main函数的返回值就是退出码,而普通的函数结束只是当前的函数结束了。
exit(i)直接结束当前的进程 i就是当前进程的退出码等价于在main函数中的return i ,之前应该是在单链表那里见到过
如下代码main函数调用fun函数,在fun函数中就exit(2)了,此时程序直接结束了,并没有打印555 且退出码是2。说明fun中用exit(2)就相当于main函数中的return 2,直接把这个进程给结束了 。
所以退出码的方式有两种
1.main函数的return
2.任意地方exit()
_exit与exit的作用是类似的
他们的区别呢?
来看一下 下图的现象
之前在进度条实现那里知道了缓冲区的问题----打印的内容不会直接显示,而是在缓冲区中,只有刷新缓冲区,内容才会显示出来。而刷新缓冲区可以用 \n、ffuls() 、另外就是在程序结束之后会自动刷新缓冲区。
现象
1.有\n的exit的和_exit的都会显示出来打印的内容(左上和右上图)
2.没有\n的exit可以显示出打印出内容(程序结束后自动刷新缓冲区),而没有\n的_exit的却没有显示出打印的内容。(左下和右下图)
所以根据现象分析,进程如果通过exit退出,进程退出后会自动刷新缓冲区;而通过_exit退出,进程退出后不会自动刷新缓冲区。
原理
exit是C库的,而_exit是系统自己的;exit是调用的c库里的而_exit是系统调用。
exit 是man手册第三页,代表库函数;而_exit在man手册第二页,代表系统调用。
而之前学习知道,进程由操作系统直接管理,能真正结束一个进程的就只有操作系统,其他都是间接封装了。而系统调用更接近操作系统,库函数在系统调用的上层,所以库里面其实是封装的系统调用,所以exit其实底层调用的就是_exit,这些都是我们已知的,这里真正要提的是:
所以缓冲区的刷新不是操作系统内部的缓冲区,而是库缓冲区,是c语言提供的缓冲区。(为之后学习的内容做铺垫)
进程等待
为什么要等待
之前在进程状态知道:如果一个子进程退出,它的父进程没有回收它的信息,子进程会进入僵尸状态 一直不被回收就会造成内存泄漏的问题,而解决方式就是通过进程等待。
1.父进程获取子进程PCB里面的信息来知道获取子进程运行结果的信息。
2. 结束子进程的僵尸状态,解决内存泄漏的问题。
而第二点回收资源是必须的,第一点获取信息不是必要的。
怎么等待
通过wait或waitpid
先来看一下wait
wait等待任意一个退出的子进程 等待成功返回值为回收的子进程的pid,等待失败为-1。
先不管里面的参数,先来见一下
如下代码如果没有wait在子进程结束之后会进入僵尸状态
而如果有wait 在子进程结束之后不会有僵尸状态
如下, 五秒后子进程会结束,此时子进程进入僵尸状态,再过五秒父进程通过wait接收子进程的PCB信息,进而结束了子进程的僵尸状态,并且wait返回值就是子进程的pid
等待失败
父子进程会同时进行,父进程到了wait后会停下来等待,直到子进程退出,那么如果子进程一直不退出呢?或者此时没有子进程正在运行呢?再或者waitpid指定等待pid对应的子进程并不存在呢?这时候就会等待失败
如下 当前进程等待pid为id+1的进程会等待失败 并且此时子进程僵尸状态不好被回收 一直是僵尸状态
waitpid
在返回值上和wait一样 接收成功返回子进程的pid 失败返回-1
接下来看一下参数
1.waitpid第一个参数pid
①-1 代表等待任何子进程就和wait就一样
②>0 等待指定子进程的pid(就可以指定等待的子进程了)
<-1及等于0的情况这里不管
2.输出型参数
wait的参数和wait_pid第二个参数为输出型参数为了让父进程知道子进程的任务完成情况(进程退出的三种情况) 。
如下代码及结果,我们观察到该值并不是我们自己设置的子进程的退出码1,而是256,说明这个输出型参数的信息并不单纯的就是退出码。
这个输出型参数参数类似于位图,一个int有32个比特位,高十六位不管,后十六位中的前八位才是真正的退出码,最后八位的第一位代表core dump标志,最后七位代表终止信号,如果不是异常的情况 后八位正常都是0。
所以如果程序没有异常,我们想拿到真正的退出码 可以通过(wai>>8)&(0xFF)(0xFF只有最后的八位为1,其他为都为0) 来获取退出码。
所以命令行输入命令 其实就是bash进程通过这样wait的方式得到命令的退出码 echo $?拿到
但是异常呢?
之前知道kill -l 可以把所有的信号 打印出来 但是这些数字加字符串的方式其实就是宏的方式,这些数字就代表着对应的字符串。
异常了一般就是收到了异常信号,后七位代表的信号,如果都为0,就代表无异常发生。一旦后七位不为0,说明接收到了异常信号,那么程序发生了异常,此时的退出码无意义。
wai>>0x7F得到wai后七位的值 也就是信号的值
是怎么通过wait或者waitpid怎做到的
进程之间具有独立性,父进程得到子进程的信息是用过系统调用waitpid 那么它是从哪里拿到的?
如上,信号及退出码存储在子进程的task_struct中,系统调用从子进程的task_struct中把这两个信息拿到,然后通过位运算的操作,把它转换为这样位图的方式,然后通过输出型参数的方式把这个值给了父进程中的变量sta 。
所以僵尸状态作用就是如此,把信息存在PCB中,由父进程通过系统调用的方式拿到这些信息。
getpid getppid也是类似的方法
上面我们是通过位操作的方式拿到退出信息的,但是其实系统提供了宏来帮我们能直接拿到退出信息。
WIFEXITED(status):判断子进程是否正常退出,正常退出返回真,如果被信号终止返回假。(查看进程 是否是正常退出)
WEXITSTATUS(status): 若 WIFEXITED非零,提取子进程退出码。(查看进程 的退出码)
所以可以写如下的代码,先判断父进程是否接受到子进程,接收到后判断是否正常退出,如果正常退出再打印其退出码,这样没接收到子进程、非正常退出,正常退出的情况都可以处理。
3.第三个参数 options 阻塞控制
里面值主要关注下面的NOHANG
阻塞于非阻塞区别
之前第三个参数为NULL时候父进程等待子进程时候就一直处于阻塞状态,接收成功返回子进程的pid,失败返回-1,父进程等待子进程期间它不会做其他的事情。
第三个参数为NOHANG后此时父进程处于非阻塞状态,返回的参数大于0代表等待结束,等于0这一轮调用结束,子进程并未退出,小于0等待失败。 所以就可以用下面这样的方式来让父进程等待其他可以做其他事情。
while(1) { if(id>0) { printf("等待成功\n"); break; } else if(id==0) { printf("这一轮等待结束\n"); //等待期间父进程做的事情 } else { printf("等待失败\n"); break; } }