Linux 进程控制

目录

进程的创建

进程如何让创建

fork 创建子进程

进程的退出

进程退出系统会做什么

进程退出的常见方式

程序退出的方法

进程的等待

获取子进程的退出状态

阻塞与非阻塞

总结

进程替换

进程替换是什么

程序替换怎么做

execl

execv

execlp

execvp

execvpe

为什么需要进程替换


进程的创建

进程如何让创建

  • 进程的创建,前面就已经说过了,在我们目前有两种创建进程的方法:

    1. 我们在 Linux 下 ./ .exe 文件,这样就算我们手动的创建了一个进程。

    2. 调用系统调用 fork() 函数来创建进程。

  • 进程的创建里面,我们还是主要说一下 fork()。

       #include 
       pid_t fork(void);
  • 返回值:子进程返回 0,父进程返回子进程的pid

fork 创建子进程

fork 后系统会做什么呢?

  • 首先,我们知道 fork 是创建一个子进程,那么创建进程当然是系统中增加了一个进程。

  • 前面我们说了,一个进程需要有自己的 PCB 和其他的一些内核数据结构。

  • 所以首先系统会为该进程创建属于他的内核数据结构,在我们目前学习到的里面有(tast_struct 和 mmstruct)。

  • 那么只有内核的数据结构还是不够的,前面还说过,进程 = PCB + 代码和数据,所以还需要自己的代码和数据。

  • 而既然需要代码和数据,那么当然是需要加载到内存中,所以系统还会为该进程分配自己的空间。

总结一下:

  1. 为进程创建内核数据结构。

  2. 为进程分配空间。

继续理解 fork

  • 当我们在父进程中调用 fork 函数,那么当创建好后,子进程会怎么做?

  • 会执行父进程的代码!

  • 那么子进程是那里来的代码和数据呢?我们前面说了, fork 创建子进程后,子进程会拷贝父进程的代码和数据。

  • 那么是全部都拷贝吗?还是说只拷贝一部分?

  • 前面我们说过,数据是写时拷贝的,那么是所有的数据都是在修改的时候拷贝吗?

  • 这里继续想一个问题,CPU 是怎么知道,我们的这个进程执行到哪条代码了?

  • 在 CPU 中有一个 PC 指针,而CPU 每一次都只会执行下一条代码,而 PC 指针就是指向的下一条代码。

  • 那么父进程和子进程的PC 指针的指向大概率是不一样的,所以PC 指针的指向是不同的,那么PC 指针也是写实拷贝吗?

  • 其实在父子进程中,一般的数据是写是拷贝的,而CPU寄存器中存储的数据(进程的上下文),还有私有栈空间里面的数据都是私有的,并不能共享,而其他的代码是共享的,而数据却是写时拷贝的。

  • 这里为什么要让代码时共享的,而数据时写时拷贝呢?

  • 如果因为代码时不会被修改的,所以即使是给子进程拷贝了一份相同的代码那么内存中有两份相同的数据,那么不是对空间的一种浪费吗?

  • 数据也是同样的道理,如果把所有的数据都拷贝给你,那么你当下就能用到这些数据吗?甚至有些数据就是只读的,那么内存中需要有两份相同的数据吗?

fork 后的代码是全部共享,还是只有 fork 后的代码共享?

  • 我们知道 fork 后 子进程是从 fork 后的代码开始执行的,那么fork 前的代码是共享的吗?

  • 回答:是共享的!

  • 这里虽然是共享的,但是子进程可以不执行前面的代码呀!

  • 这里和前面说的一样,因为CPU 的寄存器里面有一个 PC 指针,来控制CPU 下一次执行的代码的。

  • 而PC 指针又属于是进程的上下文,所以是私有的,每个进程的PC指针都是不一样的。

  • 所以在 fork 创建子进程后,子进程的 PC 指针就是指向fork 后的那条代码的,所以fork 后,子进程就指向后面的代码。

  • 但是子进程指向哪里的代码和代码是否共享是没有关系的,我们也可以让子进程执行前面的代码,我们可以在子进程执行的逻辑中在调用 main函数,让子进程从头开始执行。

进程的退出

进程退出系统会做什么

  • 既然我们知道创建一个进程系统会为该进程创建对应的内核数据结构与分配空间。

  • 那么相反的是,进程退出系统也会回收掉分配的空间,也会将对应的数据结构也销毁掉。

进程退出的常见方式

  • 如果一个进程退出,那么该进程的结果无非又三种。

    1. 代码运行完了,结果正确。

    2. 代码运行完了,结果不正确。

    3. 代码直接奔溃。

  • 那么上面三种就是进程退出的所有可能。

那么我们怎么知道一个进程运行结束后,结果是否正确呢?

回答上面的这个问题之前,我们在像一个问题,我们的C/C++的 main 函数结束后,每次都 return 的作用是什么?

  • 实际上,main 函数的 return 表示的是一个进程的退出码!

  • 而每一个退出码都表示一种状态,不同的退出码,表示的含义不同。

