【Linux】进程控制

fork函数

fork函数是从已经存在的进程中去创建一个新的进程,新的进程被称为子进程,原来的进程叫做父进程

父进程返回子进程的pid,子进程返回0,出错返回-1。

#include <unistd.h>
pid_t fork(void);

Tip:pid_t就是就是int

进程调用fork的时候,内核会做以下事情:

  • 分配新的内存块和内核数据结构(PCB等)给子进程。
  • 以父进程的PCB为模板,将父进程的部分PCB内容拷贝到子进程的PCB当中。
  • 将子进程的PCB添加到系统进程列表当中。
  • fork返回,开始调度器调度。

**调度器:**调度器是操作系统的一个重要组件,负责决定哪些进程能够使用CPU资源,并按照一定的策略来分配CPU时间片给这些进程。调度器的主要目标是提高系统的性能和资源利用率,使得各个进程能够公平地分享CPU资源。

当一个新的子进程被创建后,它会被添加到系统的进程列表中,并根据调度器的算法进行排队等待调度。调度器可能会根据进程的优先级、轮转调度、抢占式调度等策略来决定下一个要执行的进程。

一旦调度器决定让子进程运行,它会分配一个时间片给子进程,使得子进程能够在这个时间片内执行一段时间的指令。当时间片用完后,调度器会重新调度,决定下一个要执行的进程。

**时间片:**时间片是指操作系统中的一段时间,它是调度器用来分配给每个进程的最小时间单位。

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以 独立运行,看如下程序。

int main( void )
{
   
 	pid_t pid;
 	printf("Before: pid is %d\n", getpid());
 	if ( (pid=fork()) == -1 ) perror("fork()"),exit(1);
    
 	printf("After:pid is %d, fork return %d\n", getpid(), pid);
    
 	sleep(1);
 	return 0;
} 

输出:
Before: pid is 18404
After:pid is 18404, fork return 18405
After:pid is 18405, fork return 0

这里为什么只打印了一次Before pid呢?

这是由于是在打印Before pid之后才调用fork去创建子进程的,子进程也只会从fork处开始,因此不会打印到Before pid这部分的内容。

fork函数的写时拷贝

一般来说父子的代码和数据是共享的,但前面说到,进程之间是相互独立的,既然是共享的代码和数据又是怎么做到独立的呢?

这是出于考虑到节省空间所对应的解决方式,刚开始父子进程的代码和数据是共享的,这是因为你也不确定子进程是否会全部调用并且修改父进程当中的代码和数据,如果不要进程修改,那么全部拷贝下来就有点过于浪费空间了,所以,只有当数据发生修改的时候,会触发写时拷贝,就是把要修改的数据在页表中重新进行映射。

fork调用失败的原因

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

进程终止

进程退出场景

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

可以通过echo $?来查看最近的进程退出码是多少。

1.从main函数return返回退出码

2.调用exit(num)返回num为退出码

3.调用_exit(和exit类似,只是不会刷新缓冲区)。

可以看到第一次是108,第二次就变为0了,因为第二次调用echo $?时,最近的退出码就是调用echo $?这个指令的子进程的退出码,因为指令也是用c语言写的,也是一个程序,也是一个进程。

_exit函数

exit_exit函数的区别就在于_exit不会刷新缓冲区

因为_exit不会刷新缓冲区,所以不会打印任何东西。

exit函数

exit函数其实也是调用_exit来结束进程,不过在这之前还会做一些其他的事情。

Tips:缓冲区在C库。

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

int main(void)
{
   
    printf("hello world");
    exit(1);
    return 0;
}

进程终止的时候,内核在干什么?

进程=内核数据结构(PCB)+代码、数据,当一个进程结束的时候操作系统不会直接释放这部分的空间,而是将这部分的内核数据结构(PCB)交给下一个进程使用,只需要对原PCB稍作修改就可以给下一个进程了,那么为什么操作系统不直接销毁这部分空间呢?因为每一次申请空间都需要时间,但是操作系统必须要高效,因此,为了减少开辟空间的次数导致操作系统运行速度变慢,就直接将它交给下一个进程使用了,并且将下一个进程加入运行队列。

进程等待

