写时拷贝
进程的创建:
pit_t fork(void)
写时拷贝机制:
代码共享、数据独有
子进程创建后,给子进程重新开辟,把父进程的那些数据拷贝过来,这样才能保持独立,各有各的空间,但是如果子进程中根本就不访问这些数据,则空间开辟以及数据拷贝白费了,浪费了时间,浪费了内存。
为了独立,又为了提高效率,避免资源浪费,因此采用了写时拷贝技术:
思想:子进程创建流程:刚创建出来的时候,让子进程与父进程一样,映射一块物理内存,但是如果某块内存中的数据即将发生概念(任意一方要修改),则给子进程针对这一块重新开辟空间,拷贝数据过去。
进程终止
man手册:-1:命令,-2:系统调用接口,-3:库函数接口
进程终止:如何退出一个进程
1.在main函数中return (return只有在main函数中才是退出程序的运行)
2.库函数:void exit(int retval),可以在任意位置调用退出程序的运行
3.系统调用接口:void_exit(int status);可以在任意位置调用,退出程序的运行
库函数与系统调用接口的关系:库函数封装了系统调用
问题:exit与_exit的区别?
每次打印都要操作设备,如果有大量的小数据要打印或写入文件意味着每次写入都有操作,导致效率很低。
因此引入 :“缓冲区”——作为中间的数据缓冲,可以将多次小数据积累成一个大数据一次性操作完成。标准的输出设备有一个特性:换行刷新缓冲区
因此库函数exit与系统调用_exit的区别在于:退出程序前是否会刷新缓冲区
exit与return在退出程序前都会刷新缓冲区,将还没有写入文件的数据写入到文件中,而_exit调用直接退出,不会刷新缓冲区,而是直接释放资源(有可能存在缓冲区中的数据丢失)
return后边的数字和exit的参数status的作用:设置进程的退出码
退出码只保留低八位(进程退出码尽量保持在0-255之间)
进程等待
进程等待:等待子进程等待,获取子进程退出码,释放子进程资源,避免子进程成为僵尸进程。
僵尸进程:子进程先于父进程退出,为了保存退出码,没有完全释放资源。
虽然不关心子进程的退出码,但是依然需要进行进程等待,因为要避免这个子进程成为僵尸进程。
那么如何进行进程等待?
int wait ( int * status);
功能:阻塞等待 任意一个 子进程的退出。
就意味着当前有子进程且都没有退出就会阻塞进程一直等待
阻塞等待:为了完成一个功能发起了一个调用,功能不能完成则一直等待
参数:int *status ——一个int整形空间地址,用于存放退出码
返回值:成功则返回处理的后的子进程的pid,失败则返回 -1(比如没有子进程);
int waitpid (pid_t pid, int *status,int option);
功能:等待指定的子进程,以及可以进行非阻塞等待;
参数:pid_t pid 用于指定等待的子进程pid,如果为-1则表示等待任意一个子进程。
int *status 整形空间地址用于获取退出码
option:设置阻塞标志0 - 表示阻塞等待
返回值:大于零表示处理的退出子进程pid,等于0表示当前没有子进程退出(非阻塞,小于零表示出错了);
非阻塞等待:当完成了一个功能,我们发起了一个调用,如果功能不能立即完成则接口立即报错返回。
进程等待操作(wait操作):
waitpid操作:
非阻塞的用法 :需要循环操作进行,不循环的话有可能这个操作根本就没有进行,如下:
但是如果在上述所说的1s之内就退出了子进程就退出了,那这1s秒钟没处理的时候就是僵尸进程。
./main之后:
问题:在进程等待之前,已经退出的子进程怎么办?
wait/waitpid并不是只处理刚退出的子进程,而是只要存在子进程退出,有已经成为僵尸进程的就直接处理返回。
退出码的获取
status在低16位里的高8位是退出码的位置,低7位的位置就是异常信号值,core dump标志;
进程的退出场景:
正常退出 —— return / exit / _exit退出
异常退出 —— 没有运行到正常退出位置中途崩溃了
core dump:核心转储,表示程序异常退出前将自己运行信息保存起来,便于调试
只有程序正常退出,那么进程的退出码获取才有意义;
因此在获取退出码之前,首先应该判断以下程序是否正常退出;
**程序崩溃的本质:**程序运行中发生的异常,都是内核检测到的,当程序异常时候,则系统检测后给进程会发送一个异常信号,表示一个异常事件。
获取异常退出信号值(异常信号=0就表示正常,否则不正常):status &0×7f (与低7位相与操作)
(status >>8)& 0×ff —— 获取退出码
示例:
获取到了退出码:99
程序替换
程序替换:替换一个正在调度管理的程序
通常很少替换当前调度程序,而是创建子进程之后,替换子进程所运行的程序。
fork之后子进程与父进程干的事情相同,可以分摊压力,还有一种就是让子进程干其他事情,干活逻辑都是在if(fork == 0)这个判断中去完成,会导致代码庞大,不灵活。
因此创建子进程之后,根据不同工作任务,将子进程替换成不同程序,则代码模块可以灵活很多。
例如网络服务器:主程序逻辑一致 - 有请求来了,创建进程,根据不同的处理程序即可。
程序替换操作接口:
extern char **environ ; 这是一个全局变量的声明,这个全局变量保存了所有的环境变量。
int execve (char *pathname , char *argv[] , char *env[])
pathname:要替换的新程序的路径名
argv:传递给新的程序的运行参数
env:传递给新的程序的环境变量
返回值:失败返回 -1 ,成功返回0;
shell中会涉及程序运行环境的配置,我们的程序主要是传递数据,环境变量具有进程传递特性,可以通过环境变量给进程传递一些数据。
程序替换函数如果替换成功的话,则这个函数调用之后的代码就不会被执行,因为程序已经被替换成新的程序,而新的程序运行完毕之后,进程就退出了(不会返回回来运行原先的程序)。
替换进程之后,进程ID不变,PCB也不变。
这是一些execve函数的变形,根据具体需要进行查询即可。