Linux || 进程控制

Linux进程控制

一:进程的创建
1:【fork的运行规则:】以父进程为模板,创建子进程
  • 1.子进程会把父进程的PCB拷贝一份,稍加修饰,做为子进程的PCB
  • 2.子进程会把父进程的虚拟地址空间拷贝一份,做为子进程的地址空间
2:【进程调用fork】:
  • 当控制转移到内核中的fork代码后,内核做以下工作
  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中fork返回,开始调度器调度
3:【父子进程的执行逻辑】

【代码测试】

#include<stdio.h>                                   
#include<unistd.h>

int main(){
   printf("before:pid=%d,ppid=%d\n",getpid(),getppid());
   pid_t pid=fork();
   if(pid>0){
    printf("father after:pid=%d,ppid=%d,父进程的返回值:%d\n"
    ,getpid(),getppid(),pid);
    sleep(3);
    //父进程等待子进程结束,为了在子进程结束后回收子进程的资源.
    //避免父进程先结束使得子进程变成孤儿进程
   }else if(pid==0){
     printf("child after:pid=%d,ppid=%d,子进程的返回值:%d\n"
     ,getpid(),getppid());
   }else{
     perror("fork");
   }
   return 0;
 }

【运行结果】
在这里插入图片描述
【过程分析】

  • 父进程的返回值为子进程的pid,子进程返回0;
  • 在fork前后父进程的pid和ppid都没有改变
  • 可以看出子进程在fork() 之后并没有再去执行第一句printf语句则说明:fork之前父进程独立执行,fork之后.父子两个执行流分别执行
    在这里插入图片描述
3:【父子进程与写时拷贝】
  • 通常,父子进程代码共享.父子在不写入时,数据也是共享的.当任意一方试图写入,便以写时拷贝的方式各自一份副本

在这里插入图片描述
【fork常规用法】

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段.
  • 一个进程要执行一个不同的程序。

【fork调用失败的原因】

  • 系统中存在太多的进程
  • 实际用户的进程数超过了限制
二: 进程终止
1.进程退出场景
  • 1.代码运行完毕,结果正确
  • 2.代码运行完毕,结果不正确
  • 3.代码没执行完,进程异常终止
2.进程常见退出方法
正常退出方式异常退出方式
从main()函数返回Ctrl+ c,异常终止
调用exit。。。。
_exit。。。。

【注意】

  • main函数返回值叫做进程的推出码,通过这个退出码表示运行结果是否正确
退出码为0退出码非0
进程正常终止进程异常终止
3._exit()函数
#include <unistd.h>
void _exit(int status);
  • 参数:status 定义了进程的终止状态,父进程通过wait来获取该值

【使用实例】

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main(){
    printf("haha");
    _exit(1);
    //系统调用,并没有刷新缓冲区
    return 0;
}
4.exit()函数
  • 1.exit进程退出(库函数),本质上还是调用 __exit
#include<stdlib.h>                                      
#include<stdio.h>
#include<unistd.h>
  
  
int main(){
   	exit(1);
  	printf("hehe");
    return 0;
}

  • 不会打印出任何结果,在打印语句之前程序就已经结束了
5.【_exit与exit的对比】
  • exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:
  • 1.执行用户通过atexiton_exit定义的清理函数。
  • 2.关闭所有打开的流,所有的缓存数据均被写入
  • 3.调用_exit
    在这里插入图片描述
  • _exit 进程退出(系统调用)
  • _ eixt还调用了结束函数
  • exit也能退出程序,进程退出(系统调用)底层掉用 _ exit函数–> 关闭文件流并且刷新缓冲区
  • 使用exit(n)退出后 使用echo $?看到的结果即为n

【补充】

  • 显示器:行缓冲,遇到换行就刷新
    atexit(函数名) ----><stdlib.h>
  • 注册一个回调函数
  • 回调函数:掉用时机由系统决定

【使用实例】

#include<stdio.h>                                                                                      
#include<stdlib.h>
#include<unistd.h>
 
 //假设 c 语言执行过程,这个过程会写一个文件(临时文件)
 void Func(){
     printf("goodbye!\n");
 }
 
 int main(){
     atexit(Func);
     return 0;
 }
6.return
  • return是一种更常见的退出进程方法。
  • 执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返
    回值当做 exit 的参数
三:进程等待
  • 父进程对子进程进行进程等待,是为了读取子进程的运行结果
1:wait方法
  • 1.参数为输出型参数(表示退出码 + 正常/异常退出)
  • 2.返回值是子进程的 pid

【注意事项】

  • wait的使用次数必须和子进程的个数需要一致
  • wait的调用次数比子进程个数少会导致僵尸进程的产生
  • wait的调用次数比子进程个数多,多出来的 wait 就会调用出错

