【linux】:进程控制(下)

简介: 【linux】:进程控制(下)

三.进程程序替换



替换原理:


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


那么我想问问一下,创建子进程的目的是什么呢?就是为了让子进程帮我做特定的任务。而这里分为两种情况:


1.让子进程执行父进程的一部分代码

2.如果子进程向指向一个全新的程序代码,就称为进程的程序替换。


下面我们先看一下什么是程序替换:


首先我们要知道程序替换的接口execl,我们用man手册打开可以看到:


a949a72ddbef4923a9f167d0de637df9.png


第一个函数后面参数...这里是可变参数列表,第一个参数是你要执行谁,这里是个路径,如果我们将参数传完了必须以NULL结尾。接下来我们先使用一下,然后详细的讲解这个函数。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
  printf("begin.........\n");
  printf("begin.........\n");
  printf("begin.........\n");
  printf("begin.........\n");
  execl("/bin/ls","ls","-a","-l",NULL);
  printf("end..........\n");
  printf("end..........\n");
  printf("end..........\n");
  printf("end..........\n");
  return 0;
}

上图是我们所用的演示代码,我们可以看到execl这个函数的第一个参数是个路径,表示你要执行谁,第二个参数是指令,表示你要执行什么命令,而后面就是指令的选项了,记住参数传完后一定要以NULL结尾。

f5907ba9f703423d80ce48cb127f8cf2.png


运行后我们发现原先打印的begin end中只剩下begin了,这是什么原因呢?如下图:


d08371d376694daca8e05c46f4a855b2.png


也就是说程序替换会直接将原先可执行程序的代码和数据替换为新的代码和数据。下面我们来看一下程序替换的基本原理:


89b0576de166462f94f6e8da13af1956.png


那么通过上图我想问一下,进程的程序替换,有没有创建新的进程呢?答案是没有,这里的替换还是原先的进程。那么当创建进程的时候,先有进程数据结构,还是先加载代码和数据呢?这个问题的答案其实我们之前回答过,一定是先有进程数据结构,因为有数据结构才能管理相应的代码和数据。


上面的演示代码我们是用一个进程进行演示的,下面我们用父子进程来演示一下:


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
//int main()
//{
//  printf("begin.........\n");
//  printf("begin.........\n");
//  printf("begin.........\n");
//  printf("begin.........\n");
//  execl("/bin/ls","ls","-a","-l",NULL);
//  printf("end..........\n");
//  printf("end..........\n");
//  printf("end..........\n");
//  printf("end..........\n");
//  return 0;
//}
int main()
{
  pid_t id = fork();
  if (id==0)
  {
    //子进程
    printf("我是子进程:%d\n",getpid());
    execl("/bin/ls","ls","-a","-l",NULL);
  }
  sleep(5);
  //父进程
  printf("我是父进程:%d\n",getpid());
  waitpid(id,NULL,0);
  return 0;
}

我们让子进程去进行程序替换,父进程等待子进程,下面是运行结果:

4720ddfae2c441e5887bcade83623c00.png


父进程依旧还是运行了,说明了程序替换只会影响调用的那个进程。这又验证了进程具有独立性这句话。


那么程序替换会不会失败呢?如果失败了该怎么办?


8491516785ee4af2809ca3a0787e2b0a.png


我们将execl里的参数修改为错误的,然后我们运行一下:


4f1282b0b6cb4753a4ee8ebb5bcc0ac0.png


可以看到如果程序替换失败了会继续执行之前的代码和数据,并且如果失败execl的返回值为-1,如下图:


75c8f959513c4abe90b961f3f612c6c3.png


我们也可以这样理解,execl只要有返回值就失败了,不用判断返回值。


当然我们照样可以拿到子进程的退出码,如下图:


2e025cb4033441bd867273947ae8281e.png79821141b4cd47b0a01a6377bdaadcfc.png


可以看到我们确实拿到了ls指令失败的2号退出码。


下面我们详细的讲解一下execl接口:


4f9ca61630fb430daa651dbf3bd0a88f.png