进程等待的必要性

  • 如果子进程退出了,但是父进程没有对子进程进行回收,那么就会导致僵尸进程,从而长时间占用内存,导致内存泄漏
  • 如果程序变成了僵尸进程,那么就算使用kill -9也无法将进程杀死。
  • 子进程一般是父进程创建出来完成特定的任务,既然是完成任务,就需要知道任务完成的怎么样了,是否存在异常。
  • 父进程通过进程等待来回收子进程的资源,获取子进程的退出信息。

wait/waitpid方法

pid_t wait(int *status);//status为输出型参数
pid_t waitpid(pid_t pid, int *status, int options);

返回值:

成功则返回进程的pid,反之返回-1。

输出型参数获取子进程的退出状态,如果不关心退出状态可以设置为NULL

PID:

如果pid为正数,表示等待具有该PID的子进程。

如果pid为-1,表示等待任意子进程,类似于wait函数。

如果pid为0,表示等待与调用进程在同一个进程组中的任意子进程。

如果pid小于-1,表示等待进程组ID等于pid绝对值的任意子进程。

status是从子进程的task_struct中拿出来的,子进程会将自己的退出码写入task_struct

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>

int main(void)
{
   
  pid_t id=fork();
  if(id==0)
  {
   
    //子进程
    int i=5;
    //循环5秒
    while(i--)
    {
   
      printf("我是子进程,ppid=%d,pid=%d\n",getppid(),getpid());
      sleep(1);
    }
    printf("子进程退出\n");
    exit(108);//子进程的退出码
  }else
  {
   
    int son=0;
    pid_t st=wait(&son);
    //父进程
    printf("我是父进程,ppid=%d,pid=%d\n",getppid(),getpid());
    sleep(5);
    printf("父进程等待成功,退出码为:%d\n",son);                                              
  }
  return 0;
}

可以通过以下脚本来监视进程状态变化:

while :; do ps axj | head -1 ; ps axj |grep process | grep -v grep ; echo "---------------------------------------------------------------"; sleep 1 ; done

可以看到,父进程一直处于S+睡眠状态,这就说明了父进程一直在等待子进程的结果,当子进程结束后,父进程sleep(5)睡眠5秒后也被结束了进程。

但是我的退出码为什么是27648而不是108,这就是接下来的内容了

获取子进程的status

  • waitwaitpid都有一个输出型参数status,该参数的值会由系统自动填充。
  • 如果传递NULL,就表示不关系子进程的退出状态。
  • 反之就会根据参数,将子进程的退出信息反馈给父进程。
  • status不能将其看作简简单单的整形,而是当作位图看待。

Tips:虽然int占32个比特位,但是这里只研究低位的16个比特位。

可以把status中的16个比特位看作三个部分:退出状态、core dump标志、终止信号。

  1. 高八位表示的是退出状态 即我们的退出码
  2. 低七位表示的是终止信号 如果我们的进程被信号所杀则此处会有终止信号
  3. 第八位表示的是core dump表示 这个我们暂时不需要了解

那么如何获取到退出状态和终止信号呢?

可以通过以下代码获取:

//终止信号
status&0x7f
//退出状态
(status>>8)&0xff //右移8个比特位再按位与1111 1111

如果是终止信号呢?就不会返回退出状态了,因为这个程序是否正常运行已经没有意义了,在中途已经有错误信号了。

  #include<stdio.h>    
  #include<unistd.h>    
  #include<stdlib.h>    
  #include<sys/wait.h>    
  #include<sys/types.h>    
      
  int main(void)    
  {
       
    pid_t id=fork();    
    if(id==0)    
    {
       
      //子进程    
      int i=5;    
      //循环5秒    
      while(1)    
      {
       
        printf("我是子进程,ppid=%d,pid=%d\n",getppid(),getpid());    
        sleep(1);    
      }    
      printf("子进程退出\n");    
      exit(108);//子进程的退出码    
    }else    
    {
       
      int status=0;                                                                          
      pid_t st=wait(&status);
      //父进程
      printf("我是父进程,ppid=%d,pid=%d\n",getppid(),getpid());
      sleep(5);
       printf("父进程等待成功,退出码为:%d\n",status&0x7f);
    }
    return 0;
  } 

对于如上代码,子进程是在一个死循环当中,这里使用kill -9来干掉这个进程。

9号信号就是被kill掉了进程。

终止信号

