[ Linux ] 一篇带你搞懂进程控制(看完可实现一个简易的shell)

本文前半部分和上篇文章相同,之所以加进来是因为这两篇文章均是进程控制部分内容。如果大家已经看过前两部分内容,可直接从2.1起看!

目录

0.进程创建

fork()之后,操作系统做了什么?

写时拷贝

fork调用失败的原因

1.进程终止

关于终止的认识

$?

进程退出码

进程终止的常见做法

exit

_exit

关于终止,内核做了什么?

2.进程等待

2.1为什么要进程等待

2.2进程等待的必要性

2.3如何等待--进程等待的方法

2.3.1wait方法

2.3.2 waitpid

2.3.3waitpid()验证

2.3.4 阻塞等待和非阻塞等待

3.进程程序替换

3.1进程程序替换是什么?(概念,原理)

3.2为什么要让子进程执行一个新的程序呢?

3.3 怎么做(编码,如何进行程序替换)

3.3.1 见一下最基本的代码

3.3.2 引入进程创建

3.3.3大量的测试各种不同的接口

模拟实现shell​​​​​​​


0.进程创建

相比大家对下面这段代码已经不陌生了,我们在介绍fork()的时候就已经写过一遍了,fork()有两个返回值,同一个pid会有不同的值,这是上篇我们说到的伪内存问题。而本篇我们要看看fork()创建时,操作系统会干什么事情?

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

返回值:自进程中返回0,父进程返回子进程id,出错返回-1

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

  1. 分配新的内存块和内核数据结构给子进程
  2. 将父进程部分数据结构内容拷贝至子进程
  3. 添加子进程到系统进程列表当中
  4. fork返回,开始调度器调度
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
  pid_t id = fork();
  if(id == 0)
  {
    //child
    while(1)
    {
      printf("我是子进程,pid = %d,ppid = %d\n",getpid(),getppid());
      sleep(1);
    }
  }
  else 
  {
    while(1)
    {
      printf("我是父进程,pid = %d,ppid = %d\n",getpid(),getppid());
      sleep(1);
    }
  }
  return 0;
}

当一个进程调用fork之后,就有两个二进制代码相同的进程,而且他们都运行在相同的地方。但是每个进程都将可以开始他们自己的旅程。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
  printf("我是一个进程:pid = %d\n",getpid());
  fork();
  printf("我依旧是一个进程:pid = %d\n",getpid());
  return 0;
}

当我们运行完时发现,fork()之后代码共享。

fork()之前父进程独立执行,fork()之后,父子两个执行流分别执行。

那么fork()之后是否之后fork()之后的代码被父子进程共享的??

结论:一般情况下,fork()之后,父子共享所有的代码 ,因此fork()之后,父进程共享了全部的代码,只不过子进程只能从fork开始执行。子进程继承了父进程的eip(程序计数器),但是如果子进程想找到之前的代码也是可以的。

fork()之后,操作系统做了什么?

我们都知道进程=内核的进程数据结构+进程的代码和数据。当fork()创建的时候是创建子进程的内核数据结构(struct tast_struct + struct mm_struct... + 页表) + 代码继承父进程,数据以写实拷贝的形式来共享或者独立!因此,fork()之后,操作系统创建结构,代码以共享的形式,数据以是写实拷贝的形式来实现两个进程整体保持独立性!也就是说,父进程或者子进程如果有一方进程挂掉,不会影响另一方。

写时拷贝

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

那么为什么要写时拷贝?在创建子进程的时候就把数据分开不行吗?

答案是不行的,具体原因有一下三点:

  1. 父进程的数据,子进程不一定全用,即使使用,也不一定全部写入 ,因此会有浪费空间的嫌疑
  2. 最理想的情况,只有会被父子修改的数据进行分离拷贝,不需要修改的共享即可--但是从技术角度实现很复杂
  3. 如果fork的时候,就无脑拷贝数据的子进程,会增加fork的成本(内存和时间)

所以最终采用写时拷贝。只会拷贝父子修改的数据,变相的就是拷贝数据的最小成本,但是拷贝的成本依然存在。之所以写时,是因为这是延迟拷贝的策略,只有真正使用的时候操作系统再给你分配资源。因此这种写时拷贝变相的提高内存的使用率。

fork调用失败的原因

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

1.进程终止

关于终止的认识

我们在写C/C++程序的时候,每个程序都有一个main函数,这个函数也叫做入口函数。我们经常会习惯的写上

