目录
前言
进程控制包括创建、终止、等待、程序替换这四个内容。
进程创建
在linux中创建一个子进程可以用fork函数,原型pid_t fork(void),pid_t是一个类型,fork函数包含在头文件<unistd.h>中。
int main(){
pid_t pid=fork();
if(pid<0){
printf("fork failed!");//创建失败则退出
exit(-1);
}else if(pid==0){
printf("This is a Child Process!");
}else{
printf("This is a Parent Process!");
}
}
用fork创建的子进程与父进程一开始是在同一块地址空间的,但如果子进程或父进程中有改动,则会修改子进程的地址,值得注意的是创建出来的子进程不一定比父进程先运行。
另一个函数vfork,vfork创建的子进程和父进程一直都是同一个地址空间,所以为了避免两个程序同时运行造成栈混乱,该函数创建出子进程后会阻塞父进程,让子进程先运行,结束后父进程再运行。
注意:用fork创建子进程后,子进程会保留父进程的 1、进程上下文 2、进程堆栈 3、内存信息 4、打开的文件描述符 5、信号控制设置 6、进程优先级和进程组号 7、当前工作目录 8、根目录 9、资源限制 10、控制终端等
而子进程独有的:进程号(pid),自己的文件描述符和目录流的拷贝,不继承父进程的进程正文(text),数据和其他锁定内存(memory locks),不继承异步输入和输出
进程终止
程序的退出最主要用的是exit和main函数里的return,注意return只有在main函数里时才是退出进程,且main函数中的返回值就是进程的返回值(返回值的意义在于了解程序运行情况),如果return在其他函数里则只是退出函数,而exit不论在哪个位置都是直接退出进程。
进程的退出分三种情况:
①正常退出且任务完成
②正常退出但任务未完成
③异常退出
如果系统因为调用失败而终止退出,则可以用void perror (const char *msg)函数来打印上一步系统调用接口失败的原因。
const char* strerror (int errno) 函数可根据错误编号返回对应的字符串错误原因。
在pcb中有个全局变量int errno 用于记录每个系统调用接口情况,在执行返回时都会重置errno,返回0则是成功的,返回其他值则异常。所以可以用<string.h>中的extern int errno 来查看这个全局变量。
说到这里再额外提一下exit和_exit的区别:一个是库函数void exit(int reval),一个是系统调用接口void _exit(int reval)。这两的作用都是让进程退出,不同点在于exit会自动刷新缓冲区,而_exit不会刷新,这就导致在_exit之后的,本来该打印在终端的数据没有被打印。(打印数据时计算机并不是一个一个打印的,而是先将数据放入缓冲区,等到刷新缓冲区时再把里面的数据打印到终端,我们熟悉的\n换行就是刷新缓冲区的一种方式)。
int main(){
printf("你好\n");
printf("Hellow");
_exit(0);
}
可以看到“你好”被打印出来了,但Hellow没有,因为_exit不会刷新缓冲区,\n会刷新缓冲区。
一些相关指令
void perror(const char *msg) 用于打印上一步系统调用接口失败的原因
const char* strerror (int errno) 根据错误编号返回对应的字符串错误原因
extern int errno 在pcb中有个全局变量errno,每个系统调用接口在执行返回时都会重置errno(错误编号),如果返回0则执行是成功的
进程等待
父进程创建子进程后等待子进程退出,以获取退出码,及时释放资源,避免出现僵尸进程。
这里先说一下阻塞和非阻塞:
①阻塞接口:为了完成一个功能而发起一个调用,若调用完成的条件不成立则会一直等待
②非阻塞接口:为了完成一个功能而发起一个调用,若调用完成的条件不成立则会报错
> pid_t wait (int *status)是一个阻塞接口,其功能是等待任意一个子进程退出,获取其返回值,释放资源,其中status指针用于存放子进程的返回值,wait函数执行成功时返回子进程的pid,失败则返回-1。此外该函数不仅可以等待任意一个子进程退出,也可以指定某个子进程。
wait(&status)里的status是用于存放子进程返回值的,而wait函数自身的返回值则是成功时返回子进程的pid,失败返回-1;
> pid_t waitpid (pid_t pid,int *status,int options) 该接口即可阻塞也可非阻塞,如果是非阻塞则通常配合循环进行操作,当条件不成立时会先去执行其他操作,直到条件成立或循环退出。
waitpid函数里的pid如果大于0则表示等待指定的子进程退出,pid = -1 则表示等待任意子进程退出。
int main(){
pid_t pid=fork();
int count=0;
int status=-1;
if(pid<0){
printf("fork failed!\n");
exit(1);
}else if(pid==0){
printf("This is a child process! pid=%d\n",getpid());
for (int i=0;i<5;++i){
count++;
sleep(1);
printf("count=%d\n",count);
}
exit(1);//非0表示异常退出
}else{
printf("This is a parent process!\n");
wait(&status);//父进程执行到此会阻塞自己
}
printf("child process' exit status is %d\n",WEXITSTATUS(status));
return 0;
}
status的构成: 如何判断是否正常退出
需要取出status的低7位,操作: status & 0x7f (0x7f就是16进制的0111 1111)
取出退出码
操作:(status >> 8) & 0xff (status>>8 是先将status左移8位,然后再与16进制的1111 1111相与)
程序替换
pcb是描述程序的运行过程,创建子进程是为了完成一个任务,这个任务可以是1️⃣与父进程做相同的任务,分担压力 2️⃣完成另一个任务。
如果是把另一个任务放在当前代码中,则代码会变得庞大,模块化不好,所以采用程序替换让子进程pcb管理调度这个程序,也就是替换掉一个pcb所描述的要调度管理的程序,把它替换为另一个(修改映射关系)
当程序替换成功后,就不会再运行原程序的剩下部分,而是直接运行新的程序,新程序运行完后直接退出进程。
相关函数:
int execve (char* path ,char* argv[],char* env[]) 程序替换成功时没有返回值,失败会返回-1。特点:需要手动给路径以及环境变量
execv (char* path ,char* argv[]) 特点:需要给路径,不需要手动给环境变量,函数会使用默认的环境变量。
execvp (char *file,char* argv[]) 特点:不需要给路径,只需要给文件名,函数会在默认的路径下寻找该文件,不需要手动给环境变量,使用默认的环境变量。
execl (char* path, char* arg) 其中char* arg 可以用...代替,C语言中...表示不定参数,即参数个数不定
execlp(char* file, ...) 特点:不需要给路径,不需要手动给环境变量,使用默认的环境变量。
execle(char* path, ..., char *env[]) 特点:需要给路径,不需要手动给环境变量,函数会使用默认的环境变量。
int main(){
pid_t pid;
pid=fork();
if(pid<0){
printf("fork failed!\n");
exit(-1);
}else if(pid==0){
printf("This is a child process! pid=%d",getpid());
execlp("ls","ls","-a","-l",NULL);//将程序替换为文件名为ls的程序
printf("程序替换失败!\n");
exit(0);
}else{
printf("This is a parent process! pid=%d",getpid());
sleep(3);
}
return 0;
}
可以看到,以上程序已被替换为linux自带的ls程序了。