目录
(3)冲刷缓冲区 & 关闭流(标准输入,标准输出,标准错误)
3.1.2父进程进行进程等待,等待子进程退出后,回收子进程的退出状态信息,防止子进程变成僵尸进程
1.进程创建
1.1回顾fork
让正在运行的进程创建出来一个子进程。它从已存在的进程中创建一个新进程。新进程为子进程,原进程为父进程。
pid_t fork(void):
返回值:创建成功:>0:返回值大于0,返回给父进程,父进程的fork的返回值。
==0:返回值等于0,返回给子进程,子进程的fork的返回值。
创建失败:返回-1
1.2fork内部完成的事情
创建子进程,子进程拷贝父进程的PCB
- 分配新的内存块和内核数据结构(task_struct)给子程序
- 将父进程部分结构内容拷贝至子进程
- 添加子进程到系统进程列表中,添加到双向链表当中
- fork返回,开始调度器(操作系统开始调度)调度
1.3用户空间 & 内核空间
- 内核空间:Linux操作系统和驱动程序运行在内核空间,换句话说,系统调用的函数都是在内核空间运行的,因为是操作系统提供的函数。
- 用户空间:应用程序都是运行在用户空间的,换句话说,我们程序员自己写的代码都是运行在用户空间的,但是,当程序员写的代码调用了系统调用的函数,则会切换到内核空间执行,执行完毕后再返回到用户空间继续执行用户的代码。
1.4写时拷贝
父进程创建出来,子进程的PCB拷贝父进程,页表也是拷贝父进程的。在最初的时候,同一个变量的虚拟地址和物理地址的映射是一样的(因为页表也是拷贝父进程的)也就是说,操作系统并没有给子进程当中的变量在物理内存当中分配空间进行存储,子进程的变量还是原来父进程的物理变量当中的内容。
当发生改变时:才以写时拷贝的方式进行拷贝一份,此时父子进程通过各自的页表指向不同的物理地址。
不发生改变是:父子进程共享同一个数据
1.5回顾fork创建子进程的一些特性
- 父子进程是独立运行的,互不干扰,各自有各自的进程虚拟地址空间和页表,数据不会窜。
- 父子进程是抢占式执行的,谁先执行谁后执行是由操作系统调度决定的(也和自身准备的情况相关)
- 子进程是从fork之后开始执行(程序计数器 + 上下文指针)
- 代码共享,数据独有
1.6fork的一些用法
守护进程:父进程创建子进程,让子进程执行真正的业务(进程程序替换),父进程负责守护子进程。当子进程在执行业务的时候意外“挂掉了”,父进程负责重新启动子进程,让子进程继续提供服务。
一个父进程希望复制自己,使父子进程同时执行不同的代码段,例如,父进程等待客户请求,生成子进程来处理请求。
2.进程终止
2.1进程终止的场景
- 代码运行结束:结果正确
- 代码运行结束:结果错误
- 代码异常终止
2.2正常终止(可以通过echo $?查看进程的退出码)
- 从main函数当中的return返回,注意:并不知任何函数的return,都能结束进程,而是main函数的return才能结束进程。
- 调用exit函数(库函数),void exit(int status),status:进程退出时候的退出码,函数的作用就是:谁调用终止谁
- 调用_exit函数(系统调用函数),void _exit(int status),status:进程退出时候的退出码,函数的作用就是,终止一个进程,谁调用终止谁。
2.3异常终止
- 程序崩溃
- 访问空指针
- ctrl + c
2.4exit和_exit的区别
(1)如图:
exit函数比_exit函数多执行了两个步骤
>1.执行用户自定义的清理函数
>2.冲刷缓冲区,关闭流等
>3.终止进程(_exit)
exit函数的实现当中其实也是调用了_exit函数
(2)执行用户自定义的清理函数
回调函数:在代码当中注册一个函数,在特定的时候执行;
注册回调的函数
回调函数
#include<stdlib.h>
int atexit(void (*function)(void)) ; :register a function to be called at normal process termination
注册一个函数,在进程终止的时候进行调用。
代码实现:
(3)冲刷缓冲区 & 关闭流(标准输入,标准输出,标准错误)
这个缓冲区是C标准库定义的,并不是内核。
建立缓冲区是为了减少IO次数,因为IO操作比较的耗费时间。所以,就定义了缓冲区,当触发刷新缓冲区的条件的之后,缓冲区的内容才会继续IO操作(打印到屏幕,将内容写到文件当中,有时可能是从文件当中读)
2.5刷新缓冲区的方式
- exit
- main函数的return
- fflush
- \n
2.6扩展:缓冲方式
全缓冲:当缓冲区写满的时候,才进行IO
行缓冲:在这种情况下,当输入和输出中遇到换行符的时候,标准I/O库才执行I/O操作。
不缓冲:不带缓冲。标准I/O库不对字符进行缓冲。
3进程等待
3.1进程等待的必要性
3.1.1代码回顾僵尸进程
子进程先于父进程退出,父进程没有回收子进程的退出状态信息,则子进程会变成僵尸进程。
3.1.2父进程进行进程等待,等待子进程退出后,回收子进程的退出状态信息,防止子进程变成僵尸进程
在父进程的分支逻辑中调用进程等待的函数,进行等待子程序退出,回收子程序的退出状态信息。(这是解决僵尸进程的最优方案)
3.2wait函数
3.2.1wait函数的原型
pid_t wait(int *status); 等待任意子程序
作用:等待退出的子程序,引申含义就是,回收退出的子进程的退出状态信息。
返回值:成功:被等待到的进程PID(大于0)成功返回被等待进程PID
失败:返回-1
参数:出参
输出型参数,获取子进程的退出状态,不关心则可以设置为NULL
父进程调用了一个wait函数,在子进程还没有退出之前,wait函数没有执行完毕,也就意味着父进程的代码还没有执行完毕
函数特性:谁调用,谁等待。直到等待的子进程退出,把这种称之为阻塞,换句话说:当发起阻塞调用之后,如果要完成函数功能,需要等待某种资源,则会一直等待,知道资源的到来,完成某种函数的功能之后,函数才能返回。
细分下:如果发起阻塞调用的时候,资源在,无需等待,直到执行函数功能之后返回。
如果发起阻塞调用的时候,需要等待资源,则需要等待资源到来后,执行函数功能后返回。
非阻塞:当调用一个非阻塞函数的时候,函数会判断资源是否准备好。如果准备好,执行函数功能后返回。如果没有准备好,则函数报错返回。(需要知道函数的功能没有完成)
3.2.2参数status的含义
int status:只用到了低两个字节,分两种情况:
子进程正常退出:如果传递status的值,则会获取到子进程的退出状态
子进程异常退出:如果传递status的值,则会获取coredump标志位+推出信号
如何判断子程序是正常退出还是异常退出?
wait的返回值:
大于0并且退出信号没有被设置(==0):正常退出
大于0并且退出信号被设置(大于0的数,有值):异常退出的场景
3.2.3代码验证
3.3waitpid函数
3.3.1函数原型
pid_t waitpid(pid_t pid,int *status,int options);
pid: pid = -1,等待任意一个子进程,与wait等效。
pid > 0,等待其进程号ID与pid相同的子进程。
status:子进程的退出信息,同wait
options:WNOHANG:设置waitpid为非阻塞状态;若pid指定的子进程没有结束,则waitpid()函数返回0,不予等待。若正常结束,则返回该子进程的ID。
返回值:
当正常返回时候waitpid返回收集到的子进程的ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可以回收,则返回0;
如果调用中出错,则返回-1,这是errno会被设置成相应的值以指示错误所在;
3.3.2代码验证非阻塞的特性
4.进程程序替换
4.0为啥需要进程程序替换
本质:想让进程去执行不同的代码
因为父进程创建出来的子进程和父进程拥有相同的代码,所以,子进程看到的代码和父进程是一样的。当我们想要让子进程执行不同的程序的时候,就需要让子进程调用进程程序替换的接口,从而让子进程执行不同的代码。
4.1原理
替换进程的代码段和数据段,更新堆栈。
4.2exec函数簇
int execve(const char *path, char *const argv[], char *const envp[]);
参数: