Linux进程控制

一、进程创建(fork)

作用是创建一个子进程

fork返回值问题

现象

获取fork()的返回值后,并且打印返回值、PID、PPID,发现

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>

int main(){
  pid_t return_value = fork();
  printf("return value is %d,PID is %d,PPID is %d\n",return_value,getpid(),getppid());
  sleep(2);
}

当前进程的返回值是子进程返回值的PID,当前进程的PID是子进程的PPID,子进程的返回值为0

结论

fork(()对父进程的返回值是子进程的PID,对子进程的返回值是0

目的

因为进程的task_struct对象中存有PPID,可以轻易找到父亲,但是不存有子进程的PID,这样做的目的是让父进程能够找到子进程,子进程能通过task_struct对象找到父进程。

Q:为什么fork函数会返回两次?

A:因为在fork函数返回之前,子进程就已经创建出来了,在创建出来子进程之后,后面的代码都会被父子进程分别执行。

Q:操作系统是如何做到父子进程的返回值不同的?

A:在操作系统层面,父子进程会共用同一份数据,但是当有一方进程要对数据进行修改时,就会发生写时拷贝,即将公共数据拷贝一份到该进程自己的进程内存空间中,并且建立虚拟内存与物理内存的映射,所以在父子进程中各自存在一份fork()的返回值储存在自己的内存空间当中。

二、写时拷贝

1)写时拷贝

在创建子进程的时候,进程会共用同一份数据,但是当有一方进程要对数据进行修改时,就会公共数据拷贝一份到该进程自己的进程内存空间中,并且建立虚拟内存与物理内存的映射,这个过程叫做写时拷贝。

2)写时拷贝的意义

1)如果父子进程对谋一份数据读取,为了节省空间,则父子进程可以共用同一份数据,不需要开辟两份空间。

2)修改数据的时候,可能会需要基于原来的数据进行修改,所以需要进行拷贝

3)写时拷贝的原理

页表中存在虚拟地址和物理地址的映射,以及进程对该地址的权限,在进行创建子进程之间,如果父进程对变量A的地址有读写权限,那么在创建子进程后,二者的页表是相同的,会将所有的数据的内存权限改为只读,如果有一方对数据进行修改,那么该进程就会对该变量地址建立一个新的映射关系,并且将原来数据拷贝到新的映射物理地址中。从而实现写时拷贝。

三、进程终止

1)main函数的返回值问题

main函数的返回值是一个退出码,返回退出码表示进程已经结束,退出码表示进程结束的状态。0表示正常退出,非零表示出现错误,用不同的非零数字表示不同的错误原因。

在Linux下面,bash会自动记录上一个进程的退出码,可以使用echo $?查看

cpp 复制代码
#include <stdio.h>

int main(){
  printf("hello world!\n");
  return 99;
}

可以观察到,运行./mybin后,上次保存的退出码是99,属于mybin的退出码,而后面的0是属于echo的退出码。

通过以上现象可以得知,子进程的退出码可以被父进程获取,父进程可以通过子进程的退出码得知子进程的运行情况,但是退出码只能被计算机认识并且进行处理,不适合人类看,所以退出码必须与错误描述对应起来。

2)errno

通过调用C语言标准库的errno变量可以在语言层面获取上一个调用函数的退出码从而得知其运行状态。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

int main(){
  FILE* file=fopen("log.txt","r");
  printf("%d:%s\n",errno,strerror(errno));
  return 0;
}

3)Linux下面的退出码

在Linux下面,存在自带的退出码和对应的错误描述。可以通过strerror(int)查看退出码对应的错误。

例如随便输一个不存在的命令,然后在bash下打印其错误码,可以看到是127

4)自定义错误码

在C/c++里面可以通过枚举体进行自定义错误码。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

enum{
  success=0,
  open_err,
  malloc_err
};
const char* error(int code){
  switch(code){
    case success:
      return "sucess";
    case open_err:
      return "open_err";
    case malloc_err:
      return "malloc_err";
    default:
      return "Unknow Error";
  }
}

