linux操作系统进程控制详解

发布于:2022-08-05 ⋅ 阅读:(253) ⋅ 点赞:(0)

目录

前言

一、进程创建

1、fork函数

 2、写时拷贝

二、进程终止

1、退出码

 2、进程常见退出方法

2.1  正常退出

 2.2 异常退出

三、进程等待 

1、进程等待的必要性

2、进程等待的方法

2.1 wait方法

2.2 waitpid方法

3、阻塞等待和非阻塞等待

四、进程程序替换 

1、替换原理

2、替换函数 

2.1 函数解释

2.2 命名理解

总结


前言

哈喽,小伙伴们大家好,今天我来带大家了解一下进程控制的相关知识。我将主要从四个方面进行讲解,分别是进程创建、进程终止、进程等待和进程替换。希望小伙伴们看完本文后能对进程有更加深刻的认识。


一、进程创建

1、fork函数

在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

  • fork函数有两个返回值:子进程中返回0,父进程返回子进程pid,出错返回-1。
  • fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。

fork的基本使用在上一篇文章中介绍过,这里不做赘述,下面我们重点思考两个问题。

(1)为何要给子进程返回0,给父进程返回子进程的pid?

计算机中的父子关系和现实中的父子关系一样,一个父亲可以有多个孩子,但一个孩子只能有一个父亲。假设老王有三个儿子,三个儿子统一管老王叫爸爸即可。但当三个儿子同时在场时,老王想要找他们中的一个时不能简单的称呼为儿子,因为这样会造成混淆,必须叫他们对应的姓名才能加以区分。

在操作系统中也是如此,父进程不需要标识,子进程需要标识,子进程是要执行任务的,父进程需要对它们进行区分。

(2)如何理解fork有两个返回值的问题?

进程调用fork,当控制转移到内核中的fork代码后,内核做:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中

在返回之前,这一系列操作已经完成,子进程已经创建成功,返回这条代码由父子进程一起执行,所以有两个返回值。

fork调用失败的原因:

  • 系统中有太多的进程。
  • 实际用户的进程数超过了限制。

 2、写时拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。

为何需要写时拷贝?

因为进程具有独立性,要保证两个进程的数据不互相干扰。

那么为何不在一开始就把数据分开呢?

因为子进程不一定会用到或写入父进程的所有数据,如果把所有数据都拷贝一份的话会造成内存浪费。写时拷贝可以保存按需分配,同时也满足了延时分配,就好比银行一样,有人取钱时它才会给,其余时间就可以自由支配这些资源,达到资源利用的最大化。

二、进程终止

1、退出码

进程运行的目的是为了完成某种工作,那么这种工作有没有成功完成呢?进程需要给出一个反馈,来供用户查看工作是否返程,这种反馈就叫做退出码。

指令: echo $?    打印最近一次的退出码

进程退出场景: 

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

前两种退出场景:当代码运行结果正确时,退出码为0,当代码运行结果不正确时,退出码为非0。因为成功了就是成功了,不需要加以解释,而失败了一般都是有原因的,需要将失败的原因反馈出来。不同的非0值代表着不同的失败原因。由于计算机是很擅长处理数字的,所以退出码在计算机中是以数字的形式传递。但人是非常不擅长处理数字的,需要将数字进行翻译才能够识别它代表的含义。

sterror()函数:把错误码转化为对应含义的字符串

我们可以通过sterror翻译错误码来确认任务失败的原因。

注意:如果是第三种退出场景代码异常终止(比如指针越界等情况),则退出码毫无意义。就好比如果一个人考试作弊被逮到,大家只会取关心他作弊这件事本身,没有人会去关心他考了多少分。 

 2、进程常见退出方法

2.1  正常退出

  • return退出
  • 调用exit
  • _exit

注意: 只有main函数中的return代表进程退出,而在任何地方调用exit或_exit都代表进程退出。

exit和_exit的区别:exit最后也会调用_exit,只不过在调用之前还做了其它工作。

  • 执行用户定义的清理函数
  • 关闭所有打开的流,写入缓冲区中的数据
  • 调用_exit

 2.2 异常退出

ctrl+c,信号终止。进程异常退出会反馈信号,信号的详细内容之后再聊。我们只要先知道kill -l 可以打印信号列表即可。

进程终止后,操作系统会释放曾经申请的内存,释放曾经申请的数据结构,从队列等数据结构中移除。

三、进程等待 

1、进程等待的必要性

子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程的问题,进而造成内存泄漏。进程等待一般由父进程来完成。

进程等待的作用:回收子进程资源,获取子进程的退出信息。

2、进程等待的方法

2.1 wait方法

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);

返回值:成功返回被等待进程的pid,失败返回-1。