查看一个退出码的含义可以实用一个函数 strerror

NAME
       strerror, strerror_r - return string describing error number
​
SYNOPSIS
       #include 
​
       char *strerror(int errnum);

下面写一段代码,看一下 Linux 中一共又多少退出码!

void test1()
{
  for(int i = 0; i < 150; ++i)
  {
    cout << "退出码[" << i  << "]" << strerror(i) << endl;
  }
}
  • 上面没的这段代码,打印了 0 ~ 149 的多有退出码,但是我们并不确定Linux中一共有多少退出码。

  • 所以这里先打印这么多,不够再加。

结果:

退出码[0]Success
退出码[1]Operation not permitted
退出码[2]No such file or directory
退出码[3]No such process
退出码[4]Interrupted system call
...
退出码[132]Operation not possible due to RF-kill
退出码[133]Memory page has hardware error
退出码[134]Unknown error 134
退出码[135]Unknown error 135
退出码[136]Unknown error 136
  • 上面的退出码太多了,所以这里就不全部都放出来了,只看一部分。

  • 这里我们能看到退出码最多到 136 就结束了。

再 Linux中,如何查看一个程序的退出码呢?

如果再命令行上,可以实用 echo &? 可以查看最近的一个进程的退出码。

[lxy@hecs-165234 linux101]$ echo $?
0
  • 这个就是刚才的程序的退出码,而 0 就表示成功。

而前面说了 mian 函数的 return 就是表示的是一个程序的退出码,那么可以看一下试验:

int main()
{
​
  return 10;
}
  • 这里故意将 main 函数的返回值设置为 10 所以运行一下,看一下结果

[lxy@hecs-165234 linux101]$ ./myproc 
[lxy@hecs-165234 linux101]$ echo $?
10
  • 这里看到返回值确实是上面 main 函数的返回值。

那么如何可以看到运行完了,结果不正确呢?

实际上,我们可再 main函数中调用其他的函数,如果其他的函数的结果和预期的不符,那么我们就可以设置返回值,让其的返回值表示不同的错误。

那么如果是程序奔溃呢?

int main()
{ 
  int n = 10;
  cout << n / 0 << endl;
  return 10;
}
  • 上面这里,我们有一个除0错误,我们运行后然后看一下程序退出码

[lxy@hecs-165234 linux101]$ ./myproc 
Floating point exception
[lxy@hecs-165234 linux101]$ echo $?
136
  • 实际上,如果程序是奔溃掉的,那么退出码就已经没有意义了。

  • 因为程序是直接奔溃掉了,那么说明程序都没有运行结束。

  • 那么代码都没有跑完,那么退出码还有意义吗?

程序退出的方法

  • 程序的退出方法有多种:

  • 第一种就是常见的 return

int main()
{
  test2();
  return 10;
}
  • 首先如果是这样的话,那么退出码当然会是 10,因为 return 就是进程退出的一种方式。

  • 那么如果是 tets2 函数里面 ret 呢?

int test2()
{
  int sum = 0;
  for(int i = 0; i < 10; ++i)
  {
    sum += i;
  }
  return sum;
}
​
int main()
{
  int ret = test2();
  
  
  return 10;
}

结果:

[lxy@hecs-165234 linux101]$ ./myproc 
[lxy@hecs-165234 linux101]$ echo $?
10
  • 这里的结果还是 10,说明 test2 函数里面 return 不呢个让程序退出。

  • 所以现在说的再清楚一点。

  • 只有 main 函数里面的 return 才是能让程序退出,而其他函数里面的 return 只能作为返回值。

  • 程序退出还有其他的方式。

  • exit 函数

NAME
       exit - cause normal process termination
​
SYNOPSIS
       #include 
​
       void exit(int status);
  • exit 这个函数我们再C语言上就有,那么他能会使函数退出吗?

int test3()
{
  int sum = 0;
  for(int i = 0; i < 50; ++i)
  {
    sum += i;
  }
  return sum;
}
​
int main()
{
  int ret =  test3();
​
  exit(ret);
  
  return 0;
}
  • 如果是这样的话,那么退出码是多少呢?

[lxy@hecs-165234 linux101]$ ./myproc 
[lxy@hecs-165234 linux101]$ echo $?
201
  • 这里看到退出码是我们传入的值。

  • 那么 return 和 exit 的区别是什么呢?

int test3()
{
  int sum = 0;
  for(int i = 0; i < 50; ++i)
  {
    sum += i;
  }
  
  exit(99);
​
  return sum;
}
​
int main()
{
  int ret =  test3();
​
  exit(ret);
  
  return 0;
}
  • 如果再 test3 函数里面直接调用 exit 函数呢?

