进程创建
- pid_t fork(void); – 创建一个进程 — 父子进程数据独有,代码共享
写时拷贝技术: 子进程复制了父进程,一开始与父进程指向同一块物理内存;因此看起来父子进程完全相同,但是进程之间具有独立性,意味着当这块物理内存中数据即将发生改变时会重新给子进程开辟物理内存,将数据拷贝过来,因为子进程应该有自己的数据
代码共享: 因为代码段是共有的
进程创建时采用写时拷贝技术,目的是提高进程的创建效率,按理说每个进程应该具有自己独立的内存空间,但是创建子进程时直接开辟空间和拷贝数据,将会比较缓慢,并且拷贝过来的数据子进程从来都不用
子进程创建pcb的时候,就会创建页表,虚拟地址空间……然后从父进程pcb中去拷贝这些数据过来
- pid_t vfork(void):创建一个子进程,并且会阻塞父进程,直到子进程 exit 退出或者程序替换之后,父进程才会运行
vfork创建子进程效率比较高,因为vfork创建子进程后,父子进程公用同一块虚拟地址空间,意味着公用代码段数据段,这样如果父子进程同时运行会造成栈混乱,因此必须子进程先运行,父进程阻塞,直到子进程退出,所有函数出栈,vofork创建的子进程是不能使用main中return退出的,因为 return 退出会释放进程资源。
进程终止
- main 函数中 return,退出时会刷新缓冲区
- void exit(int status) — 库函数,退出调用进程,将status作为返回值给父进程
- void _exit(int status) — 系统调用接口,退出调用进程,将status 返回给父进程
exit / return 退出时都会刷新缓冲区;_exit退出时直接释放资源,不刷新缓冲区
exit 和 return 的区别:return 之后再main函数中的时候才会退出进程,而exit是在任意位置调用都会退出调用进程
进程等待
作用: 父进程等待子进程的退出,获取子进程的返回值,避免产生僵尸进程
-
pid_t wait(int *status) — 阻塞等待任意一个子进程退出,获取子进程的返回值放到 status 指向的空间中,并且释放子进程资源,返回退出的子进程pid
阻塞: 为了完成某个功能发起一个调用,若当前不具备完成功能的条件,则调用不返回一直等待
非阻塞: 为了完成某个功能发起一个调用,若当前不具备完成某个功能的条件,则立即报错;非阻塞操作通常需要循环操作 -
pid_t waitpid(pid_t pid,int *status,int options);
pid:若 == -1,则表示等待任意一个子进程退出,若是 >0,则表示等待指定子进程退出
status:输出型参数,传入一个 int 空间的首地址,获取退出的子进程返回值
options:选项参数,0 – 则表示默认阻塞等待;WNOHANG – 将waitpid设置为非阻塞 – 没有子进程已经退出的话就立即报错返回
返回值:若等待到了子进程退出则返回子进程的pid;若有子进程但是没有退出则返回0;出错返回 -1
wait(status)==waitpid(-1,status,0)
status
进程在终止时并不一定时正常退出,若子进程是异常退出,获取这个返回值则毫无意义,因此获取一个进程返回值的前提是这个进程是正常退出的!
如何判断子进程是否正常退出
在status这个变量中,低 8 位保存了一些特殊数据
core dump:核心转出 – 程序异常退出时,则保存程序的运行信息,便于时候调试
信号:通知进程发生了某个事件,中断进程当前的操作,去处理这个事件,而操作系统中的信号的体现实际上是一个数字,某个数字表示某个异常时间,程序因为异常退出的时候,则会将这个异常退出时间的信号值放在 低7位
结论:若status低 7位位 0则表示程序正常退出,否则百世程序异常退出
获取子进程的返回值: 0x7f & status
WIFEXITED(status) – 用于根据status判断子进程是否正常退出
WEXITSTATUS(status) – 从status 中取出子进程退出返回值
程序替换
创建一个子进程的目的是什么?
- 干着跟父进程相同的事情,分摊压力
- 让子进程干其他事情,让子进程背锅
缺陷: 程序的耦合度非常强,因为所有功能代码都是在当前程序中编写的 – 如果想改变子进程的功能处理流程,需要修改整个程序代码,然后重新编译,这样一来程序就会非常大
程序替换: 替换一个进程正在调度的程序信息(进程是pcb,负责调度管理一个程序的运行,运行的数据和代码都在内存中),将另一个程序加载到内存中,然后让原有的pcb不在调度原程序,而是去调度这个新的程序 。本质来说就是替换一个pcb在内存中对应的代码和数据(加载另一个程序到内存中,然后更新页表信息,初始化虚拟地址空间),这个进程的pcb将从头重新开始调度新的程序运行。
如何实现程序替换:
-
int execl(const char *path,const char *arg,…);
path 带有路径的新程序名称,就是使用这个程序替换进程正在调度运行的程序, arg 将新程序的运行参数,通过不定参的形式传进新程序,以NULL结尾 -
int execlp(const char *file,const char *arg,…);
-
int execle(const char *path,const char *arg,…,char *const envp[]);
-
int execv(const char *path,char *const argv[]);
-
int execvp(const char *file,char *const argv[]);
-
int execve(const char *path,char *const argv[],char *const envp[]);
l 和 v 的区别:在于程序运行参数的赋予方式不同,l 通过不定参完成 / v 通过字符串指针数组进行赋予
有没有 p 的区别:在于第一个参数执行新程序时,是否需要带路径,有 p 则可以不带路径,默认回去 PATH 环境变量指定路径下查找
有没有 e 的区别:在于这个进程的环境变量是否需要重新初始化;有 e 则表示初始化,若没有 e 则表示使用默认的环境变量
程序替换之后,当前进程运行完替换后的程序就会退出,并不会回去运行原先的程序