int main(){
  int code=malloc_err;
  printf("%s\n",error(code));
  return 0;
}

5)进程终止

进程终止总的来讲有三种情况:

(1)进程正常运行结束并且获得正确的结果

(2)进程异常运行结束并且获得错误的结果

进程是否正常运行可以由退出码得知,进程是否获取了正确的结果,是否正常结束。

(3)进程异常并且被系统终止

进程异常终止是在运行过程中就出现问题,其得到的结果就无意义,其退出码也没有意义,本质原因是收到了来自系统的异常信号,每一种信号对应了不同的退出原因,只能根据信号来知道异常的原因。

综上所述,任何进程最终执行情况,都可以通过退出码和终止信号来获取

6)exit函数/-exit函数/return

exit函数/-exit函数,头文件<stdlib.h>/<unistd.h>,无返回值,参数为指定退出码,作用是终止当前进程,前者是C语言提供的库函数,后者是Linux提供的系统调用函数。

exit:

_exit:

同:

二者都没有返回值,参数相同,作用相同,都是用于终止当前进程。

异:

exit会刷新缓冲区,而_exit不会刷新缓冲区,exit底层是封装的_exit实现的。该缓冲区不是系统层面的缓冲区,而是库缓冲区。

四、进程等待

在进程结束时,会进入僵尸状态,在僵尸状态,会保留PCB里面的部分信息,例如退出码和退出信号,必须等父进程来回收其空间,僵尸进程才能完全从内存中删除。所以僵尸进程就必须通过wait父进程让父进程获取其推出信息。

等待的目的:回收僵尸进程的空间,获取进程退出状态

1)wait

可以用于回收僵尸进程

wait:默认会进行阻塞等待,等待任意一个子进程,返回值大于0,说明等待成功,等待的子进程PID为返回值,若返回值为0,则说明等待失败。参数status:获取退出信号和退出码

ststus格式,只考虑低16位。

低7位表示进程的退出信号,高8位表示退出码,所以通过status可以获得退出信号和退出码

wait使用实例
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>
#include <sys/types.h>

int main(){
  pid_t id = fork();
  if(id==0){
    int cnt = 5;
    while(cnt--){
      printf("Child:PID is %d,PPID is %d\n",getpid(),getppid());
      sleep(1);
    }
    //提示子进程即将死亡
    printf("child process is dying\n");
    //终止子进程
    exit(5);
  }
  //进行阻塞等待
  printf("Parent Process is sleeping\n");
  sleep(20);
  pid_t cpid = wait(NULL);
  if(cpid>0){ 
    printf("sucess.cpid is %d\n",cpid);
  }
  printf("recycle sucessfully!\n");
  sleep(3);

}

通过查看运行结果,可以看到,wait能够成功获取到僵尸进程的PID并且回收其资源。

从对进程的监视可以看到,子进程开始是僵尸状态,等wait回收后,僵尸状态推出了,就在进程列表中查不到该进程

2)waitpid

和wait一样,返回值大于0表示等待成功,返回值为等待到的子进程。参数:pid(-1:等待任意一个子进程,>0:等待特定的子进程),status(退出状态),options(等待方式)

waitpid使用实例
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>
#include <sys/types.h>

int main(){
  pid_t id = fork();
  if(id==0){
    int cnt = 5;
    while(cnt--){
      printf("Child:PID is %d,PPID is %d\n",getpid(),getppid());
      //sleep(1);
    }
    //提示子进程即将死亡
    printf("child process is dying\n");
    //终止子进程
    exit(5);
  }
  //进行阻塞等待
  printf("Parent Process is sleeping\n");
    
  // sleep(20);
  int status=0;
  pid_t cpid = waitpid(id,&status,0);
  if(cpid>0){ 
    printf("sucess:cpid is %d,status is %d,exit_sta is %d,exit_code is %d\n",cpid,status,status&0x7f,(status>>8)&0x77);
  }
  printf("recycle sucessfully!\n");
  sleep(3);

}

