【Linux】进程控制

进程创建

fork函数

进程调用fork,当控制转移到内核中的fork代码后,内核做:
分配新的内存块和内核数据结构给子进程
将父进程部分数据结构内容拷贝至子进程
添加子进程到系统进程列表当中
fork返回,开始调度器调度

在fork结束之后,父进程和子进程都进行了调度,之后先运行谁是取决于调度器的。
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。

fork函数返回值

子进程返回0,
父进程返回的是子进程的pid

fork常规用法

一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子
进程来处理请求。
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

fork调用失败的原因

系统中有太多的进程 (子进程的创建是要占用资源的)
实际用户的进程数超过了限制

写时拷贝

操作系统是不允许任何不高效,和浪费的情况出现的,所以当fork之后,子进程并不会傻乎乎的将父进程的所有数据的代码都拷贝过来,当一些子进程完全不需要的东西就是不会拷贝过来了。
所以写时拷贝是一种按需申请资源的策略
现在我所理解的就是进程数据可以进行写时拷贝,代码虽然不能写时拷贝但是能进行整体替换。

在这里插入图片描述

进程终止

进程退出的场景(情况)

代码运行完毕,结果正确
代码运行完毕,结果不正确
代码异常终止,【信号】(崩溃的本质:进程因为某些原因,导致收到了操作系统的信号(kill -9))

退出码

进程终止的状态可以由进程的退出码看出运行完毕是否正确,在代码异常终止时时因为什么原因。
echo $?是用来查看进程结束的退出码的,它查看的退出码只有第一次查看是有效的,重复查看一个进程的退出码它会变化的。
在这里插入图片描述

在这里插入图片描述

上面的退出码错误表是C语言的,
下面的图片是查看它的退出码,

在这里插入图片描述
在这里插入图片描述

进程常见退出方法

进程退出:就是将该进程所对应的内核数据结构和相关数据和代码进行释放
一、main() return在其他函数中调用并不是进程退出,只有在main()当中才是
二、exit(),C标准库函数,它是在任何地方都是进程结束并且exit后面的代码并不会继续执行。它是等价main() returm
三、_exit()它是直接进行退出不做任何处理的,而exit是进行缓冲区刷新,执行用户定义的清理函数
在这里也对return,exit进行了测试都是具有刷新缓冲区的功能的。

在这里插入图片描述

在这里插入图片描述

 1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 
  5 void add()
  6 {
  7   printf("exit前");
  8   //exit(123);
  9   printf("exit后");
 10 }
 11 
 12 int main()
 13 {
 14   //add();
 15   //
 16   printf("hello world");
 17   sleep(2);
 18   //exit(1);
 19   //return 0;                                                                                                                                                                                
 20   //return 0;
 21   _exit(11);
 22 }
~

在这里插入图片描述
在这里插入图片描述

进程等待

子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法
杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,
或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

进程等待的原因就是:防止内存泄漏,和获取子进程退出信息(在必要的情况下)
等待:就是通过系统调用,获取子进程退出码或者退出信号的方式,并顺便释放内存

下面的代码在子进程代码走完会等待5秒,10-5那5秒当中子进程是属于僵尸进程,等待父进程获取子进程信息,最后的等待5秒是子进程已经退出,只剩下父进程,最后wait返回的是子进程的pid
在这里插入图片描述
在这里插入图片描述

wait方法

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

waitpid方法

pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:
status并不要把它当作一个整数指针,而是将他看作成位图(由一个比特位或多个比特位组成表示某些状态的数据结构)

下面的这两个是,这两个宏的目的就是不用在我们自己算出退出码了
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进
程的ID。

退出码是数字,可以输入kill -l来查看信号(1~31称为普通信号,也是会用到的)
在这里插入图片描述

当信号为零时表示进程正常退出,之后在查看退出码是看因为什么原因退出的。
在这里插入图片描述
在这里插入图片描述

    1 #include<stdio.h>
    2 #include<stdlib.h>
    3 #include<unistd.h>
    4 
    5 int main()
    6 {
    7   pid_t id =fork();
    8   if(id==0)
    9   {
   10     int cnt=5;
   11     while(cnt)
   12     {
   13       printf("我还活着!!%d,pid=%d,ppid=%d\n",cnt--,getpid(),getppid());
   14       sleep(1);
   15     }
   16     exit(1);
   17   }
   18   //sleep(10);
   19   int status=0;
E> 20   pid_t ret=waitpid(id,&status,0);
   21   printf("pid=%d,ppid=%d,ret=%d,status=%d,child code=%d,child signal=%d",getpid(),getppid(),ret,status,status>>8&0xFF,status&0x7F);                                                        
   22   //sleep(5);
   23 
   24 }

在这里插入图片描述

如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退
出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。

父进程是如何获取子进程的推出信息的
读取子进程的内核数据结构在这里插入图片描述

父进程在wait时,如果子进程没有退出,则父进程在干什么
在子进程一直没有退出,父进程则是一直调用waitpid进行等待–阻塞等待
当子进程结束时,会有个指针指向父进程,然后父进程从阻塞的队列当中移到正在运行的队列当中

如果并不想要父进程是阻塞状态呢
WNOHANGwaitpid当中的参数,代表夯住了,也是非阻塞状态
举个例子就是,当我想要找朋友出去玩,就打电话叫他,当朋友有事情,说让我等他一会,
我有两种选择就是
一、我给他打电话不挂断,一直等到他结束(属于阻塞等待)
二、我给他打电话挂断,过一会就打一个,在不打电话的期间我可以去做自己的事情(属于非阻塞状态)
在这里插入图片描述

    1 #include<stdio.h>
    2 #include<stdlib.h>
    3 #include<unistd.h>
    4 
    5 int main()
    6 {
    7   pid_t id =fork();
    8   if(id==0)
    9   {
   10     int cnt=5;
   11     while(cnt)
   12     {
   13       printf("我还活着!!%d,pid=%d,ppid=%d\n",cnt--,getpid(),getppid());
   14       sleep(1);
   15     }
   16     exit(1);
   17   }
   18   //sleep(10);
   19   while(1)
   20   {                                                                                                                                                                                        
   21     int status=0; 
E> 22     pid_t ret=waitpid(id,&status,WNOHANG);
   23                                           
   24     if(ret<0)
   25     {        
   26       perror("waitpid err");
   27       exit(1);              
   28     }         
   29     else if(ret==0)
   30     {              
   31       printf("处于非阻塞状态\n");
   32       sleep(1);                  
   33       continue;
   34     }          
   35     else{
   36       printf("pid=%d,ppid=%d,ret=%d,status=%d,child code=%d,child signal=%d",getpid(),getppid(),ret,status,status>>8&0xFF,status&0x7F);
   37       break;                                                                                                                           
   38     }       
   39   }   //sleep(5);
   40                                                                
   41 }  

在这里插入图片描述

进程程序替换

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

在这里插入图片描述

从下面的代码和输出结果可以看出,当我们进行程序替换之后,后面的代码就不会在调用了, 程序替换换的是整体,并不是局部替换,在子进程中进行程序替换并不会影响父进程,因为进程具有独立性, 子进程进行发生写时拷贝

 #include<stdio.h>
  2 #include<unistd.h>
  3 
  4 int main()
  5 {
  6   printf("hello world\n");
  7   printf("hello world\n");
  8   printf("hello world\n");
  9   printf("hello world\n");
 10   execl("/bin/ls","ls","-a","-l",NULL);
 11   printf("hi world\n");
 12   printf("hi world\n");
 13   printf("hi world\n");                                                                                                                                                                      
 14 }

在这里插入图片描述

execl的返回值失败返回-1成功不返回值,只要有返回值就是失败了,所以execl是不需要进行返回值判断的,只要继续向后运行就是失败了。
当程序替换失败后则会继续运行后面的代码,成功则不会。
在下面的代码中子进程的退出码是由execl当中的ls决定的,hello.txt不存在则退出码是2,并不是1

#include <sys/wait.h>

int main()
{
    extern char **environ;
    pid_t id = fork();
    if(id == 0)
    {
        //child
        printf("我是子进程: %d\n", getpid());
        //execl: 如果替换成功,不会有返回值,如果替换失败,一定有返回值 ==》如果失败了,必定返回 ==》 只要有返回值,就失败了
        //不用对该函数进行返回值判断,只要继续向后运行一定是失败的!
        execl("/bin/ls", "ls","hello。txt", NULL);
        exit(1);
    }
    sleep(1);
    //father
    int status = 0;
    printf("我是父进程: %d\n", getpid());
    waitpid(id, &status, 0);
    printf("child exit code: %d\n", WEXITSTATUS(status));

    return 0;
}

execl的所有接口

execl
int execl(const char *path,const char *arg,...);

从下面可以看出,程序替换execl和在命令行执行的结果是一样的,execl的最后一个字母可以理解成为list
在这里插入图片描述

在这里插入图片描述

execv
int execv(const char *path,char *const argv[]);

execvexecl的区别在于最后一个单词,这里的最后一个代表的是vector,所以这两个并没有本质的区别,只是在传参的方式上有所不同。
在这里插入图片描述

execlp
int execlp(const char *file,const char *arg,...);

当我们在执行此程序时,只需要指定此程序名即可,系统会自动在环境变量PATH当中查找,execlp的最后一个字母可以理解成PATH,在下面图片当中的两个ls的意思是不一样的,第一个代表的意思是我要执行谁,第二个及其往后是代表我要怎么执行它,有时也是可以省略的。
在这里插入图片描述

execvp
int execvp(const char *file,char *const argv[]);

经过上面的程序替换,也就可以推算出execvp的用法了。
在这里插入图片描述

execle
int execle(const char *path, const char *arg, ..., char * const envp[]);

这里替换的程序也可以是自己写的程序,execle的前两个参数还是像之前一样,最后一个参数是传自定义环境变量的,otherfile单独使用是没有MYENV的环境变量的,用过myfile,使其获得了该环境变量
自定义环境变量会覆盖式的将PATH进行修改,也就是下面的argv将PATH进行了覆盖,只剩下了,MYENV you can see me
当我们想要使用当前系统的环境变量的话,之前用过char **environ,将该函数名替换自定义环境变量,就可以了
但是这样自定义就用不了了,所以及想要自定义,又想要系统给的不被覆盖就要使用putenv是将自定义环境变量添加到environ
在这里插入图片描述

    1 #include<stdio.h>
    2 #include<unistd.h>
    3 #include<stdlib.h>
    4 int main()
    5 {//该代码就是将自定义环境变量添加到系统的环境变量当中。
    6   extern char **environ;                                                                                                                                                                   
   12 //  char *const argv[]={"MYENV=you can see me ",NULL}; 
   19   putenv("MYENV=you can see me");//
   20   execle("./otherfile","otherfile",NULL,environ);
   21 }
execvpe

经过前面的介绍,execvpe和和前面的都大同小异。

int execvpe(const char *file, char *const argv[],
                   char *const envp[]);
execve

经过前面的介绍,execvpe和和前面的都大同小异。

int execve(const char *filename, char *const argv[],
                  char *const envp[]);
通过下面的图片知道,一共由七种接口,但是在3号表当中只有6个,还有一个`execve`在2号表当中,是因为在3号表当中的的6个接口都是将`execve`进行封装的

在这里插入图片描述

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值