参数status:输出型参数 ,获取子进程退出状态,不关心则可以设置成NULL。

status不能简单的当成整形来看待,应该当成位图来看待。我们只关心后十六位。后十六位中的低七位代表进程退出时的退出信号,高八位代表进程退出时的退出码。

但在实际写代码中,我们一般不会用位操作来控制status,因为这样很复杂。库里给我提供了对应的宏。

  • WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
  • WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

wait的时候,父进程在干什么?

父进程什么也不干,只是单纯的在“等”。父子谁先运行不知道,但是wait之后,一般都是子进程先退出,父进程拿到子进程退出消息后才会退出。

2.2 waitpid方法

pid_ t waitpid(pid_t pid, int* status, int options);

参数:

  • pid: 如果pid=-1,则等待任意一个子进程,与wait等价。若pid>0,等待进程ID与pid相等的进程。
  • status:与wait相同
  • options:暂时不关心 ,传0即可。
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>                    
#include <sys/wait.h>                                                                                                                                                   
int main()            
{                    
  pid_t id=fork();
  if(id==0)
  {
    //child
    int count=10;
    while(count--)
    {
      printf("I am child\n");
      sleep(1);
    }
  }
 else 
  {
    int status=0;
    //father
    pid_t ret=waitpid(id,&status,0);
    if(ret>=0)
    {
      printf("wait success\n");
      if(WIFEXITED(status))
      {
        printf("%d\n",WEXITSTATUS(status));
      }

      else 
      {
        printf("child not exit normal\n");
      }
    }
  }
   return 0;
}

3、阻塞等待和非阻塞等待

阻塞等待:父进程什么也不干,一直卡在wait函数专门等子进程结束。

非阻塞等待:父进程调用wait函数时如果子进程还未结束,父进程先往下走,每隔一段时间调用一次wait函数查看子进程是否完成,在时间间隔时可以干其它事情。

在看一下waitpid:

pid_ t waitpid(pid_t pid, int* status, int options);

 第三个参数options如果传0的话默认是阻塞等待,如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0,然后执行下面的代码。若等待成功,则和之前一样,返回该子进程的ID。

int main()
{
  pid_t id=fork();
  if(id==0)
  {
    //child
    int count=5;
    while(count--)
    {
      printf("I am child\n");
      sleep(3);
     }
  }
  else 
  {
    //father
    while(1)
    {    
      int status=0;    
      pid_t ret=waitpid(id,&status,WNOHANG);    
      if(ret>0)    
      {    
        printf("wait success\n");    
        break;
      }
      else if(ret==0)
      {
        //子进程未完成
        printf("father do other things\n");
        sleep(1);
      }
      else 
      {
        printf("waitpid error\n");
        break;
      }
    }
 }
 return 0;
}

四、进程程序替换 

1、替换原理

用fork创建子进程后,子进程和父进程共用的是一个代码(虽然可能是不同的代码路径)。但实际情况下,子进程一般不会和父进程用一套代码,而是会使用exec函数,使子进程的代码和数据被新程序替换,执行另一套程序。

注意:

  • exec函数替换的仅仅是程序,不会创建新进程,所以子进程的pid不会改变。
  • 进程程序替换,替换成功后将不会返回,exec后面的代码不会被执行。
  • 如果exec调用失败,依旧执行原程序,后续代码不会受到影响。

2、替换函数 

2.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[]); 

系统实际调用的是最后一个函数,前五个函数是对最后一个函数进行的封装 。

注意: 

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
  • 如果调用出错则返回-1,exec函数只有出错的返回值而没有成功的返回值。我们一般情况下不会关注exec函数的返回值,因为只要返回了就说明调用失败了。
  • exec函数可以用系统的程序进行替换,可以用我们自己写的程序进行替换,甚至可以在c语言中调用phython,java等其它语言的程序进行替换。

2.2 命名理解

  • l(list):表示参数采用列表传,一个一个传
  • v(vector):表示参数用数组传,一起传过去
  • p(path):有p自动搜索环境变量PATH下的路径
  • e(env):表示自己维护环境变量,不要系统默认的
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};//自己定义的环境变量
//带l的,传参一个一个传
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使用环境变量PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,传入自己定义的环境变量
execle("ps", "ps", "-ef", NULL, envp);

//带v的,直接传一个数组
execv("/bin/ps", argv);
// 带p的,可以使用环境变量PATH,无需写全路径
execvp("ps", argv);
// 带e的,传入自己定义的环境变量
execve("/bin/ps", argv, envp);


总结

以上就是今天要将的内容。本文主要从四个方面来讲解了进程控制的相关知识,不知道小伙伴们看完后有没有收获呢。感谢大家的阅读,来日方长,我们下次见~


网站公告

今日签到

点亮在社区的每一天
去签到