WIFEXITED(status)查看子进程是否是正常退出的,正常退出为真
WIFSIGNALED(status)查看子进程是否为信号终止,信号终止返回真
WEXITSTATUS(status)提取子进程退出码
WTERMSIG(status)提取子进程退出信号

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>

int main(void)
{
   
  pid_t id=fork();
  if(id==0)
  {
   
    //子进程
    int i=5;
    //循环5秒
    while(i--)
    {
   
      printf("我是子进程,ppid=%d,pid=%d\n",getppid(),getpid());
      sleep(1);
    }
    printf("子进程退出\n");
    exit(108);//子进程的退出码
  }else
  {
   
    int son=0;
    pid_t st=wait(&son);
    //父进程
    printf("我是父进程,ppid=%d,pid=%d\n",getppid(),getpid());
    sleep(5);
    printf("父进程等待成功,退出码为:%d  退出信号为:%d \n",WEXITSTATUS(son),WTERMSIG(son));
    printf("子进程是否正常退出:%d ,子进程是否信号终止:%d \n",WIFEXITED(son),WIFSIGNALED(son));
  }
  return 0;
}

阻塞等待和非阻塞等待

阻塞等待:在子进程结束之前,父进程什么都不做,处于休眠状态,当子进程结束了,将退出码或信号传递给了父进程,父进程接收到了才会继续运行。

非阻塞等待:在子进程结束之前,父进程每隔n秒就会去查看子进程是否运行结束了,也可以在这期间去做其他事情。

假如现在是期末复习阶段 明天就要考试了 你却完全没有复习
你的一个朋友张三 它复习的特别好 你想要去找他要复习资料
现在你来到张三的宿舍楼下 打电话给张三 让他下来请他去吃个饭 顺便要一下复习资料
张三电话里回复你说 现在还不太方便 还要30分钟才能下楼
假设你现在跟张三说 那你先别挂电话了 我在下面一直等着你 然后你就一直等着 什么事情都不做 这就叫做阻塞等待
假设你现在跟张三说 那你先忙 我先玩会儿游戏 然后你就去打游戏 每隔五分钟再打个电话问张三有没有好 这就叫做非阻塞等待

WNOHANG     return immediately if no child has exited.
如果子进程没有结束,就会立即返回,继续往后运行。

非阻塞等待

pid_t waitpid(pid_t pid, int *status, int options);
//把option换成WNOHANG,就可以实现非阻塞等待了。
pid_t waitpid(pid_t pid, int *status, WNOHANG);

如果返回-1:出错了

如果返回0:父进程还在等待子进程

如果返回>0:父进程等待成功并且子进程已退出

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>

int Add(int* a,int* b)
{
   
  return (*a)+(*b);
}

int main(void)
{
   
  int a=0,b=0;
  pid_t id=fork();
  if(id==0)
  {
   
    //子进程
    int i=5;    
    //循环5秒    
    while(i--)    
    {
       
      printf("我是子进程,ppid=%d,pid=%d\n",getppid(),getpid());    
      sleep(1);    
    }    
    printf("子进程退出\n");    
    exit(108);//子进程的退出码    
  }else    
  {
       
    while(1)    
    {
       
      int son=0;    
      pid_t st=waitpid(id,&son,WNOHANG);    
      //父进程    
      if(st==0)    
      {
       
        printf("我是父进程,ppid=%d,pid=%d\n",getppid(),getpid());    
        printf("我正在等待子进程,还没结束我先去调用Add()了  ");    
        a++,b++;                                                                                                                                                                              
        printf("Add(%d,%d)=%d\n",a,b,Add(&a,&b));    
        sleep(1);    
       }else if(st>0)    
       {
       
         printf("我是父进程,等待子进程成功,退出码:%d 退出状态:%d\n",WEXITSTATUS(son),WTERMSIG(son));    
         break;    
	   }else
       {
   
         printf("子进程出错,信号为:kill -%d\n",WTERMSIG(son));
         break;
       }
     }
  }
  return 0;
}

这种多次调用waitpid的方法叫做**轮询检测**。

每隔一段时间就会去看一下子进程运行的怎么样了,有没有返回退出码给父进程,没有的话父进程也不会干等着,会去做自己的其他事情。

子进程进入僵尸状态等待父进程回收。

进程替换(Point)