[lxy@hecs-165234 linux101]$ ./myproc 
[lxy@hecs-165234 linux101]$ echo $?
99
  • 这里看到是 test3 函数里面 exit 传入的值。

  • 所以我们也就知道了 exit 和 return 的区别。

  • exit 可以再任意函数里面直接退出进程,而 return 只能再 main 函数里面才能终止进程。

  • 实际上还有一个函数可以终止进程 _exit

NAME
       _exit, _Exit - terminate the calling process
​
SYNOPSIS
       #include 
​
       void _exit(int status);
​
       #include 
​
       void _Exit(int status);
  • 这里有两个 _exit 只不过一个是大写E 一个是小写e

  • 但是两个函数是一样的。

  • 那么这个 _exit 和 exit 是什么关系呢?

  • _exit 是系统调用,而 exit 是库函数(C语言的库函数)。

  • 那么他们两个有什么区别吗?

void test4()
{
  printf("hello world\n");
  sleep(3);
  exit(1);
}
​
int main()
{
  test4();
  return 0;
}
  • 上面的这段代码会有什么效果?

  • 他一旦运行会打印出 hello world,然后sleep 3 秒,最后退出

[lxy@hecs-165234 linux101]$ ./myproc 
hello world
  • 那么我们要是去掉 \n 呢?

  • 如果我们去掉\n 的话,由于再C语言里面是行刷新策略,所以他刚开始不打印,等程序结束的时候才打印。

void test4()
{
  printf("hello world");
  sleep(3);
  exit(1);
}
​
int main()
{
  test4();
  return 0;
}
[lxy@hecs-165234 linux101]$ ./myproc 
hello world[lxy@hecs-165234 linux101]$ 
  • 那么我们将 exit 换成 _exit会有什么样的效果?

void test4()
{
  printf("hello world\n");
  sleep(3);
  exit(1);
}
​
int main()
{
  test4();
  return 0;
}
[lxy@hecs-165234 linux101]$ ./myproc 
[lxy@hecs-165234 linux101]$ 
  • 这里的结果是什么都没有打印出来。

  • 我们前面说了再C语言打印的时候,是行刷新策略。

  • 如果没有换行的话,缓冲区的的数据又不足一行,那么就会等到程序结束的时候刷新。

  • 而_exit 终止程序的话,没有刷新出来。

  • 而 _exit 是系统调用的接口,如果该缓冲区是系统上维护的缓冲区的话,那么 _exit 也是可以刷新出来结果的,但是 _exit 没有刷新出来,说明该缓冲区并不是系统维护的。

  • 所以说明了 printf 的缓冲区是是上层自己维护的。

进程的等待

  • 现在我们知道,进程退出是有结果的。

  • 那么我们现在创建一个子进程,如果我们想知道该进程的退出结果,我们要怎么做呢?

  • 那么如果我们创建的子进程我们不需要知道他的结果,但是我们什么都不做的话,他会成为僵尸进程,导致内存泄露,那么又该怎么办呢?

下面我们就回答上面的两个问题!

首先回答第一个问题!

获取子进程的退出状态

  • 如果我们想得知子进程的退出状态的话,我们可以实用两个函数。

  • wait 函数 和 waitpid 函数

NAME
       wait, waitpid, waitid - wait for process to change state
​
SYNOPSIS
       #include 
       #include 
​
       pid_t wait(int *status);
​
       pid_t waitpid(pid_t pid, int *status, int options);
  • 其实这两个函数不仅可以解决获取子进程退出的状态信息,还可以解决子进程僵尸问题!

  • 其中 wiat 里面的参数是一个输出型参数,只要又这个参数,那么父进程就可以获取到子进程的退出状态。

  • 如果不需要或等子进程的退出状态,那么就传 NULL。

  • 而 waitpid 里面的第一个参数是 pid 表示要等的子进程的 id,如果传为 -1 表示等任意子进程。

  • 第二个参数的意思和wait 里面的意思是一样的。

  • 第三个参数现在先不介绍,默认传 0 即可。

int main()
{
  pid_t id = fork();
  if(id < 0)
  {
    // 子进程创建失败!
    perror("fork ");
  }
  else if(id == 0)
  {
    // 子进程
   int count = 5;
   while(count)
   {
     printf("我是子进程  count: %d, pid: %d, ppid: %d\n", count, getpid(), getppid());
     --count;
     sleep(1);
   }
  }
  else 
  {
    // 父进程
    while(1)
    {
      printf("我是父进程 pid: %d, ppid: %d\n", getpid(), getppid());
      sleep(1);
    }
  }
  return 0;
}
  • 如果是上面这样子的话,那么子进程会先退出,但是i父进程没有 wiat 子进程所以子进程会进入僵尸状态。

  • 所以我们可以 wait 子进程。

int main()
{
  pid_t id = fork();
  if(id < 0)
  {
    // 子进程创建失败!
    perror("fork ");
  }
  else if(id == 0)
  {
    // 子进程
   int count = 5;
   while(count)
   {
     printf("我是子进程  count: %d, pid: %d, ppid: %d\n", count, getpid(), getppid());
     --count;
     sleep(1);
   }
  }
  else 
  {
    // 父进程
    int status = 0;
    wait(NULL);
  }
  return 0;
}