return 0 ,那么这里将会产生两个问题:

  • return 0 给谁返回?
  • 必须返回0吗?返回别的数字可以吗?

此时我们首先要了解到进程退出的场景:

  1. 代码跑完,结果正确
  2. 代码跑完,结果不正确
  3. 代码没跑完,程序异常了

任何一个程序无外乎这三种退出场景,本篇文章主要介绍前两种场景。我们将举一个例子来加什么对这两种场景的认识和理解:假设张三要参加期末考试,他无非就3中情况:1.正常参加考试,考了100分。2.正常参加考试,考了20分。3.为正常参考,原因也多种多样。类比到这里,我们也能够理解,一个程序也无外乎这三种情况,代码跑完,结果正确;代码跑完,结果不正确;代码未跑完,程序发生异常。就好比我们写一个排序算法要将一组数据进行排序要么代码跑完,排序成功;要么程序跑完,排序失败;要么程序都压根没跑完。

我们用0表示sucess结果正确,非0表示结果失败。(非0标识不同的原因也不同)

因此retun X(X叫做进程退出码)进程退出码表征了进程退出的信息,这个进程退出信息将来要将父进程读取的。因此这个退出信息码非常的重要。因此我们这里回答了return是给父进程返回

我们写一段程序验证一下

#include <stdio.h>

int main()
{
  return 123;
}

当我们运行这个代码的时候,该进程的父进程是bash,因此这个程序的退出码我们可以使用bash下的命令echo来查看退出码

echo $?

$?

这个$?表示在bash中,最近一次执行完毕时,对应进程的退出码!当我们再查看一次的时候发现是0,大家也不要觉得奇怪。这是因为在shell看来,echo $?这条命令也被当成是一个进程(虽然他不是),因此就会变成了0

进程退出码

在我们刚刚说正常退出 进程退出码是0 0表示success,那么异常退出的时候其他的退出码都表示什么含义呢?

比如这里看一个ls 跟上一串随机字符,我们查看退出码就为2(非0)

因此,一般而言,失败的零值该如何设置呢?以及默认表达的含义?这里我们大家也不需要刻意记忆每个进程退出码对应的含义,因为我们可以自定义来设置,或者用的时候查一查就行。那么我们现在看看系统的代码是什么含义,我们可以使用strerror函数进行查看(下图为man帮助手册查看的strerror的作用及其用法)

#include <stdio.h>
#include <string.h>
int main()
{
  int i = 0;
  for(;i<100;++i)
  {
    printf("%d:%s\n",i,strerror(i));
  }
  return 0;
}

我们在这里大概看几个,我们看到0表示success,1表示权限不允许(可执行程序),2表示找不到文件

因此我们可以得出结论:不同的进程退出码可以对应不同错误原因。

进程终止的常见做法

一般我们有两种做法最常见:

  1. 在main函数中return,代表进程结束,非main函数return表示函数调用结束,为什么其他函数不行呢?
  2. 在自己的代码中任意地点中,调用exit(),即使非main函数也可以退出

exit

我们来看看exit的用法

我们写一段简单的程序看看

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void fun()
{
  printf("fun()\n");
  exit(20);
}
int main()
{
  fun();
  return 123;
}

我们执行这段代码,我们通过查看进程退出码可以确定该程序是从exit推出的还是return出去的。通过结果我们可以看到,程序是从exit退出的。

因此如果以后我们想终止一个进程,可以在想终止的地方调用exit()。

_exit

这里我们之所以介绍_exit仅仅是因为他和我们刚刚介绍的exit长得很像,我们在这里也不需要特别记忆_exit的用法。在此处,我们就简单介绍一下_exit如何使用,以及_exit和exit的区别。

我们通过查看_exit发现,_exit是一个系统调用,其实exit调用了_exit。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
void fun()
{
  printf("fun()\n");
  _exit(123);
}
int main()
{
  fun();
  return 20;
}

此时我们发现_exit和exit好像没有什么区别,实际上他俩还是有区别的,我们来看看下面这段代码

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
  printf("hello world");
  sleep(1);
  exit(111);
  return 20;
}

我们首先使用exit来提前终止进程,我们查看结果发现hello world能够被刷新出来

而当我们在调用_exit时,显示器什么也没输出。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
  printf("hello world");
  sleep(1);
  _exit(111);
  return 20;
}