之前我们从fork()中创建的进程都是执行父进程的程序,最多修改期中一些代码和数据,但如果子进程想要执行一个全新的程序怎么办?这就要用到进程替换了。

替换原理

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

程序替换其实就是将子进程的对应数据段和代码段的页表在物理内存中映射的地址进行了替换。

替换函数

所需要的头文件:#include <unistd.h>

  1. int execl(const char *path, const char *arg, ...);
  • 在当前进程中执行指定路径的可执行文件,并传递命令行参数。参数列表以NULL结尾,例如:execl("/bin/ls", "ls", "-l", NULL);
  1. int execlp(const char *file, const char *arg, ...);
  • 在当前进程中执行指定文件名的可执行文件,并传递命令行参数。会在环境变量PATH中查找可执行文件。参数列表以NULL结尾,例如:execlp("ls", "ls", "-l", NULL);
  1. int execle(const char *path, const char *arg, ..., char * const envp[]);
  • 在当前进程中执行指定路径的可执行文件,并传递命令行参数和环境变量。参数列表以NULL结尾,最后一个参数是以NULL结尾的环境变量数组,例如:execle("/bin/ls", "ls", "-l", NULL, envp);
  1. int execv(const char *path, char *const argv[]);
  • 在当前进程中执行指定路径的可执行文件,并传递命令行参数。参数列表是以NULL结尾的命令行参数数组,例如:char *const argv[]={"ls", "-l", NULL}; execv("/bin/ls", argv);
  1. int execvp(const char *file, char *const argv[]);
  • 在当前进程中执行指定文件名的可执行文件,并传递命令行参数。会在环境变量PATH中查找可执行文件。参数列表是以NULL结尾的命令行参数数组,例如:char *const argv[]={"ls", "-l", NULL}; execvp("ls", argv);
  1. int execvpe(const char *file, char *const argv[],char *const envp[]);
  • 在当前进程中执行指定文件名的可执行文件,并传递命令行参数和环境变量。会在环境变量PATH中查找可执行文件。参数列表是以NULL结尾的命令行参数数组,最后一个参数是以NULL结尾的环境变量数组,例如:char *const argv[]={"ls", "-l", NULL}; execvpe("ls", argv, envp);

l(list) : 表示参数采用列表

v(vector) : 参数用数组

p(path) : 有p自动搜索环境变量PATH

e(env) : 表示自己维护环境变量

这个函数如果调用成功了就会加载新的程序运行,不会返回。

这个函数运行成功不会有返回值,出错返回-1。

excel

int execl(const char *path, const char *arg, ...);
  • path是具体路径。
  • arg是运行的程序名称。
  • …是可变参数,以NULL结尾。
#include<stdio.h>    
#include<unistd.h>    
#include<sys/types.h>    
    
int main()    
{
       
  printf("开始测试\n");    
  //调用execl    
  int ret = execl("/usr/bin/ls","ls","-l",NULL);    
  printf("执行结束 %d\n",ret);                                                                                         
  return 0;    
} 

为什么printf("执行结束 %d\n",ret);不见了呢?

因为原来的程序早就被替换掉了,肯定不会再运行printf("执行结束 %d\n",ret);了。

如果程序替换失败了就会返回-1

#include<stdio.h>    
#include<unistd.h>    
#include<sys/types.h>    
    
int main()    
{
       
  printf("开始测试\n");    
  //调用execl    
  int ret = execl("/usr/bin/lsaaa","ls","-l",NULL);    
  printf("执行结束 %d\n",ret);                                                                                         
  return 0;    
} 

引入python程序

#include<stdio.h>    
#include<unistd.h>    
#include<sys/types.h>    
    
int main()    
{
       
  printf("开始测试\n");    
  //调用execl    
  int ret = execl("/usr/bin/python3","python3","test.py",NULL);                                                        
  printf("执行结束 %d\n",ret);    
  return 0;    
} 

execv

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

这里的argv[]表示需要用数组来传程序名称和要执行的操作。

#include<stdio.h>    
#include<unistd.h>    
#include<sys/types.h>    
      
int main()    
{
       
  printf("开始测试\n");    
  //调用execv    
  char* const argv[]={
       
  	"ls",    
  	"-a",    
  	"-l",    
  	"-n",   
     NULL    
  };    
  int ret = execv("/usr/bin/ls",argv);                                                                               
  printf("执行结束 %d\n",ret);    
  return 0;    
}