结果:

[lxy@hecs-165234 linux101]$ ./myproc 
我是子进程  count: 5, pid: 16873, ppid: 16872
我是子进程  count: 4, pid: 16873, ppid: 16872
我是子进程  count: 3, pid: 16873, ppid: 16872
我是子进程  count: 2, pid: 16873, ppid: 16872
我是子进程  count: 1, pid: 16873, ppid: 16872
这里我们写一个监控脚本看一下
 
while :; do ps axj | head -1 && ps axj | grep myproc | grep -v grep; sleep 1; done;
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13038 16872 16872 13038 pts/0    16872 S+    1000   0:00 ./myproc
16872 16873 16872 13038 pts/0    16872 S+    1000   0:00 ./myproc
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13038 16872 16872 13038 pts/0    16872 S+    1000   0:00 ./myproc
16872 16873 16872 13038 pts/0    16872 S+    1000   0:00 ./myproc
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13038 16872 16872 13038 pts/0    16872 S+    1000   0:00 ./myproc
16872 16873 16872 13038 pts/0    16872 S+    1000   0:00 ./myproc
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13038 16872 16872 13038 pts/0    16872 S+    1000   0:00 ./myproc
16872 16873 16872 13038 pts/0    16872 S+    1000   0:00 ./myproc
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13038 16872 16872 13038 pts/0    16872 S+    1000   0:00 ./myproc
16872 16873 16872 13038 pts/0    16872 S+    1000   0:00 ./myproc
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
  • 这里我们看到,我们前面两个进程一直都在,然后等子进程结束的那一刻,父进程也马上就结束了

  • 这里因为父进程一直在等待子进程,所以当子进程结束的那一刻,父进程也就结束了

  • 下面我们可以让父进程多休眠几秒

[lxy@hecs-165234 linux101]$ ./myproc 
我是子进程  count: 5, pid: 17039, ppid: 17038
我是子进程  count: 4, pid: 17039, ppid: 17038
我是子进程  count: 3, pid: 17039, ppid: 17038
我是子进程  count: 2, pid: 17039, ppid: 17038
我是子进程  count: 1, pid: 17039, ppid: 17038
PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13038 17038 17038 13038 pts/0    17038 S+    1000   0:00 ./myproc
17038 17039 17038 13038 pts/0    17038 S+    1000   0:00 ./myproc
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13038 17038 17038 13038 pts/0    17038 S+    1000   0:00 ./myproc
17038 17039 17038 13038 pts/0    17038 S+    1000   0:00 ./myproc
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13038 17038 17038 13038 pts/0    17038 S+    1000   0:00 ./myproc
17038 17039 17038 13038 pts/0    17038 S+    1000   0:00 ./myproc
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13038 17038 17038 13038 pts/0    17038 S+    1000   0:00 ./myproc
17038 17039 17038 13038 pts/0    17038 S+    1000   0:00 ./myproc
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13038 17038 17038 13038 pts/0    17038 S+    1000   0:00 ./myproc
17038 17039 17038 13038 pts/0    17038 Z+    1000   0:00 [myproc] 
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
13038 17038 17038 13038 pts/0    17038 S+    1000   0:00 ./myproc
17038 17039 17038 13038 pts/0    17038 Z+    1000   0:00 [myproc] 
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
  • 这里我们看到了子进程有了两秒的 Z 状态,这里因为子进程退出后的两面,父进程还在sleep

  • 实际上 wait 和 waitpid 基本是一样的,我们看一下 waitpid

  • 但是这一次我们来看一下子进程的退出码。

  • 我们让子进程退出码设置为 20.

int main()
{
  pid_t id = fork();
  if(id < 0)
  {
    // 子进程创建失败!
    perror("fork ");
  }
  else if(id == 0)
  {
    // 子进程
   int count = 5;
   while(count)
   {
     printf("我是子进程  count: %d, pid: %d, ppid: %d\n", count, getpid(), getppid());
     --count;
     sleep(1);
   }
   exit(20);
  }
  else 
  {
    // 父进程
    int status = 0;
    sleep(7);
    waitpid(-1, &status, 0);
    printf("status: %d\n", status);
  }
  return 0;
}

结果:

[lxy@hecs-165234 linux101]$ ./myproc 
我是子进程  count: 5, pid: 17440, ppid: 17439
我是子进程  count: 4, pid: 17440, ppid: 17439
我是子进程  count: 3, pid: 17440, ppid: 17439
我是子进程  count: 2, pid: 17440, ppid: 17439
我是子进程  count: 1, pid: 17440, ppid: 17439
status: 5120
  • 这里打印出来的status 是 5120? 不是我们设置的20.

  • 这里要说明一下,实际上 status 是 int 类型的,但是这里并不是使用整个 status

  • 而是只使用 status的后面 16 个字节

  • 而次 8 个字节用来存储 退出码,而后面 7 个字节存储的是终止信号(也就是被那个信号终止的)

  • 如果不是被信号终止,那么后7个字节存储的是 0

  • 而第8个字节存储的是 core dump 标志,这歌标志表示进程是否正常退出,先不说这个。

  • 所以我们想看退出码,那么我们就要看后次8个字节

  • 怎么看呢?

(status >> 8) & 0xFF
  • 这里只需要让 status 向右移动 8 个字节然后与 0xFF 与 (0xFF -> 0000 0000 ... 1111 1111)

  • 那么如何看信号呢?

status & 0x7F)
  • 信号再后7位,所以只需要让 status 与 0x7F(0x7F -> 0000 0000 ... 0111 1111)

 
  

结果:

[lxy@hecs-165234 linux101]$ ./myproc 
我是子进程  count: 5, pid: 17783, ppid: 17782
int main()
{
  pid_t id = fork();
  if(id < 0)
  {
    // 子进程创建失败!
    perror("fork ");
  }
  else if(id == 0)
  {
    // 子进程
   int count = 5;
   while(count)
   {
     printf("我是子进程  count: %d, pid: %d, ppid: %d\n", count, getpid(), getppid());
     --count;
     sleep(1);
   }
   exit(20);
  }
  else 
  {
    // 父进程
    int status = 0;
    sleep(7);
    waitpid(-1, &status, 0);
    printf("status: %d, 退出信号:%d\n", (status >> 8) & 0xFF, status & 0x7F);
  }
  return 0;
}
我是子进程  count: 4, pid: 17783, ppid: 17782
我是子进程  count: 3, pid: 17783, ppid: 17782
我是子进程  count: 2, pid: 17783, ppid: 17782
我是子进程  count: 1, pid: 17783, ppid: 17782
status: 20, 退出信号:0

既然我们也知道如何获取子进程的退出的状态码了,那么下面我们有几个问题!

为什么要这么麻烦的获取子进程的退出码呢?我们能不能直接再全局定义一个变量,然后让子进程退出的时候修改该变量呢?

  • 不可以,因为子进程和父进程的数据是写时拷贝的,一旦有人修改,那么系统会单独开一块空间,来存储该变量让其修改。

还有一问题,既然说进程时独立的,那么wait/waitpid 能获取到进程的退出码吗?进程的退出码不也是进程的数据吗?

  • 可以的!因为 wait/waitpid 是系统调用,系统调用当然可以访问自己管理的数据!

  • 而我们获得退出码,也是让 wait/waitpid 从进程的PCB 里面就获取的。

  • 因为只要没有人处理退出的子进程,那么子进程的PCB 会一直保留,直到有人回收为止。

上面索然获取到了子进程的退出状态,但是上面获取的方式比较麻烦,那么有没有简单一点的获取方法呢?

  • WIFEXITED:这个是一个宏函数,如哦返回值为非0,表示子进程正常结束,所以可以获取退出码!

  • WEXITSTATUS:该宏可以获得子进程的退出码,其实里面的实现也就是我们上面写的那段代码!

   
 // 父进程
    int status;
    pid_t ret = waitpid(-1, &status, 0);
    if(WIFEXITED(status))
    {
      printf("exit_code: %d\n", WEXITSTATUS(status));
    }
    else 
    {
      // 等待失败
    }

结果:

我是子进程 count: 5
我是子进程 count: 4
我是子进程 count: 3
我是子进程 count: 2
我是子进程 count: 1
exit_code: 20

阻塞与非阻塞

  • 上面我们 waitpid 的时候,左后一个参数默认传了 0,那么传 0 表示什么意思呢?

  • 0 表示父进程再等待的时候是以阻塞方式等待的。

  • 那么什么是阻塞?什么是非阻塞呢?

    1. 阻塞其实再前面,进程状态的时候,我们也说过,实际上,阻塞就是系统将该进程由运行状态改为s状态。

    2. 那么是如何实现的呢?下面写一段伪代码来看一下 waitpid 的阻塞与非阻塞的实现!

      pid_t waitpid(pid_t id, int* status, int options)
      {
          // 检测 id 是否运行结束
          // 如果 id 是 -1 的话,那么就是任意一个子进程
          if(id 结束)
          {
              status |= exit_code;
              status |= exit_signal;
              ...
              return id;
          }
          else if(id 没有结束)
          {
              // 这里没有结束,所以需要判断是否为非阻塞的等待
              if(id == 0)
              {
                  // 阻塞等待
                  阻塞....;
              }
              else
              {
                  //非阻塞等待
                  return id;
              }
          }
          else
          {
              // 出问题了
          }
      }

  • 既然知道了阻塞与非阻塞,那么我们来看一下如何实现阻塞的等待!

  • 既然是非阻塞的等待,那么就不能只等待一次,因为只等待一次的话,如果子进程没有结束,那么后面也不会再等待了,等子进程结束后,子进程由会变成僵尸进程。

  • 所以这里如果是非阻塞的等待的话,一定需要是轮询检测,也就是 基于非阻塞等待的轮询检测模式。