结论:exit终止进程会刷新缓冲区 _exit终止进程不会刷新缓冲区(_exit我们就了解这么多)

关于终止,内核做了什么?

进程 = 内核结构 + 进程代码和数据

当进程终止时,代码和数据一定会被释放掉。对于内核结构(tast_struct && mm_struct),操作系统可能并不会释放该进程的内核数据结构。

2.进程等待

2.1为什么要进程等待

  1. 解决僵尸进程问题 -- 解决内存泄漏的问题
  2. 解决获取子进程的退出状态问题。首先:一个进程理应该获得子进程的退出状态。在今天,我们只讨论父进程必须通过进程等待的方式获取子进程的退出状态。

2.2进程等待的必要性

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

2.3如何等待--进程等待的方法

2.3.1wait方法

我们使用 man 手册查看wait方法

我们刚才说到了进程等待可以解决将进程由僵尸状态变成释放状态,我们写一段代码来验证一下

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

int main()
{
 pid_t id = fork();
 if(id == 0)
 {
   //child
   while(1)
   {
     printf("我是子进程,我正在运行.... Pid:%d\n",getpid());
     sleep(1);
   }
 }
 else
 {
  //parent
  printf("我是父进程,pid:%d,我准备等待子进程啦\n",getpid());
  sleep(40);
  pid_t ret = wait(NULL);
  if(ret<0)
  {
    printf("等待失败\n");
  }
  else
  {
    printf("等待成功: result: %d\n",ret);
  }
  sleep(20);
 }
	return 0;
}

这段代码,我们可以预期看到3个现象:

  1. 子进程由运行变成Z状态
  2. Z状态在等待成功时就没了
  3. result的返回值等于子进程的pid

我们写一段监控监本代码来实时查看这个过程

while :; do ps ajx | head -1 
&& ps ajx | grep wait | grep -v grep;
echo"--------------------------------------------";
sleep 1;done

以上就是wait的基本调用。我们使用wait()的方案可以解决子进程Z状态,让子进程进入X状态。

2.3.2 waitpid

wait的作用是等待任意一个退出的进程,而接下来我们重点介绍一个waitpid(). wait()/waitpid() 是系统调用!!

返回值pid_t:

>0 : 等待子进程成功,返回值就是子进程的id

<0:等待失败

第一个参数pid:

>0:是几,就代表等待拿一个子进程,pid = 1234 ,等待1234进程,因此就是等待指定进程

-1: 等待任意进程

第二个参数status:

  • status是什么:

这个参数,是一个输出型参数,通过调用这个函数,从函数内部拿出来特定的数据。因为status是一个整形指针,因此拿出来的一定是要一个整数。从内部拿出来的数据就是从子进程的进程控制块(tast_struct)中拿出子进程退出的退出码!

  • status的构成

我们关于status只需要关心改整数的低16个比特位。这16个比特位会分为3个部分。次低8位(8-15)存放这子进程的退出码

关于低7位的作用,我们刚刚说到,代码跑完结果正确,代码跑完,结果不正确,那么代码异常呢?因此低7位的作用就是处理异常。一个进程如果异常退出,是因为这个信号收到了特定的信号!!

第三个参数options:

0 : 我们可以先设置为0,0表示阻塞等待。什么意思呢?可以理解为父进程在等待回收子进程,但是子进程就是不返回呢,那么父进程就等待受阻了,这时父进程就阻塞等待了。

2.3.3waitpid()验证

验证status次低8位是子进程退出码:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
 pid_t id = fork();
 if(id == 0)
 {
   int cnt = 5;
   //child
   while(1)
   {
     printf("我是子进程,我正在运行.... Pid:%d\n",getpid());
     sleep(1);
     cnt--;
     if(!cnt)
     {
       break;
     }
   }
   exit(20);
 }
 else
 {
  //parent
  int status = 0;
  printf("我是父进程,pid:%d,我准备等待子进程啦\n",getpid());
  pid_t ret = waitpid(id,&status,0);
  if(ret > 0)
  {
    printf("wait success,ret : %d,我所等待的子进程的退出码:%d\n",ret,(status>>8)&0xFF);
  }
 }
}

我们假设这里把退出码换成31-->exit(31);

此时我们已经成功的获得了子进程的退出码。

但是我们发现使用位操作的成本是比较高的,因此linux给我提供了一些宏来直接可以调用

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

WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