第一个参数path是要执行指令的路径,后面的参数就是如何加载执行这个指令,比如ls命令有ls -l  ls-l -n等等,也就是说我们在命令行怎么执行这个命令就将这个指令一个个的传递给execl即可,只是当我们将所有的指令传完后后面必须加上NULL结尾。


接下来我们讲解一下execv这个函数接口:


这个接口的第一个参数还是path路径,第二个参数argv[]是什么呢?最后一个V其实是vector的意思是一个数组,意思就是以数组的方式去传指令,下面我们演示一下:


7920c2fc12fb4b2587ca93015a5c43e9.pngbdf93838e9224b52a9d26f2b68470bf0.png


通过结果我们可以看到execv的结果与execl是一样的。这个就相当于c++的函数重载


下面再讲一下execlp接口,此接口与execl的不同是第一个参数:


8d64fcdee791413d95be9f44aab4a8ac.png


我们可以看到第一个参数是file,这是什么意思呢?其实这个接口就是不需要你在传路径了,你直接传你的指令然后系统会帮助你找到路径不需要你在传了,用这个接口你的环境变量必须在PATH中或者在PATH中设置过,下面我们演示一下:


86714b087b564e1898202468c81207eb.png


可以看到我们直接传第一个参数ls,现在我们运行一下:


299275477bfe4cdca800b6f9b41665a4.png


可以看到运行是没问题的。


接下来我们再讲解一下execle接口:

967a615930cb40b7917d5b23ef39517a.png

这个接口的第三个参数envp[]是什么意思呢?这个参数是自定义环境变量,有什么用呢?比如上面我们的父子进程,子进程的环境变量是继承父进程的,而这个参数的意思是如果我们不想要继承父进程的环境变量我们可以自己传环境变量。下面我们演示一下:


3e256b61b1724304acb9fdb0cae28f90.png


我们先创建一个.cc文件,然后随便写一段代码:

ee55cd0367174db0bd1456d624a9b6d9.png

然后我们再进入刚刚3-16的文件,将环境变量的参数写为刚刚的.cc生成的可执行文件:

50d3b11ff52e4d08b0c3847e38994572.png


然后我们运行起来,如果能成功的运行other 的代码的话就说明execle这个接口可以将程序替换为自己写的程序:

34efb85bcf8840ffb2d6cad15c9769ba.png


我们可以看到pid相同运行的程序确实不同并且运行了我们刚刚写的程序。接下来我们再演示一下传环境变量:


5aa8d1c441c543798135e79c9d51c7d5.png


首先将other中的代码修改为取得一个环境变量MYENV,如果返回值为NULL说明没取到,否则就是取到了

8f25bde6d9df456dbe1efa0043907c91.png


当我们运行起来发现为NULL,这是因为并没有MYENV这个环境变量,然后我们在刚刚3-16的文件中设置一下这个环境变量:

dc673e6a3cd3411db633de5e3e755cac.png

可以看到我们将MYENV设置为youcanseeme,然后我们运行起来:


cdaa1ab12e29493d89f135f89bfb3663.png


这个时候我们看到确实把环境变量传进去了,下面我们看看最后一个接口:

1257d5ec05a14a7fbb29a40efd5ec7d8.png

为什么这个接口不在刚刚那个里面而被单独列出来呢?因为这个是真正的系统调用,而上面我们讲的都是这个接口的封装,这是为了满足各种场景。


下面我们编写一个极简版本的shell(bash),目的是为了较为深刻的理解shell的运行原理。

8f0d1394a57743f5b2d2c11b2f7b20ff.png


我们先创建一个shell.sh的文件,然后写一段简单的代码:


84ec9017a04843c3948d040fb22fd982.png12b9b7d4764b47c8858d340c4a204192.png


然后我们就可以运行了。


下面我们写一个自动切割字符串的shell:


我们先写一个.c文件,然后代码如下:


#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
#define MAX 1024
#define SEP " "
int split(char* commandstr,char*argv[])
{
  assert(conmandstr);
  assert(argv);
  argv[0] = strtok(commandstr,SEP);
  int i = 1;
  while (1)
  {
    argv[i] = strtok(NULL,SEP);
    if (argv[i]==NULL) break;
    i++;
  }
  return 0;
}
void debugprint(char* argv[])
{
  for (int i = 0;argv[i];i++)
  {
    printf("%d:%s\n",i,argv[i]);
  }
}
int main()
{
  char commandstr[MAX] = {0};
  while (1)
  {
    char commandstr[MAX] = {0};
    char* argv[ARGC] = {NULL};
    printf("lisi@mymachine currpath]# ");
    fflush(stdout);
    char* s = fgets(commandstr,sizeof(commandstr),stdin);
    assert(s);
    (void)s;  //保证在release方式发布后,因为去掉assert了,所以s就没有被使用,而带来的编译告警,什么都没做但是充当依次使用
    commandstr[strlen(commandstr)-1] = '\0';
    int n = split(commandstr,argv);
    if (n!=0) continue;
    debugprint(argv);
    pid_t id = fork();
    assert(id>=0);
    (void)id;
    if (id==0)
    {
      //子进程
      exit(0);
    }
    int status = 0;
    waitpid(id,&status,0);
  }
  return 0;}


4514e3e5ee064cc8b962537ed4a7d5b8.png


通过结果我们可以看到确实完成了字符串的切割,通过这两个极简版本的shell,我们也确实可以深刻的理解了shell 的运行原理。


总结



对于Linux操作系统的学习我们其实已经进入了一个入门的阶段,从进程开始的学习是相对比较困难与枯燥的,但是我们只要闯过了这一关我们就见到真正的阳光。下一篇是Linux文件描述符相关的学习,希望得到大家的支持!


目录
相关文章
|
22天前
|
资源调度 Linux 调度
Linux c/c++之进程基础
这篇文章主要介绍了Linux下C/C++进程的基本概念、组成、模式、运行和状态,以及如何使用系统调用创建和管理进程。
28 0
|
3月前
|
网络协议 Linux
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
525 2
|
3月前
|
Linux Python
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
63 2
|
5天前
|
运维 Linux
Linux查找占用的端口,并杀死进程的简单方法
通过上述步骤和命令,您能够迅速识别并根据实际情况管理Linux系统中占用特定端口的进程。为了获得更全面的服务器管理技巧和解决方案,提供了丰富的资源和专业服务,是您提升运维技能的理想选择。
8 1
|
16天前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
【10月更文挑战第9天】本文将深入浅出地介绍Linux系统中的进程管理机制,包括进程的概念、状态、调度以及如何在Linux环境下进行进程控制。我们将通过直观的语言和生动的比喻,让读者轻松掌握这一核心概念。文章不仅适合初学者构建基础,也能帮助有经验的用户加深对进程管理的理解。
15 1
|
21天前
|
消息中间件 Linux API
Linux c/c++之IPC进程间通信
这篇文章详细介绍了Linux下C/C++进程间通信(IPC)的三种主要技术:共享内存、消息队列和信号量,包括它们的编程模型、API函数原型、优势与缺点,并通过示例代码展示了它们的创建、使用和管理方法。
20 0
Linux c/c++之IPC进程间通信
|
22天前
|
Linux C++
Linux c/c++进程间通信(1)
这篇文章介绍了Linux下C/C++进程间通信的几种方式,包括普通文件、文件映射虚拟内存、管道通信(FIFO),并提供了示例代码和标准输入输出设备的应用。
17 0
Linux c/c++进程间通信(1)
|
22天前
|
Linux C++
Linux c/c++之进程的创建
这篇文章介绍了在Linux环境下使用C/C++创建进程的三种方式:system函数、fork函数以及exec族函数,并展示了它们的代码示例和运行结果。
23 0
Linux c/c++之进程的创建
|
2月前
|
Linux Shell
6-9|linux查询现在运行的进程
6-9|linux查询现在运行的进程
|
22天前
|
Linux C++
Linux c/c++进程之僵尸进程和守护进程
这篇文章介绍了Linux系统中僵尸进程和守护进程的概念、产生原因、解决方法以及如何创建守护进程。
15 0