这里我们知道了由两种等待模式:

  1. 阻塞

  2. 非阻塞

那么这为什么要由两种等待的模式呢?

这里我们想一下 fork 创建子进程是用来干嘛的?

  1. 创建子进程为了执行和父进程同一份代码里面的不同分支的代码!

  2. 为了让子进程执行和父进程完全不同的代码!

所以子进程创建子进程为了让子进程和父进程做不同的事情,那么如果父进程创建子进程后,父进程还要一直阻塞的等待子进程,那么父进程不就什么都做不了了吗?那么创建子进程的意义何在?

所以创建子进程是为了做不同的事情,而非阻塞等待,就可以让父进程再不阻塞的情况下,还可以回收子进程,同时也可以做自己的事情。

下面我们可以看一下代码如何实现!

int main()
{
  pid_t id = fork();
  
  if(id == 0) // 子进程
  {
    int count = 5;
    while(count)
    {
      printf("我是子进程 count: %d\n", count--);
      sleep(1);
    }
    exit(1);
  }
  else // 父进程 
  {
    int status = 0;
    int quit = 0;
    while(!quit)
    {
      pid_t ret = waitpid(-1, &status, WNOHANG);
      if(ret > 0)
      {
        // 表示等待成功,程序已经退出
        printf("等待成功~~~~\n");
        if(WIFEXITED(status))
        {
          printf("退出码:%d\n", WEXITSTATUS(status));
        }
        quit = 1;
      }
      else if(ret == 0)
      {
        // 表示等待成功,但是程序没有退出
        printf("等待成功,但是子进程还没有退出~\n");
      }
      else 
      {
        //表示等待失败
        quit = 1;
      }
      sleep(1);
    }
  }
​
  return 0;
}
  • 上面主要看父进程等待的代码,由于是非阻塞的,所以需要一直等待。

  • 但是上面我们看到 waitpid 里面的左后一个参数是 WNOHANG

  • WNOHANG:表示等待的时候不要 夯住,其实这个值也可以传为 1。

  • 但是由于这些系统函数突然冒出来一个不知名数字会让人难以理解,所以一般这些数字都是用宏来代替。

  • 而waitpid 再非阻塞模式下其实是有三个返回值的。

  • 大于0:表示等待成功,且子进程退出了,那么子进程退出了,父进程也就没有必要一直等待了。

  • 等于0:表示等待成功,但是子进程没有退出,那么这时候我们就需要继续等待!

  • 小于0:表示等待失败了,既然等待失败了,那么也就没有必要等待了,所以直接退出即可。

但是凭什么收此时的父进程没有阻塞呢?

  • 再代码里面,我们再等待成功,但是子进程没有退出的情况下打印了消息。

  • 如果子进程没有退出,且再阻塞模式下,此时父进程是什么都做不了的。

  • 现在是非阻塞模式,我们看一下是否可以成功打印消息。

结果:

[lxy@hecs-165234 linux102]$ ./myproc 
等待成功,但是子进程还没有退出~
我是子进程 count: 5
等待成功,但是子进程还没有退出~
我是子进程 count: 4
等待成功,但是子进程还没有退出~
我是子进程 count: 3
等待成功,但是子进程还没有退出~
我是子进程 count: 2
等待成功,但是子进程还没有退出~
我是子进程 count: 1
等待成功,但是子进程还没有退出~
等待成功~~~~
退出码:1

总结

  • 进程等待是什么:就是一个进程被系统给阻塞住了,然后等待某一种系统资源,直到这种系统资源到了的时候,系统又会将这该进程给恢复运行(唤醒)。

  • 为什么需要等待:因为如果子进程不等待的话,那么子进程退出后没有人回收子进程,导致子进程变成僵尸进程而浪费系统资源,或者是父进程需要拿到子进程的退出信息,而等待子进程。

  • 如何等待:wait/waitpid

进程替换

进程替换是什么

  • 进程替换就是一个进程的代码和数据被替换为了其他的代码和数据,然后执行其他的代码和数据。

那么进程是如何替换的?

前面我们说了,再 Linux 种创建一个进程的时候,会创建进程对应的 task_struct、 mm_struct、页表等。

  • 那么这里有一个疑问,进程替换需要创建新的进程吗?

  • 当然进程替换是一个已经存在的进程,然后去替换一个还没有被加载到内存中的代码和数据!

  • 所以这里的进程替换实际上就是将代码和数据加载到内存中。

  • 而既然是一个存在的进程替换还没有加载到内存中的代码和数据,实际上是不需要创建新的进程的!

  • 因为有了页表的存在,我们只需要将新的代码和数据,通过页表映射,将代码和数据指向新的代码。

  • 让程序执行新的代码和数据,这样就完成了程序的替换。