因为我们可以改写一下等待代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
 pid_t id = fork();
 if(id == 0)
 {
   //child
   while(1)
   {
     printf("我是子进程,我正在运行.... Pid:%d\n",getpid());
     sleep(5);
     break;
   }
   exit(0);
 }
 else
 {
  //parent
  int status = 0;
  printf("我是父进程,pid:%d,我准备等待子进程啦\n",getpid());
  pid_t ret = waitpid(id,&status,0);
  if(ret > 0)
  {
    //是否正产退出
    if(WIFEXITED(status))
    {
      printf("子进程是正常退出的,退出码:%d\n",WEXITSTATUS(status));
    }
  }
 }
 return 0;
}

验证status最低7位的作用 -- 处理异常

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
 pid_t id = fork();
 if(id == 0)
 {
   while(1)
   {
     printf("我是子进程,我正在运行.... Pid:%d\n",getpid());
     sleep(1);
   }
   exit(0);
 }
 else
 {
  //parent
  int status = 0;
  printf("我是父进程,pid:%d,我准备等待子进程啦\n",getpid());
  pid_t ret = waitpid(id,&status,0);
  if(ret > 0)
  {
    printf("wait success,ret : %d,我所等待的子进程的退出码:%d,退出信号是:%d\n",
        ret,(status>>8)&0xFF,status&0x7F);
  }
 }
}

这段代码子进程会一直死循环运行,在子进程运行期间,父进程在阻塞等待子进程。

我们可以使用kill -l 查看其他的进程信号

我们刚刚验证了 9号信号 我们再验证一个3号信号看看

通过这两个验证我们确实已经发现了status的低7位是存储的进程异常退出的信号。

其他信号的验证

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
 pid_t id = fork();
 if(id == 0)
 {
   while(1)
   {
     printf("我是子进程,我正在运行.... Pid:%d\n",getpid());
     sleep(5);
     //除零异常
     int a  = 10/0;
   }
   exit(0);
 }
 else
 {
  //parent
  int status = 0;
  printf("我是父进程,pid:%d,我准备等待子进程啦\n",getpid());
  pid_t ret = waitpid(id,&status,0);
  if(ret > 0)
  {
    printf("wait success,ret : %d,我所等待的子进程的退出码:%d,退出信号是:%d\n",
        ret,(status>>8)&0xFF,status&0x7F);
  }
 }
}

我们查看8号信号发现是浮点数异常

那父进程拿到了子进程的退出码和退出信号,父进程先看谁呢?

进程退出码对应的前两种进程退出:1.代码跑完,结果正确;2.代码跑完,结果不正确

而进程一旦出现异常,只需要关心退出信号,退出码没有任何意义

2.3.4 阻塞等待和非阻塞等待

阻塞等待:

当我们调用某些函数的时候,因为条件不就绪,需要我们阻塞等待其实本质就是当前进程自己变成阻塞状态,等条件就绪的时候再被唤醒。因此所谓的阻塞就是进程阻塞。

进程的非阻塞等待:

WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

将options设置为WNOHANG就非阻塞等待,如果是非阻塞等,子进程没有退出,返回值为0,等待失败,返回-1,等待成功,返回子进程pid

那么我们将代码改成非阻塞等待

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
 pid_t id = fork();
 if(id == 0)
 {
   while(1)
   {
     printf("我是子进程,我正在运行.... Pid:%d\n",getpid());
     sleep(3);
   }
   exit(0);
 }
 else
 {
  //parent
  int status = 0;
  while(1)
  {
    pid_t ret = waitpid(-1,&status,WNOHANG);
    if(ret>0)
    {
      printf("等待成功,%d,exit sig:%d,exit code:%d\n",
          ret,status&0x7F,(status>>8)&0xFF);

    }
    else if(ret == 0)
    {
      //等待成功了 但是子进程没有退出
      printf("子进程好了吗,还没,那么父进程做其他事情.....\n");
      sleep(1);
    }
    else{
      //出错了 暂时不处理
    }
  }
 }
}

以上就是非阻塞轮询检测。

使用kill -9 pid 杀掉子进程,此时父进程等待成功,获取子进程pid以及退出信号

3.进程程序替换

3.1进程程序替换是什么?(概念,原理)

子进程执行的是父进程的代码片段,如果我们想让创建出来的子进程,执行全新的程序呢?想让父子进程彻底分开!

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

