进程创建
创建进程的函数接口:
pid_t vfork(); // 创建一个子进程,并且阻塞父进程,避免调用栈混乱(vfork创建出的子进程与父进程共用虚拟地址空间)
pid_t fork(void); // 通过复制父进程创建子进程;父子进程数据独有,代码共享
- 代码共享:主要因为代码是只读的,不能改变的
- 返回值:对父进程来说 返回值是子进程的pid;对子进程来说,返回值是0(可以通过返回值对父子进程进行代码分流,让其各自执行不同的代码段)
写时拷贝技术:子进程创建出来后,各个虚拟地址在物理内存中的指向与父进程完全相同,这是为了提高子进程的创建效率(否则要为子进程开辟物理内存,拷贝数据、更新页表,整个过程比较慢,并且有可能子进程根本不会用这些数据),若某一块物理内存中的数据即将发生改变(某个进程要对这块内存进行写操作),则给子进程相应的数据开辟物理内存,将数据拷贝过来
进程终止(退出进程)
进程退出的场景:正常退出且结果符合预期/正常退出且结果不符合预期/异常退出
进程正常退出的方式:
- 在main函数中return以退出进程:return退出进程时会刷新缓冲区
- 调用系统调用接口
void _exit(int status);
退出调用进程:任意位置都能调用,不限于main函数;status为退出返回值,退出进程时不会刷新缓冲区,直接释放资源 - 调用库函数
void exit(int status);
退出调用进程:任意位置都能调用,不限于main函数;status为退出返回值,退出进程时会刷新缓冲区
异常退出:程序没有运行完毕,而是在运行中因为某些异常直接退出了,这时进程的返回值是不能作为评判结果的,因此在获取返回值之前,应该先判断进程是否是正常退出的
进程等待
概念:等待子进程退出,获取退出子进程返回值,释放子进程资源,避免产生僵尸进程
- 僵尸进程:处于僵死状态的进程。退出了但是资源没有完全释放的进程;子进程先于父进程退出,父进程没有关注子进程的退出状态
- 如何避免僵尸进程的产生:使用进程等待
进程等待接口:
pid_t wait(int *status); // 阻塞等待任意一个子进程的退出,通过status获取子进程的退出返回值,返回退出子进程的pid
pid_t waitpid(pid_t pid,int *status,int options); // 等待指定进程的退出,也可等待任意子进程退出
// 参数:
// pid:如果值为-1表示等待任意一个子进程退出,值>0表示等待指定子进程退出
// status:用于获取子进程的退出返回值
// options:值为0表示阻塞等待子进程退出;值为WNOHANG时,表示将waitpid设置为非阻塞
waitpid(-1,status,0); /*等价于*/ wait(&status);
// 阻塞:为了完成一个功能发起一个调用,若当前不具备完成功能的条件,则一直等待
// 非阻塞:为了完成一个功能,发起调用,若当前不具备完成功能的条件,则立即报错返回
// 非阻塞可以在不满足条件期间做其它事情,对资源的利用率更高,但是通常需要循环判断操作
- wait/waitpid不仅仅是处理刚退出子进程(只处理一个),只要在调用wait的时候,有子进程退出,wait都回去获取返回值,释放资源
- waitpid的返回值:-1表示出错;0表示没有子进程退出;>0表示有子进程退出,退出子进程的pid就是返回值
返回值的获取:
-
子进程的返回值实际上只用了一个字节进行保存(进程的返回值最好不要大于256,否则就会进位),通过wait/waitpid函数中的status获取返回值时,status有4个字节(32位),其中高16位未使用;低16位中的高8位存放返回值,低7位中存放进程异常退出的信号值,中间一位是core dump,返回值放在低16位中的高8位。返回值的获取:(status>>8)&0xff
- core dump:核心转储。程序异常退出时,保存程序的运行信息到一个core文件中,使用这个core文件,便于时候调试;默认关闭
-
如何判断进程是否正常退出:status低7位中存放的是进程异常退出的信号值,因此若status中低7位为0则表示正常退出,否则表示异常退出(status&0x7f)
- WIFEXITED(status)判断进程是否正常退出(调用exit/_exit/main函数中的return)
- WEXITSTATUS(status)获取子进程退出返回值
进程替换
替换一个进程正在运行的程序,让当前进程pcb调度运行管理另一个新的程序
- 将一个新的程序加载到内存中,修改当前进程pcb对应的页表信息、初始化虚拟地址空间,让这个页表映射到新的程序上,这样就实现了不改变pcb(id都不改变),但是运行的程序为新的程序
如何替换?exec函数族:
int execl(const char* path,const char* arg, ...); /*例*/execl("ls","ls","-l","-a",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[]); /*例*/argv[]={"ls","ls","-l","-a",NULL};execv("ls",argv);
int execvp(const char* file,char* const argv[]);
int execve(const char* path,char* const argv[], char* const envp[]);
程序替换首先要告诉shell新的程序在哪里、叫什么名字、程序的运行参数是什么、环境变量有什么
- l和v的区别:程序运行参数赋予方式不同。l逐个通过函数不定参数赋予,v通过字符串指针数组赋予
- 有没有p的区别:新的程序文件是否需要指定路径在哪里。带有p就可以不需要指定路径,shell会默认去PATH环境变量指定的路径下查找
- 有没有e的区别:运行新程序时是否重新初始化环境变量。若没有e,则默认使用父进程已有的环境变量,若有e,则表示自定义