程序替换怎么做

  • 程序替换主要是通过系统提供的函数来实现!

  • 下面我们主要以代码为主!

首先看一下关于程序替换的函数,下面我会挑一些介绍:

NAME
       execl, execlp, execle, execv, execvp, execvpe - execute a file
​
SYNOPSIS
       #include 
​
       extern char **environ;
​
       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[]);
  • 上面的这些函数是 exec 系列的一些函数,主要功能都是讲一个程序替换成其他的程序。

  • 这些程序如果替换成功的话,是没有返回值的,如果替换失败,那么返回值就是 -1。

  • 而上面这些函数的区别仅仅是传入的参数不同。

execl
int execl(const char *path, const char *arg, ...);
  • 首先介绍一下这里的参数。

  • path:表示想要替换的程序的路径(这里的路径既可以是绝对路径,也可以是相对路径)

  • argv:argc这里是一个 char 类型的指针里面就是写的是如何执行这个函数

  • ... :可变参数列表,可以传入任意类型个数的的参数,但是主要是为了替换程序的命令行参数,与argv一样

  • 而可变参数列表的最后一个必须要传 NULL

下面我们可以写一个很简单的函数,来看一下这个函数:

  • 这里我们想要讲一个我们自己的程序替换成,系统的 ls 命令(之前说过,系统命令也是函数)。

int main()
{
  printf("开始替换程序\n");
​
  // ls 的路径再系统 /usr/bin/ls 里面,而第二个参数开始就是表示我们想要如何执行这个命令
  // ls 命令既可以带选项,也可以不带选项,下面我们先不带选项
  execl("/usr/bin/ls", "ls", NULL);
​
  printf("结束替换程序\n");
  return 0;
}

结果:

[lxy@hecs-165234 linux102]$ ./exec 
开始替换程序
exec  exec.c  makefile  myproc.cc
  • 这里看到,确实执行了 ls 命令

  • 但是我们上面又两句打印,为什么只有上面的那一句被打印出来了?

  • 因为 exec 是一个程序替换,如果替换失败的话,那么是有返回值的,也就是 -1。

  • 但是替换成功的话,表示所有的进程的所有代码和数据都被替换成了目标程序的代码和数据。

  • 所以旧程序后面的 printf 当然是不会被执行的。

下面我们可以带选项看一下替换的 ls:

int main()
{
  printf("开始替换程序\n");
​
  execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
​
  printf("结束替换程序\n");
  return 0;
}
  • 这里将如何执行后面还加了 -a -l 选项,那么我们执行看是否何预期的效果相同呢?

结果:

[lxy@hecs-165234 linux102]$ ./exec 
开始替换程序
total 32
drwxrwxr-x  2 lxy lxy 4096 Oct  5 17:04 .
drwxrwxr-x 41 lxy lxy 4096 Oct  5 10:14 ..
-rwxrwxr-x  1 lxy lxy 8408 Oct  5 17:04 exec
-rw-rw-r--  1 lxy lxy  184 Oct  5 17:04 exec.c
-rw-rw-r--  1 lxy lxy   61 Oct  5 16:53 makefile
-rw-rw-r--  1 lxy lxy 1748 Oct  5 11:07 myproc.cc
execv
int execv(const char *path, char *const argv[]);
  • 这个函数何上面的那个有什么区别吗?

  • 前面的 path 参数是没有任何区别的,还是传入想要替换目标程序的路径。

  • 但是后面的 argv 和 可变参数列表成了一个指针数组,那么这个如何传参呢?

  • 实际上这个指针数组就是第一个函数从第二个参数开始的内容全都写到该指针数组里面就可以了。

  • 下面还是使用 ls 做试验

int main()
{
  printf("开始替换程序\n");
​
  // 构建第二个参数
  char* argv[] = {
    "ls",
    "-l",
    "-a",
    NULL
  };
    
  execv("/usr/bin/ls", argv);
​
  printf("结束替换程序\n");
  return 0;
}
  • 其实这个函数比第一个函数还要简单一些

结果:

[lxy@hecs-165234 linux102]$ ./exec 
开始替换程序
total 32
drwxrwxr-x  2 lxy lxy 4096 Oct  5 17:14 .
drwxrwxr-x 41 lxy lxy 4096 Oct  5 10:14 ..
-rwxrwxr-x  1 lxy lxy 8408 Oct  5 17:14 exec
-rw-rw-r--  1 lxy lxy  281 Oct  5 17:14 exec.c
-rw-rw-r--  1 lxy lxy   61 Oct  5 16:53 makefile
-rw-rw-r--  1 lxy lxy 1748 Oct  5 11:07 myproc.cc
  • 这里的结果和前面的还是一样的