程序替换的原理:将磁盘中的程序加载入内存结构,重新建立页表映射,谁执行程序替换,就重新建立谁的映射(子进程),效果:让我们的父进程和子进程彻底分离,并让子进程执行一个全新的程序!!!

3.2为什么要让子进程执行一个新的程序呢?

我们一般在服务器设计(linux编程) 的时候,往往需要子进程干两件种类事情:

  1. 让子进程执行父进程的代码片段(服务器代码)
  2. 让子进程执行磁盘中一个全新的程序(shell,想让客户端执行对应的程序,通过我们的进程,执行别人写的进程代码等等),C/C++,python,shell,java......

3.3 怎么做(编码,如何进行程序替换)

3.3.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. 先找到这个程序在那里?
  2. 程序可能携带选项进行执行(也可以不携带),明确告诉OS,我想要怎么执行这个程序

1.execl

其中参数列表中:"..." 叫做可变参数,说白了就是可以按照用户的意愿传入参数的大小个数,如果还不理解,大家肯定都用过C语言中的printf函数吧,printf有没有规定你只能打印几个参数呢?没有的,这是根据用户自己来定义的!这就是可变参数

execl 第一个参数 path: 就是告诉OS,这个程序在哪里。

execl 第二个参数:就是告诉OS,我想怎么执行。

我们使用C语言来看看,我们发现调用了ls -l -a 的命令

#include <stdio.h>
#include <unistd.h>
int main()
{
  printf("我是一个进程,我的pid是:%d\n",getpid());
  // ls -a -l
  execl("/usr/bin/ls","ls","-l","-a",NULL);
  printf("我执行完毕了,我的pid是:%d\n",getpid());
  return 0;
}

我们再调用一个top命令看看

#include <stdio.h>
#include <unistd.h>
int main()
{
  printf("我是一个进程,我的pid是:%d\n",getpid());
  // ls -a -l
  //execl("/usr/bin/ls","ls","-l","-a",NULL);
  //top
  execl("/usr/bin/top","top",NULL);
  printf("我执行完毕了,我的pid是:%d\n",getpid());
  return 0;
}

这个就是程序替换!但是在程序替换的过程中,发生了什么问题呢?我们发现最后一行代码没有打印!

答案是因为一旦execl替换成功,是将当前进程的代码和数据全部替换了!!后面的printf是代码,是代码已经被替换了,该代码就不存在了!

所以这个程序替换函数用不用判断返回值? 为什么?

int ret = execl(....);

execl是一个程序替换,一旦替换成功了,还会执行返回语句吗?不会了,因为程序替换成功之后没机会了,那么这里的程序替换的返回值有意义吗?答案依然是有意义的,因为替换成功才不会执行,那么失败的时候,必然会继续向后执行,可以通过返回值得到什么原因导致的替换失败!

那我们依然用代码来看看替换失败的时候

#include <stdio.h>
#include <unistd.h>
int main()
{
  printf("我是一个进程,我的pid是:%d\n",getpid());
  // ls -a -l
  int ret = execl("/usr/bin/lsssss","ls","-l","-a",NULL);
  printf("我执行完毕了,我的pid是:%d,ret = %d\n",getpid(),ret);
  return 0;
}

3.3.2 引入进程创建

我们刚刚写的程序并没有创建子进程,都是自己替换自己,而我们有时候就想让子进程做这个事情,那么我们把进程创建进入进来,我们修改一下代码

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h> 
int main()
{
  printf("我是父进程,我的pid是:%d\n",getpid());
  pid_t id = fork();
  if(id == 0)
  {
    //child
    //我们让子进程执行全新的程序,以前是执行父进程的代码片段
	printf("我是子进程,我的pid是%d\n",getpid());
    execl("/usr/bin/ls","ls","-a","-l",NULL);
    exit(1);//只要执行了exit 意味着execl函数失败
  }
  //这里就是父进程
  int status = 0;
  int ret = waitpid(id,&status,0);
  if(ret == id)
  {
    printf("父进程等待成功\n");
  }



  // ls -a -l
  //execl("/usr/bin/ls","ls","-l","-a",NULL);
  //top
  //int ret = execl("/usr/bin/lsssss","ls","-l","-a",NULL);
  //printf("我执行完毕了,我的pid是:%d,ret = %d\n",getpid(),ret);
  return 0;
}

通过这个例子我们得到新的结论:子进程执行程序替换,不会影响父进程的程序,因为进程具有独立性!因此子进程无论怎么替换都不会影响父进程!

