进程控制:创建、终止、等待、替换
一、创建
如何创建一个进程??
pid_fork(void); 写时拷贝的方式创建一个新的子进程出来,父子进程代码共享数据独有
pid_vfork(void); 创建一个新的子进程,父子进程共用同一个虚拟地址空间(不常用)
因为vfork创建子进程后,父子进程共用虚拟地址空间,因此使用的是同一个栈,因此一个进程对数据做的改变,也会体会到另一方。但是这样就会有一个大隐患,调用栈混乱,因此vfork创建子进程后,父进程是要阻塞的,直到子进程exit退出或者进行了程序替换,创建了自己的地址空间,以及各项数据。
vfork是早期为了提高进程创建效率而提出来的,但是在fork实现了写时拷贝技术之后就很少用了。
二、终止
如何退出一个程序??
退出场景:正常退出(符合预期退出,不符合预期退出),异常退出
int main(){
int a;
char *null_ptr = NULL;
scanf("%d",&a);
if(a > 10){
printf("%s",null_ptr);
return 1;
}else{return 0;}
}
获取上一次系统调用接口的错误原因的接口:
perror("fork error");//获取上一步系统调用接口的错误原因
printf("fork error:%s\n",strerror(errno));//这个就等价于上面的perror
void perror(char *dsc);
char *strerror(int errno);
int errno;//全局变量,用于存放上一次系统调用的错误原因代码
如何退出一个进程:
在main函数中return;(注意:return只有在main中使用的时候是退出程序,否则在其他函数中只能表示退出对应的函数)
库函数:
void exit(int return_val);
可以在程序代码的任意位置进行调用,用于退出程序的运行,退出前会刷新缓存区数据到文件
系统调用接口:
void ——exit(int return_val);
可以在程序代码的任意位置进行调用,用于退出程序的运行,直接退出释放资源
库函数和系统调用接口是什么关系??
库函数是大佬们针对典型应用功能对系统调用接口进行封装,便于使用。
缓冲区和缓存的区别??
buff-缓冲区:相对于文件来说就是数据写入文件前,先到缓冲区中,积累到成为大数据一次性刷新缓冲区写入文件中,减少io次数
cache-缓存:相对于文件来说从文件中读取数据,一次性拿出的是一块磁盘块的数据,放到内存中,下次读取数据的时候先从缓存中对比
三、等待
父进程创建子进程后,等待子进程退出,获取子进程的退出返回值,释放子进程的资源,避免僵尸进程的产生。
僵尸进程的产生:子进程先于父进程退出,为了保存退出原因,因此没有完全释放资源,而是通知父进程,但是父进程没有进行处理。(因为父进程没有感受到子进程退出的通知,因此干脆让父进程创建子进程之后一直等着,避免因为没有收到通知而错过)
等待操作:
#include<sys/wait.h>
pid_t wait(int *status);//阻塞等待任意一个子进程退出,使用status作为输出参数获取子进程的退出返回值
成功则返回处理的这个退出的在机场的进程ID;失败返回-1
pid_t waitpid(pid_t pid,int *status,int options);
pid_t pid:指定要等待的子进程的ID;-1表示等待任意一个子进程
int *status:输出,用于向外界返回退出子进程的返回值
int options:操作选项,0——阻塞等待;WNOHANG——如果没有子进程已经退出,则waitpid接口则直接返回,不阻塞。(控制是否阻塞等待)
返回值:成功等待子进程退出,则返回子进程ID;如果当前没有子进程返回0;出错-1
wai、waitpid接口都是等待一个子进程退出,那如果等待前就有子进程退出,那么会被处理么?
答:会,如果调用wait&waitpid的时候已经有子进程退出,则直接进行处理,处理完毕接口返回
waitpid的非阻塞使用,一定要使用循环操作,否则操作就有可能没完成
进程的退出返回值,实际上只用了一个字节进行保存,也就是说只能返回0~255之间的数据,再多就会截断
进程有俩种退出方式:
正常退出:exit,或者return
异常退出:中途崩溃
一个进程如果异常退出,是没有返回值的,这时候保存退出返回值的空间中的数据就是不可知的,获取到的退出返回值也是没有意义的。
在status获取的数据中,低7位是进程的异常退出码,如果不为0就表示进程是异常退出的,因此在进程获取返回值之前,应该先获取异常退出码,看看进程是否异常退出,非异常退出才会去获取退出返回值
异常退出码的获取:4字节中低7位数据,status&0x7f(0111 1111)
进程退出返回值获取:4字节中低16位中的高8位(status>>8)&0xff
WIFEXITED(status)等价于(exit_code & 0x7f) == 0正常终止返回true
WEXITSTATUS(status)等价于0xff &(exit_code>>8)
四、替换
概念
程序替换:将一个新的程序加载到内存中,然后改变一个进程的页表映射信息,将其更改到新的程序的指令和数据上,这时候当前的pcb管理的就不是原来的程序运行了,而是一个新的程序。
代码:
#include<stdio.h>
//程序是有运行参数和运行变量的
//程序的运行参数在赋予的时候,是运行程序的时候以空格间隔在程序后 ./main -l -a -c
//程序的运行参数,就会被保存在argc这个字符数组中
//程序的环境变量,就保存在env这个字符数组中,或者有个全局变量environ
//程序的运行参数,和环境变量都可以从外界传入,这样的话对于程序的运行就会非常灵活,比如ls程序的-l-a
int main(int argc , char *argv[] , char *env[])
{
extern char **environ;
printf("程序的参数个数:%d\n" , argc);
printf("程序的运行参数:\n");
for(int i = 0;argv[i] != NULL;i++){
printf("\t%s\n" , argv[i]);
}
printf("程序的环境变量:\n");
for(int i = 0;environ[i] != NULL;i++){
printf("\t%s\n" , environ[i]);
}
return 0;
}
execl函数簇(了解)
extern char **environ;
int execve(const char *path , char *const argv[] , char *const envp[]);系统调用接口
const char *path:要加载替换的新的程序文件路径名称
char *const argv[]:程序的运行参数
char *const envp[]:进程的环境变量
int execl(const char *path , const char *arg , ....);
path:带路径的新名称,arg是参数,不定参
execl("./example" , "./example" , "-a" , "-l" , NULL);
int execlp(const char *file , const char *arg , ...);
file:不带路径的新名称,在path环境变量指定的文件路径下找程序,常用于指令程序的替换
execlp("ls" , "ls" , "-l" , NULL);
int execlp(const char *path , const char *arg , .... , char *const envp[]);
path:带路径的新名称;arg是参数,不定参;envp是环境变量
execlp("./example" , "./example" , "-a" , "-l" , NULL,environ);
int execv(const char *path , char *const argv[]);
与execl的区别在于参数不是一个一个给,是组织成数组给
int execvp(const char *file, char *const argv[]);
在指定路径下找程序,参数通过数组给
l和v的区别在于参数是一个一个给,还是组织成数组给
有没有p的区别,在于是否会默认到path环境变量指定的路径下找程序
有没有e的区别,在于是否需要自定义变量
实现简单的minishell:
shell:命令行解释器,功能就是捕捉用户输入,然后根据输入的信息指令命令程序
[username@hostname] $ ls -a -l
1.捕捉用户输入
gest(char *buf); 从标准输入读取一行数据" ls -a -l "
2.字符串解析
字符串解析,得到命令名称和运行参数" ls -a -l " -> argv[]={"ls" "-a" "-l"}
3.创建子进程
给子进程进行程序替换,并设置运行参数 execvp(argv[0] , argv);
注意:不能直接对shell替换,因为替换后运行完新程序进程就退出了,如果替换了shell运行完就会退出,并且万一要是某个指令崩溃了,shell就崩溃了,因此要创建子进程让子进程运行指令
4.进程等待
防止子进程运行完指令退出后成为僵尸进程
5.循环到第一步,重新开始捕捉输入
//minishell代码实现: