目录
1. 程序地址空间
1.1 内存管理和进程管理
我们可以看到的所有地址都是虚拟地址,平时写的C/C++,用的指针,指针里面的地址,全部都不是物理地址。
1.2 子进程的进程管理与内存管理
- 写时拷贝是由操作系统自动完成的,操作系统重新开辟空间,但是在这个过程中,页表左侧的虚拟地址是0感知的,一直都是不变的。
- 页表的
rw
表示该数据的读写属性,即确定了数据是否能修改。 - 子进程的页表是由父进程拷贝而来,但不同的是,不管父进程
rw
那一栏是什么,子进程页表的rw
那一栏都是r
。当子进程要修改数据时,操作系统会辨别数据是否能修改,若能修改,那操作系统会重新开辟空间,进行写时拷贝,并修改rw
属性
1.3 页表
什么是页表?
页表是一种数据结构,用于管理虚拟内存和物理内存之间的映射关系。在操作系统中,当一个程序需要访问内存时,它会先访问虚拟内存,然后再通过页表将虚拟地址映射到物理内存中的实际地址。
页表的作用是什么?
- 让进程以统一的视角看待内存。
- 增加进程虚拟地址空间可以让我们访问内存的时候,增加一个转换的过程,在这个转化的过程中,可以对我们的寻址请求进行审查,所以一旦异常访问,直接拦截,该请求不会到达物理内存,保护物理内存
- 因为有地址空间和页表的存在,将进程管理模块,和内存管理模块进行解耦合!
页表地址:
页表地址也属于进程上下文,进程运行时,被放在了寄存器了。
1.4 大文件的惰性加载
- 概况程序在计算机如何运行:本身程序是在硬盘上,需要把程序加载进内存,然后由CPU去执行。
- 操作系统对大文件可以实现分批加载,意思就是说,先将大文件放在硬盘里,cpu运行时,需要哪部分数据或代码再把哪部分数据或代码加载到内存里,cpu用完后,再把在内存中的这部分数据或代码释放在内存中释放掉。
缺页中断
:当cpu需要某数据或代码时,会去进程地址空间查找虚拟地址,再通过页表查看对应代码或数据有没有加载到内存,如果有,就通过物理地址在内存中查找,如果没有,就在将数据或代码从硬盘加载到内存。当没有被加载的情况,就叫做缺页中断- 那么进程在被创建的时候,是先创建内核数据结构呢? 先加载对应的可执行程序呢?---------先创建内核数据结构。
2. 进程终止
2.1 进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
- 对于情况 1 和 2 :统一会采用进程的退出码来进行判定结果的正确性!
- 退出码----->return 的不同的返回值数字。
- main函数 的返回值,本质表示:进程运行完成时是否是正确的结果,如果不是,可以用不同的数字,表示不同的出错原因!
- 情况3:本质可能就是代码没有跑完,该情况下,进程的退出码无意义
2.2 进程常见退出方法
正常终止:
- 从main返回
- 调用exit
- _exit
可以通过 echo $? 查看上个进程的退出码:
2.2.1 exit 和 return
exit 和 return
区别:exit
在任意地方被调用,都表示调用进程直接退出,而 return
只表示当前函数返。
2.2.2 _exit 和 exit
_exit
函数和 exit
函数:
_exit
:系统调用接口,不刷新缓存区
exit
:库函数调用接口,刷新缓存区
3. 进程等待
3.1 含义
通过系统调用wait/waitpid,来进行对子进程进行状态检测与回收的功能
具体方法:父进程通过调用wait/waitpid进行僵尸进程的回收问题。
3.2 进程等待必要性
- 父进程如果不管不顾,不进行进程等待,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
- 有时父进程需要知道,父进程派给子进程的任务完成的如何。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
3.3 进程等待的方法
3.3.1 wait
pid_t wait(int*status);
头文件:#include<sys/types.h>
#include<sys/wait.h>返回值:
成功返回被等待进程:pid
,失败返回:-1
。参数:
输出型参数,status 获取子进程退出状态,不关心则可以设置成为 NULL
3.3.2 waitpid
- pid_ t waitpid(pid_t pid, int *status, int options);
- 返回值:
- 成功:当正常返回的时候waitpid返回收集到的
子进程ID
;- 失败:如果调用中出错,则返回
-1
,这时 errno 会被设置成相应的值以指示错误所在;- 子进程未僵尸:
如果设置了选项WNOHANG
,而调用中waitpid发现没有已退出的子进程可收集,则返回0
;- 参数:
- pid:
- Pid= -1,等待任一个子进程。与wait等效。
- Pid > 0,等待其进程ID与pid相等的子进程。
- status:
- WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
- 如果调用时,status 传递的是 NULL,表示不关心子进程的退出状态信息
- options:
- WNOHANG : 若
函数调用时
,options == WNOHANG
----------那么,若pid指定的子进程没有结束,则waitpid()函数返回 0 ,不予以等待
;若正常结束,则返回该子进程的ID;若调用失败,则返回 -1。- 其他:若函数调用时,options 为其他值--------那么,若pid指定的子进程没有结束,则waitpid() 函数会一直等待;若正常结束,则返回该子进程的ID;若调用失败,则返回 -1。
注意:
- waitpid() 函数调用失败,可能是不存在 pid为 参数pid 的进程。
- 如果子进程不退出,父进程调用了 wait 和 waitpid 函数的时候,且函数没有返回,这种情况叫做阻塞状态!
3.3.3 status 的切割
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
- status不能简单的当作整形来看待,且只有 status 的低16比特位有意义,status可以当作位图来看待:
3.4 进程等待的过程
4. 进程程序替换
4.1 替换原理
- 当进程调用一种 exec 函数时,该进程的用户
空间代码和数据
完全被新程序替换,页表也会更新,从新程序的启动例程开始执行,新程序之后的旧程序不会执行了
。 - 调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
4.2 替换函数
- 其实有六种以exec开头的函数,统称exec函数。
- exec函数又分为系统调用函数和库调用函数。5个库调用函数 到最后其实也是转变为 那一个库调用函数。
- 库调用:
#include <unistd.h>`
int execl(const char *path, const char *arg, ...);
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[]);
4.3 替换函数返回值
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1
- 所以exec函数
只有出错的返回值而没有成功的返回值。
4.4 函数参数理解
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH,不用带路径
- e(env) : 表示自己维护环境变量
exec调用举例如下:
#include <unistd.h>
int main()
{
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使用环境变量PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,需要自己组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
execv("/bin/ps", argv);
// 带p的,可以使用环境变量PATH,无需写全路径
execvp("ps", argv);
// 带e的,需要自己组装环境变量
execve("/bin/ps", argv, envp);
exit(0);
}
5. 做一个简易的shell
代码放在gitee里了:shell代码链接