那么请问如何做到的?子进程和父进程如何在代码和数据上做分离的呢?当时我们说数据层面上发生写实拷贝!那么么代码不是共享的吗,父进程不会发生变化吗?实际上,当程序替换的时候,我们可以理解为,代码和数据都发生了写实拷贝完成父子的分离!

3.3.3大量的测试各种不同的接口

我们刚刚测试了一下execl接口,还有其他的接口,我们挑几个来测试一下。

execv

execv第一个参数和execl一致,而第二个参数是一个指针数组,这个指针数组指向的是什么?

我们也很好理解,刚刚execl是将一个一个char* 传入函数内,而execv是把这些char*整成一个数组,最后把这个指针数组传入execv即可

我们通过代码测试一下:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h> 
int main()
{
  printf("我是父进程,我的pid是:%d\n",getpid());
  pid_t id = fork();
  if(id == 0)
  {
    //child
    //我们让子进程执行全新的程序,以前是执行父进程的代码片段
    printf("我是子进程,我的pid是%d\n",getpid());
    char* const argv_[] = {
      (char*) "ls",
      (char*) "-l",
      (char*) "-a",
      NULL
    };
    execv("/usr/bin/ls",argv_);

    //execl("/usr/bin/ls","ls","-a","-l",NULL);
    exit(1);//只要执行了exit 意味着execl函数失败
  }
  //这里就是父进程
  int status = 0;
  int ret = waitpid(id,&status,0);
  if(ret == id)
  {
    printf("父进程等待成功\n");
  }
  return 0;
}

因此我们发现execl和execv这两个接口其实并没有很大的区别,只是传入参数的方式不同而已!

execlp

第一个参数:你想执行什么程序 -- 找到它 我们执行指令的时候,默认的搜索路径是path,这里的p就是表示的path,因此带p的可以不带路径,只说出你要执行哪一个程序即可

第二个参数:如何执行它。

我们通过代码来验证一下:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h> 
int main()
{
  printf("我是父进程,我的pid是:%d\n",getpid());
  pid_t id = fork();
  if(id == 0)
  {
    //child
    //我们让子进程执行全新的程序,以前是执行父进程的代码片段
    printf("我是子进程,我的pid是%d\n",getpid());
    execlp("ls","ls","-a","-l",NULL);
    //这里出现的两个ls,可以省略吗,含义一样吗?
    //不能省略 含义不一样
    exit(1);//只要执行了exit 意味着execl函数失败
  }
  //这里就是父进程
  int status = 0;
  int ret = waitpid(id,&status,0);
  if(ret == id)
  {
    printf("父进程等待成功\n");
  }
  return 0;
}

execvp

有了上面几个的铺垫,我们再看这个接口就特别的好理解了,第一个参数在Path找,第二个接口传入一个指针数组。我们也来验证一下:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h> 
int main()
{
  printf("我是父进程,我的pid是:%d\n",getpid());
  pid_t id = fork();
  if(id == 0)
  {
    //child
    //我们让子进程执行全新的程序,以前是执行父进程的代码片段
    printf("我是子进程,我的pid是%d\n",getpid());
    char* const argv_[] = {
      (char*) "ls",
      (char*) "-l",
      (char*) "-a",
      NULL
    };
    execvp("ls",argv_);
    exit(1);//只要执行了exit 意味着execl函数失败
  }
  //这里就是父进程
  int status = 0;
  int ret = waitpid(id,&status,0);
  if(ret == id)
  {
    printf("父进程等待成功\n");
  }

  return 0;
}

execle

这里的前两个接口都非常熟悉了,这里最后一个接口叫做环境变量。那么为什么要有这个接口呢?

说到环境变量之前我们先来看一下这个问题,我们刚刚提到过,进程替换可以让我们执行其他语言写的程序,那么我们怎么来执行呢?(我们使用execl 函数来调用)