【使用实例】

  • 如果有多个子进程,任何一个子进程结束都会触发wait的返回
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

//两次调用fork函数只有一个wait,导致僵尸进程
int main(){
  pid_t ret=fork();
   if(ret<0){
     perror("fork");
     return 0;
   }
   if(ret==0){
     exit(0);
   }
   ret=fork();
   if(ret==0){
     exit(0);
   }       
   wait(NULL);
   while(1){
    sleep(1);
   }
    return 0;
}
  • wait的调用次数比进程个数少,导致僵尸进程的产生

在这里插入图片描述

  • 进程的创建个数必须和wait的个数匹配
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

 int main(){
   pid_t ret=fork();
   if(ret<0){
     perror("fork");
     return 0;
   }
   if(ret==0){
    printf("child1:ppid=%d,pid=%d\n",getppid(),getpid());
    exit(0);
   }
   ret=fork();
   if(ret==0){
   	printf("child2:ppid=%d,pid=%d\n",getppid(),getpid());                                                                    
     exit(0);
   }
 
  printf("father : %d\n",getpid()) ;
  ret=wait(NULL);
  printf("wait1: %d\n",ret);
  ret=wait(NULL);
  printf("wait2: %d\n",ret);
  ret=wait(NULL);
  printf("wait3: %d\n",ret);
  return 0
}

【运行结果】
在这里插入图片描述

  • wait的目标顺序与子进程的创建顺序一致
  • 可以看到每个wait的返回值都是自己所等待的子进程的 pid

kill -l

显示所有信号,其中32,33两个不存在

2:waitpid
【功能简介】
pid_ t waitpid(pid_t pid, int *status, int options);

【返回值】:

  • 正常返回的时候 waitpid返回收集到的子进程的进程ID
  • 如果设置了选项WNOH ANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
  • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在

【参数】:

  • pid:
  • Pid=-1,等待任一个子进程。与wait等效。
  • Pid>0.等待其进程ID与pid相等的子进程。
  • status:
  • WIFEXITED(status): 若为正常终止子进程返回的状态,则为真.(查看进程是否是正常退出)
  • WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码.(查看进程的退出码)
  • options:
  • WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0.不予以等待
  • 若正常结束,则返回该子进程的ID

【使用实例】

#include<stdio.h>                                                                                                          
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>
int main(){
  pid_t ret1=fork();
  if(ret1==0){
    printf("child1: %d\n",getpid());
    sleep(3);
    exit(0);
  }
 
  pid_t ret2=fork();
  if(ret2==0){
    printf("child2:%d\n",getpid());
    sleep(1);
    exit(0);
  }
 
 printf("father : %d\n",getpid()) ;
 int ret=waitpid(ret1,NULL,0);
 printf("waitpid1: %d\n",ret);
 ret=waitpid(ret2,NULL,0);
 printf("waitpid2: %d\n",ret);
 return 0;
}

【运行结果】
在这里插入图片描述

  • waitpid 用法与 wait 用法类似

【非阻塞waitpid的使用】

#include<stdio.h>                                                                                                        
#include<stdlib.h>
#include<unistd.h>

int main(){
  pid_t ret1=fork();
  if(ret1==0){
    printf("child1: %d\n",getpid());
    sleep(3);
    exit(0);
  }
   
  pid_t ret2=fork();
  if(ret2==0){
    printf("child2:%d\n",getpid());
    sleep(1);
    exit(0);
 }

 printf("father : %d\n",getpid()) ;
 int ret=0;
 int count=0;
 while(1){
 ret=waitpid(-1,NULL,WNOHANG);
 //WNOHANG加上之后,waitpid就变成了非阻塞
 printf("waitpid1: %d\n",ret);
 if(ret>0){
   break;
 } 
 ++count;
}
 printf("count=%d\n",count);
 ret=waitpid(ret2,NULL,0);
 printf("waitpid2: %d\n",ret);
 return 0;
}           

【运行结果】
在这里插入图片描述

【非阻塞轮询式的wait】

【好处】:

  • 能够更加灵活的控制代码,充分利用等待时间去做其他事情

【坏处】:

  • 代码写起来更复杂
四:进程程序替换

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

1:替换函数
  • 其实有六种以exec开头的函数,统称exec函数
#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[]);
2.函数解释
  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
  • 如果调用出错则返回-1
  • 所以exec函数只有出错的返回值而没有成功的返回值
3. 命名理解
  • 这些函数原型看起来很容易混,但只要掌握了规律就很好记。
  • l(list) : 表示参数采用列表
  • v(vector) : 参数用数组
  • p(path) : 有p自动搜索环境变量PATH
  • e(env) : 表示自己维护环境变量

在这里插入图片描述

  • 只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。
  • 下图exec函数族 一个完整的例子:
    在这里插入图片描述
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值