成功解析退出信号和退出码

提取退出码的宏

在获取到status之后,可以直接用库实现的宏来获取退出信号和退出码。

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

3)阻塞等待和非阻塞等待

阻塞等待的作用是父进程在等待子进程的过程中始终不执行其他操作,此时父进程直接进程阻塞状态,而非阻塞等待的作用是在等待的过程中,可以执行其他操作,父进程会周期性检查子进程的运行情况,直到获取到了子进程的退出信息,则退出等待状态。

非阻塞状态宏:WNOHANG,表示在等待过程中父进程不要挂起。

五、进程程序替换

程序替换是创建的子进程,子进程不再与父进程共享代码,转而执行其他程序的代码

程序替换的原理

在运行过程中,将子进程的代码段替换为其他代码,替换数据段,并且初始化堆栈,修改页表映射关系,从而实现程序替换。

excl接口c
cpp 复制代码
       #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 execvpe(const char *file, char *const argv[], char *const envp[]);
execl函数

参数:

path:程序的路径

arg:执行程序的参数,最后必须以NULL结尾。

cpp 复制代码
int main(){
  execl("/usr/bin/ls","ls","-l","-a",NULL);
  return 0;
}

执行结果可以在进程中执行其他程序

1.程序替换一旦成功,exec*,后续的代码不再执行,转而执行目标程序代码

2.exex*只有失败返回值,没有成功返回值,因为如果成功,返回值对原来进程就没有意义了。

3.在进行替换时,不会再创建新的进程。

4.在创建一个进程的时候,是先创建其数据结构,然后再加载程序并且建立映射关系,所以替换的本质就是加载程序。

六、exec接口

通过查看man手册,exec系列一共有7个接口,其作用都是替换程序,

cpp 复制代码
       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 execvpe(const char *file, char *const argv[], char *const envp[]);

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

1)命名带l:说明目标程序参数以列表的形式传参,参数以NULL结尾(NULL的作用是表示可变参数结束)。例如execl("/usr/bin/ls","ls","-l","-a",NULL);

2)命名带v:表示目标程序参数以数组的形式传参,参数不需要带NULL。例如,char*argv[]={"ls","-l","-a"};execv("/usr/bin/ls",argv);

3)命名带p:传参时只需要传文件名即可,系统会自动去环境变量中寻找,不带p的需要带入参数,例如execlp("ls","ls","-l","-a",NULL);

4)命名带e:传参时需要传环境变量以为目标进程创建新的环境变量,因为在替换的时候,不会将原来进程地址空间中的环境变量覆盖,目标进程会续用原来的环境变量。

通过exec接口,可以通过调用该接口进而调用其他语言的程序,因为其他语言其本质运行起来都是进程,是二进制文件。

cpp 复制代码
//test.c
int main(){
  execl("/usr/bin/python3","python3","pp.py",NULL);
}
python 复制代码
# pp.py
print("hello world")
相关推荐
向往风的男子10 分钟前
【devops】devops-gitlab之部署与日常使用
运维·gitlab·devops
Reuuse35 分钟前
【HCIA-Datacom】华为VRP系统
服务器·网络·华为
轩轶子1 小时前
【C-项目】网盘(一期,线程池版)
服务器·c语言
GDAL1 小时前
全面讲解GNU:从起源到应用
服务器·云计算·gnu
GDAL2 小时前
GNU力量注入Windows:打造高效跨平台开发新纪元
服务器·windows·gnu
hgdlip2 小时前
电脑和另一台电脑IP地址相同怎么办
服务器·电脑·ip地址
Dola_Pan2 小时前
Linux文件IO(一)-open使用详解
java·linux·dubbo
Spring-wind2 小时前
【linux】pwd命令
linux
geekrabbit2 小时前
机器学习和深度学习的区别
运维·人工智能·深度学习·机器学习·浪浪云
ken_coding3 小时前
Windows11 WSL2的ubuntu 22.04中拉取镜像报错
linux·ubuntu·docker