我们现在的目标是想用我们写的myproc.c把mycmd.cpp调用起来,那么怎么来用呢?

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h> 
int main()
{
  printf("我是父进程,我的pid是:%d\n",getpid());
  pid_t id = fork();
  if(id == 0)
  {
    //child
    //我们让子进程执行全新的程序,以前是执行父进程的代码片段
    printf("我是子进程,我的pid是%d\n",getpid());
    execl("/home/Lxy/code/linux-code/practice/10-28/mycmd",/*使用绝对路径的方式*/
        "mycmd",NULL);
    exit(1);//只要执行了exit 意味着execl函数失败
  }
  //这里就是父进程
  int status = 0;
  int ret = waitpid(id,&status,0);
  if(ret == id)
  {
    printf("父进程等待成功\n");
  }
  return 0;
}
#include <iostream>
using namespace std;
int main()
{
	cout<<"hello world"<<endl;
	cout<<"hello world"<<endl;
	cout<<"hello world"<<endl;
	cout<<"hello world"<<endl;
	cout<<"hello world"<<endl;
	cout<<"hello world"<<endl;
	cout<<"hello world"<<endl;
	cout<<"hello world"<<endl;
	return 0;
}

我们发现,我们成功的用我们程序调用了cpp文件,我们刚刚使用的是绝对路径,我们也可以使用相对路径,只需要将路径修改为相对路径即可

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h> 
int main()
{
  printf("我是父进程,我的pid是:%d\n",getpid());
  pid_t id = fork();
  if(id == 0)
  {
    //child
    //我们让子进程执行全新的程序,以前是执行父进程的代码片段
    printf("我是子进程,我的pid是%d\n",getpid());
    execl("./mycmd","mycmd",NULL);//使用相对路径的方式
    exit(1);//只要执行了exit 意味着execl函数失败
  }
  //这里就是父进程
  int status = 0;
  int ret = waitpid(id,&status,0);
  if(ret == id)
  {
    printf("父进程等待成功\n");
  }
  return 0;
}

我们再运行发现,仍然是可以成功的。当然也可以用我们的程序来调用python语言,shell脚本语言等等,在这里我就不写代码了,将我自己的实验截图放在此处。

测试这么多,我们知道了任何程序都可以用系统级接口调用其他语言的

谈完这个话题我们再来谈谈环境变量,execle这个函数多了一个e,这个e就是环境变量,如果你想给这个函数传入环境变量,我们就可以传入环境变量。

首先我们先来传入一个系统存在的环境变量,我们使用myproc.c程序调用这个cpp文件

#include <iostream>
#include <stdlib.h>
using namespace std;
int main()
{
  cout<<"PATH:"<<getenv("PATH")<<endl;
  cout<<"hello world"<<endl;
  cout<<"hello world"<<endl;
  cout<<"hello world"<<endl;
  cout<<"hello world"<<endl;
  cout<<"hello world"<<endl;
  cout<<"hello world"<<endl;
  cout<<"hello world"<<endl;
  cout<<"hello world"<<endl;
  return 0;
}

发现没有任何问题,那么如果我们想 传入一个自己手动写的环境变量呢?我们就可以使用execle函数了

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h> 
int main()
{
  printf("我是父进程,我的pid是:%d\n",getpid());
  pid_t id = fork();
  if(id == 0)
  {
    //child
    //我们让子进程执行全新的程序,以前是执行父进程的代码片段
    printf("我是子进程,我的pid是%d\n",getpid());  
    char *const env[] = {
        (char*)"MYPATH=youcanseeme!",NULL };
    //e:添加环境变量给目标进程是覆盖式的
    execle("./mycmd","mycmd",NULL,env); 
    exit(1);//只要执行了exit 意味着execl函数失败
  }
  //这里就是父进程
  int status = 0;
  int ret = waitpid(id,&status,0);
  if(ret == id)
  {
    printf("父进程等待成功\n");
  }
  return 0;
}
#include <iostream>
#include <stdlib.h>
using namespace std;
int main()
{
  //cout<<"PATH:"<<getenv("PATH")<<endl;
  cout<<"---------------------------------\n";
  cout<<"MYPATH:"<<getenv("MYPATH")<<endl;
  cout<<"---------------------------------\n"; 
  cout<<"hello world"<<endl;
  cout<<"hello world"<<endl;
  cout<<"hello world"<<endl;
  cout<<"hello world"<<endl;
  cout<<"hello world"<<endl;
  cout<<"hello world"<<endl;
  cout<<"hello world"<<endl;
  cout<<"hello world"<<endl;
  return 0;
}

模拟实现shell​​​​​​​

讲完这些接口我们发现还剩下的其他接口参数大致相同,我们掌握上面几个接口之后,我们就可以使用其他的接口!至此,进程控制结束,我们可以简易实现一个shell,我把实现的思路和代码整理成一篇新的博客,大家可以点击此处查看[ Linux ] 手动实现一个简易版的shell

(本篇完)

评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白又菜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值