这里总结一下前两个函数:

  • 如果 exec 带 l 的话,那么传参的方式像是以 list 的方式一个一个传参

  • 如果 exec 带 v 的话,那么传参的方式像 vector 一样(也就是数组)的方式

execlp
int execlp(const char *file, const char *arg, ...);
  • 这里看到这个函数带了 l 说明传参的方式像 list 一样,一个一个传参

  • 但是这歌函数还有一个 p ,那么 p 与前面的函数有什么不同呢?

  • 实际上增加了这个 p 与前面的函数的第一个参数是有区别的。

  • 没有 p 的函数第一个参数是 path 也就是路径,而增加了 p 表示可以传想要替换程序的名。

  • 而如果直接传名称的话,那么他会在系统的环境变量 PATH 里面搜索是否这个函数,不需要传路径。

以 ls 为例:

int main()
{
  printf("开始替换程序\n");
​
  // 这里使用的是 execlp 带 p 的话,可以直接传函数名,所以不需要传路径
  execlp("ls", "ls", "-l", "-a", "-i", NULL);
​
  printf("结束替换程序\n");
  return 0;
}

结果:

[lxy@hecs-165234 linux102]$ ./exec 
开始替换程序
total 32
1704167 drwxrwxr-x  2 lxy lxy 4096 Oct  5 17:22 .
1579684 drwxrwxr-x 41 lxy lxy 4096 Oct  5 10:14 ..
1704363 -rwxrwxr-x  1 lxy lxy 8408 Oct  5 17:22 exec
1704170 -rw-rw-r--  1 lxy lxy  329 Oct  5 17:22 exec.c
1704169 -rw-rw-r--  1 lxy lxy   61 Oct  5 16:53 makefile
1704168 -rw-rw-r--  1 lxy lxy 1748 Oct  5 11:07 myproc.cc
  • 这里也是可以成功的

  • 其实这几个函数的功能都是一样的,只是这几个函数的传参是不同的。

execvp
int execvp(const char *file, char *const argv[]);
  • 相信通过前面我们学到的,我们现在也可以知道这个函数的参数需要传哪些内容。

  • 首先是带了 v ,说明这个函数的传参方式是通过数组的方式传参。

  • 还有一个 p ,说明该函数传参可以不带路径

以 ls 为例:

int main()
{
  printf("开始替换程序\n");
​
  char* argv[] = {
    "ls",
    "-l",
    "-a",
    NULL
  };
  execvp("ls", argv);
  
  printf("结束替换程序\n");
  return 0;
}

结果:

[lxy@hecs-165234 linux102]$ ./exec 
开始替换程序
total 32
drwxrwxr-x  2 lxy lxy 4096 Oct  5 17:27 .
drwxrwxr-x 41 lxy lxy 4096 Oct  5 10:14 ..
-rwxrwxr-x  1 lxy lxy 8408 Oct  5 17:27 exec
-rw-rw-r--  1 lxy lxy  353 Oct  5 17:27 exec.c
-rw-rw-r--  1 lxy lxy   61 Oct  5 16:53 makefile
-rw-rw-r--  1 lxy lxy 1748 Oct  5 11:07 myproc.cc

下面我们再介绍一个函数。

execvpe
 int execvpe(const char *file, char *const argv[],
                   char *const envp[]);
  • 这个函数的带了一个 v 和 p,还有一个 e。

  • v 和 p 我们都是知道的,分别是传参通过数组,还有就是替换函数不需要带路径。

  • 那么 e 是什么?

  • 这里其实可以看函数的参数,我也可以知道, envp 这个和前面的环境变量很像,而这个参数实际上也是传环境变量的。

下面我们写一个自己的函数,然后使用execvpr替换,让我们写的函数里面可以获取一个环境变量,然后当前函数通过execvpe传入环境变量,让目标函数获取。

int main()
{
  // 获取一个环境变量
  printf("environ: %s\n", getenv("MY_ENV"));
  return 0;
}
  • 上面这个函数就是说去一个环境变量 MY_ENV。

  • 但是当前是没有改环境变量的。

现在可以运行一下这个函数看一下结果:

[lxy@hecs-165234 linux102]$ ./target 
environ: (null)

下面让exec 函数替换该函数:

int main()
{
  printf("开始替换程序\n");
  
  // 设置命令行参数
  char* argv[] = {"./target"};
  // 设置环境变量
  char* env[] = {"MY_ENV=889900"};
  execvpe("./target", argv,env );
​
  printf("结束替换程序\n");
  return 0;
}

结果:

[lxy@hecs-165234 linux102]$ ./exec 
开始替换程序
environ: 889900

这里也就看到成功了。

为什么需要进程替换

  • 这里回答一下为什么需要进程替换,实际上进程是否替换是根据需求来的。

  • 如果有需要进程替换的地方,那么就需要进程替换。

  • 如果没有需要进程替换的常见,那么当然也不需要进程替换。

你可能感兴趣的:(linux)