#include<stdio.h>    
#include<unistd.h>    
#include<sys/types.h>    
      
int main()    
{
       
  printf("开始测试\n");    
  //调用execv    
  char* const argv[]={
       
  	"python3",    
  	"test.py",     
  	 NULL    
  };    
  int ret = execv("/usr/bin/python3",argv);                                                                               
  printf("执行结束 %d\n",ret);    
  return 0;    
}

execlp

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

file:系统会去PATH当中找,不需要传准确地址了。

#include<stdio.h>    
#include<unistd.h>    
#include<sys/types.h>    
    
int main()    
{
       
  printf("开始测试\n");    
  //调用execl    
  int ret = execlp("python3","python3","test.py",NULL);                                                        
  printf("执行结束 %d\n",ret);    
  return 0;    
} 

Tips:只会在PATH环境变量当中去找,如果不在PATH环境变量中是找不到的,会报错。

execvp

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

文件名+数组传参

#include<stdio.h>    
#include<unistd.h>    
#include<sys/types.h>    
    
int main()    
{
   
  char* const argv[]={
   
      "pytnon3",
      "test.py",
      NULL
  };
  printf("开始测试\n");    
  //调用execl    
  int ret = execlp("python3",argv);                                                        
  printf("执行结束 %d\n",ret);    
  return 0;    
} 

execle/execvpe/execve

int execle(const char *path, const char *arg,
                  ..., char * const envp[]);
int execvpe(const char *file, char *const argv[],
                   char *const envp[]);
int execve(const char *filename, char *const argv[],
           char *const envp[]);

都是可以自己传递环境变量的。

通过一个C++程序来测试环境变量:

#include <iostream>
#include <stdlib.h>
using namespace std;

int main()
{
   
    cout << "hello c++" << endl;
    cout << "-------------------------------------------\n";
    cout << "PATH:" << getenv("PATH") << endl;
    cout << "-------------------------------------------\n";
    cout << "MYPATH:" << getenv("MYPATH") << endl;
    cout << "-------------------------------------------\n";
    
    return 0;
}

可以看到MYPATH什么都没有,这是因为环境变量当中没有MYPATH

手动添加MYPATHexport MYPATH=123456

手动取消MYPATHunset MYPATH

然后通过execve来调用这个程序。

#include<stdio.h>    
#include<unistd.h>    
#include<sys/types.h>    
      
int main()    
{
       
  char* const argv[]={
       
    "./test.cpp",    
      NULL    
  };    
  extern char** environ;    
  char* const envp[]={
       
      
  };    
  printf("开始测试\n");    
  //调用execl        
  int ret = execve("/root/study/mod/test",argv,environ);                                                             
  printf("执行结束 %d\n",ret);    
  return 0;    
} 

自定义环境变量

#include<stdio.h>    
#include<unistd.h>    
#include<sys/types.h>    
#include<stdlib.h>    

int main()    
{
       
  char* const argv[]={
       
      "./test.cpp",    
      NULL    
  };    
      
  extern char** environ;                                                                                             
  char* const envp[]={
       
      "PATH=NEW PATH",    
      "MYPATH=108108",    
      NULL    
  };    
  printf("开始测试\n");    
  //调用execl        
  int ret = execve("/root/study/mod/test",argv,envp);    
  printf("执行结束 %d\n",ret);    
  return 0;    
}  

子进程会继承父进程的环境变量就是通过传递PATH实现的。

Tips:execve为系统接口,其他所有接口都是在execve的基础上稍作修改实现的。

相关推荐

最近更新

  1. TCP协议是安全的吗?

    2023-12-05 15:16:31       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2023-12-05 15:16:31       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2023-12-05 15:16:31       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2023-12-05 15:16:31       18 阅读

热门阅读

  1. AR技术详解

    2023-12-05 15:16:31       36 阅读
  2. UVM中的config_db机制传递interface

    2023-12-05 15:16:31       39 阅读
  3. 上机十 文件的读取

    2023-12-05 15:16:31       33 阅读
  4. 【单片机】单片机裸机实现多任务调度

    2023-12-05 15:16:31       38 阅读
  5. leetcode704. 二分查找

    2023-12-05 15:16:31       37 阅读