【linux】进程控制

文章目录

  • 一、进程创建
    • 1、三个小问题
    • 2、写时拷贝
    • 3、fork函数常规使用方法
    • 4、fork函数调用失败原因
  • 二、进程终止(进程退出)
    • 1、退出码
    • 2、echo $?指令
    • 3、进程退出场景
    • 4、进程如何退出
    • 5、exit和_exit的关系
    • 6、进程终止总结
  • 三、进程等待
    • 1、进程等待的必要
    • 2、wait(回收子进程资源)
    • 3、waitpid(获取子进程退出信息)
    • 4、status(获取子进程)
    • 5、知识点
    • 6、阻塞与非阻塞
    • 7、轮询
    • 8、进程等待总结
  • 四、进程程序替换
    • 1、什么是进程程序替换
    • 2、进程程序替换原理
    • 3、进程程序替换操作
      • (1)exec函数
      • (2)exec函数命名理解
      • (3)函数的使用
        • execl
        • execlp
        • execv
        • execvp
        • execle
      • 小结
  • 五、实现一个简单的shell
    • 1、初步实现
    • 2、当前路径
    • 3、内建(内置)命令
      • type命令
    • 4、最终版本

一、进程创建

我们前面了解到了fork函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程

那么在复习fork函数之前先来解决几个问题

1、三个小问题

问题1:如何理解fork函数有两个返回值?
问题2:如何理解fork函数返回之后,给父进程返回子进程的pid,给子进程返回0?
问题3:如何理解同一个id,怎么能够保存两个不同的值,同时执行if和else if语句呢?

这里我们就一一来解答一下,先来看问题2:

我们知道现实生活中,一个孩子只有一个父亲,但是一个父亲可以有多个儿子。父亲为了区分每一个儿子就会给他们取不同的名字,方便父亲找到对应的孩子。那么进程也是同理,父进程可以有多个子进程,但是子进程只能有一个父进程,所以fork函数给父进程返回子进程pid就是为了方便找到对应的子进程,然后对该子进程进行相关操作

就先来来解决问题1:

【linux】进程控制_第1张图片
当我们的代码走到fork函数的时候,就开始创建子进程了,进行上面图片中的一系列操作,最终创建好了子进程。但是,我们知道,在return的时候,我们程序的核心代码已经全部完成了,也就是说,return的时候,子进程已经被创建出来了。那么在return的时候,就已经有了两个进程,一个父进程,一个子进程;这个时候两个进程各自调用return语句,所以有了两个返回值。

知识点:

返回的本质就是:写入
那么,由于父进程和子进程的先后执行顺序由调度器决定的,所以我们也不知道谁先返回。但是,谁先返回,谁就先
写入id;又因为上节我们学到的知识,因为进程具有独立性,所以先返回的进程会发生写时拷贝

最后我们来解决问题3:

我们前面学了进程地址空间,那么这里就不难理解了。我们同一个id,地址一样,但是内容却不一样,因为进程的独立性,发生了写时拷贝,所以id的地址相同,内容不同。然后,fork函数调用完之后,父进程的代码是被父子进程共享的,相当于父进程执行一遍,子进程执行一遍,那么执行两遍代码,得出两个结果,然后执行对应的if和else if语句也就不奇怪了

# include 
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1

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

1、分配新的内存块和内核数据结构给子进程
2、将父进程部分数据结构内容拷贝至子进程
3、添加子进程到系统进程列表当中
4、fork返回,开始调度器调度

【linux】进程控制_第2张图片
当一个进程调用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;
} 

运行结果:
【linux】进程控制_第3张图片
这里看到了三行输出,一行before,两行after。进程43676先打印before消息,然后它有打印after。另一个after消息有43677打印的。注意到进程43677没有打印before,为什么呢?如下图所示:

【linux】进程控制_第4张图片
所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定

2、写时拷贝

我们在学习一下写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
【linux】进程控制_第5张图片

简单来说:哪一个进程先要改变物理内存中数据,该进程就发生写时拷贝,会在物理内存重新找一块空间拷贝原来物理内存的数据,然后重新与进程地址空间构成映射关系

3、fork函数常规使用方法

1、一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
2、一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

4、fork函数调用失败原因

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


二、进程终止(进程退出)

1、退出码

提问:我们前面写main函数为什么总是要在最后写一个return 0呢?

在linux中,这个0是进程退出的时候,对应的退出码。这个退出码能够标定进程执行结果是否正确

我们先来看看一段代码:

  1 #include   
  2 #include   
  3 int addsum(int a,int b)  
  4 {  
  5     int ret=0;  
  6     for(int i=a;i

【linux】进程控制_第6张图片
我们这个程序的本意是:判断结果是不是等于5050,是退出码为0,否则退出码为1
但是,可以看到./a.out的时候,没有显示退出码。不过指令echo $?可以显示退出码
就下来就介绍这个指令

2、echo $?指令

echo $? 
永远记录最近一个进程在命令行中执行完时对应的退出码(也就是:main -> return ?)

所以,上面结果echo $?之后,能够打印出1
【linux】进程控制_第7张图片

小结

如何设定main函数返回值呢??如果不关心进程退出码,return 0就行
如果未来我们是要关心进程退出码的时候,要返回特定的数据表明特定的错误
一般而言,我们返回0表示成功,非0表示失败

退出码的意义:>0:success, !0:标识失败, !0具体是几,标识不同的错误
但是数字对人不友好,对计算机友好,所以,我们一般而言,退出码,都必须有对应的退出码的文字描述,1. 可以自定义 2. 可以使用系统的映射关系(不太频繁)

那么,我们接下来就来看看系统中有哪一些帮我们设置好的退出码,以及每个退出码对应的信息:

#include 
int main()                                   
 {                                            
      for(int i=0;i<200;++i)                   
      {                                        
          printf("%d:%s\n",i,strerror(i));                                                                                                     
      }   
 }

【linux】进程控制_第8张图片
【linux】进程控制_第9张图片

可以看到有100多种错误码,其中0表示正确,其他的每个错误码都对应不同的错误信息

3、进程退出场景

进程退出一共无非三种情况:

情况1:代码执行完了,结果正确——return 0
情况2:代码执行完了,结果不正确——return !0(退出码这个时候起效果)
情况3:代码没有执行完,程序异常,退出码无意义

4、进程如何退出

1、main函数,return返回
2、任意地方调用void exit(int status)函数,status就是退出码

举例:

  1 #include 
  2 #include 
  3 #include 
  4 #include 
  5 int addsum(int a,int b)
  6 {
  7     int ret=0;
  8     for(int i=a;i

在这里插入图片描述
3、任意地方调用_exit(int status)函数,status就是退出码

  1 #include 
  2 #include 
  3 #include 
  4 #include 
  5 int addsum(int a,int b)
  6 {
  7     int ret=0;
  8     for(int i=a;i

5、exit和_exit的关系

exit是库函数,而_exit是系统调用;所以,两个是上下层关系。其中,exit在_exit的上层

接下来举例说明:

printf("hello");//前面学到过,不带\n的话,没有进行行刷新,所以hello在缓存区里面不被刷新,等到sleep两秒之后才被刷新打印出来
sleep(2);
exit(1);

【linux】进程控制_第10张图片
这里的hello的确过了两秒被打印出来了,因为exit函数把缓存区的数据刷出来了

printf("hello");
sleep(2);
_exit(1);//这里换成_exit函数

【linux】进程控制_第11张图片
这里不会打印hello,也就是说_exit函数不会刷新缓存区

6、进程终止总结

exit函数结束进程,会主动刷新缓存区(前面学过,exit就是对_exit系统函数进行了封装)
_exit函数结束进程,不会主动刷新缓存区
缓存区的位置在:用户层,不在内核层(OS)。因为exit函数是库函数,在用户层;而_exit函数是系统调用函数,在内核层。如果缓存区在内核层,那么_exit函数也会刷新缓存区的,由此可见,缓存区是在用户层的,是一个用户级的缓存区

【linux】进程控制_第12张图片
【linux】进程控制_第13张图片


三、进程等待

我们前面学到了进程的一种状态——Z:僵尸状态,这种状态的进程是要等待父进程回收僵尸进程的退出信息来处理的,不然的话会造成内存泄漏等问题
那么可以通过进程等待的方式来解决僵尸状态进程的问题!

1、进程等待的必要

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

所以进程要等待的原因就是:1、回收子进程资源;2、获取子进程退出信息

2、wait(回收子进程资源)

这里主要讲如何回收子进程资源,至于获取子进程退出信息,下面再讲

接下来我们直接来见见进程等待,首先要介绍等待接口wait

wait函数等待成功就返回子进程的pid,否则就返回-1

#include
#include
pid_t wait(int*status);
返回值:
 成功返回被等待进程pid,失败返回-1。
参数:
 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
   9      pid_t id = fork(); 
   10     if(id == 0)    //子进程          
   11     {                        
   13         int cnt = 5;
   14         while(cnt)                
   15         {                                                           
   16             printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);
   17             sleep(1);
   20         }                                                
   26         exit(12); //子进程退出
   27     }                       
   28     // 父进程          
   29     //sleep(15);                          
   30     pid_t ret = wait(NULL);
   31     if(id > 0)
		    {
		        printf("wait success: %d\n",ret);
		    }
   38     sleep(5); //父进程休眠5秒,观察子进程被回收的过程           

【linux】进程控制_第14张图片
【linux】进程控制_第15张图片

3、waitpid(获取子进程退出信息)

接下来我们继续深入学习一下,怎么获取子进程退出信息。这就就要用到waitpid接口了

pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
 当正常返回的时候waitpid返回收集到的子进程的进程ID;
 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
 
参数:
 pid:
 Pid==-1,等待任一个子进程。与wait等效。
 Pid>0,等待其进程ID与pid相等的子进程。
 pid == 0 等待其组I D等于调用进程的组I D的任一子进程。换句话说是与调用者进程同在一个组的进程。
 pid < -1 等待其组I D等于p i d的绝对值的任一子进程。
 
 status:
 WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
 WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
 
 options:
 0:阻塞式等待
 WNOHANG: 非阻塞等待
 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

4、status(获取子进程)

1、wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
2、如果传递NULL,表示不关心子进程的退出状态信息。
3、否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
4、status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
【linux】进程控制_第16张图片
这个core dump标志我们后面再讲。
终止信号:表示代码是否执行完毕
退出状态:表示代码结果是否正确
注意:如果终止信号不为0,也就是代码没有正常跑完,中途挂了,那么退出状态就是0,因为此时退出状态的位图全都是0,不会被填写

接下来我们还是见见猪跑:

int main()
{
    pid_t id = fork();
    if(id == 0) //子进程
    {
        int cnt = 5;
        while(cnt)
        {
            printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);
            sleep(1);
        }
        // 运行完
        // 1. 代码完,结果对
        // 2. 代码完,结果不对
        // 异常
        // 3. 代码没跑完,出异常了
        exit(12); //进程退出
    }
    // 父进程
    //sleep(15);
    //pid_t ret = wait(NULL);
    int status = 0; // 不是被整体使用的,有自己的位图结构
    pid_t ret = waitpid(id, &status, 0);
    if(id > 0)
    {
        printf("wait success: %d, sig number: %d, child exit code: %d\n", ret, (status & 0x7F), (status>>8)&0xFF);
    }
    sleep(5);

知识点:

1、如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
2、如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
3、如果不存在该子进程,则立即出错返回。
【linux】进程控制_第17张图片

但是一直使用位操作的方法来获取退出状态和终止信号太复杂了,所以linux中有特定的宏来完成:

    int ret = waitpid(id, &status, 0);
    if(ret > 0)
    {
      if(WIFEXITED(status))  // 是否正常退出
      {
          printf("exit code: %d\n", WEXITSTATUS(status)); // 判断子进程运行结果是否ok
      }
      else
      {
           //TODO
          printf("child exit not normal!\n");
       }
    //printf("wait success, exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);
    //不再用位操作这么麻烦了
    }

5、知识点

子进程在退出的时候,会释放掉子进程的代码和数据,但是子进程的退出信息(退出码和退出信号)会保存在子进程的PCB(进程控制块)里面(进程退出,pcb不会消失,要等待父进程来处理)。
此时,子进程就变成了Z状态也就是僵尸状态,如果父进程调用wait/waitpid函数接口,通过id找到对应的僵尸进程,把该进程的退出信息(退出状态,终止信号)写入status里面,通过&status的操作拿到僵尸进程的退出状态和终止信号,返回僵尸进程的pid。最后,父进程在将子进程的PCB(进程控制块)释放掉,这样才算将一个子进程退出了

【linux】进程控制_第18张图片

6、阻塞与非阻塞

waitpid的第三个参数options为0时表示阻塞等待,为WNOHANG表示非阻塞等待

阻塞等待:当父进程执行的waitpid函数时,如果子进程没有退出,父进程就只能阻塞在waitpid函数,一直等到子进程退出。然后,父进程通过waitpid函数读取子进程退出信息之后,才能继续执行后面的代码

非阻塞等待:当父进程执行的waitpid函数时,如果子进程没有退出,父进程会直接读取子进程的状态并且返回,然后父进程继续执行其他代码完成其他任务,不用等待子进程退出

7、轮询

轮询的前提条件是:非阻塞等待
在非阻塞等待的前提下,父进程多次/循环的非阻塞等待子进程就形成了轮询。父进程在轮询期间并不是一直进行非阻塞等待子进程,还会做其他工作

    1 #include                                                                                                                                                                                                                          
    2 #include 
    3 #include 
    4 #include 
    5 #include 
    6 #include 
    7 #include 
    8 int main()
    9 {
   10     pid_t id = fork();
   11     assert(id != -1);
   12     if(id == 0)
   13     {
   14         //child
   15         int cnt = 10;
   16         while(cnt)
   17         {
   18             printf("child running, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
   19             sleep(1);
   20         }
   21 
   22         exit(10);
   23     }
   24 
   25     // parent
   26     int status = 0;
   27     while(1)
   28     {
   29         pid_t ret = waitpid(id, &status, WNOHANG); //WNOHANG: 非阻塞-> 子进程没有退出, 父进程检测时候,立即返回
   30         if(ret == 0)
   31         {
   32             // waitpid调用成功 && 子进程没退出
   33             //子进程没有退出,我的waitpid没有等待失败,仅仅是监测到了子进程没退出.
   34             printf("wait done, but child is running...., parent running other things\n");
   35         }
   36         else if(ret > 0)
   37         {
   38             // 1.waitpid调用成功 && 子进程退出了
   39             printf("wait success, exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);
   40             break;
   41         }
   42         else
   43         {
   44             // waitpid调用失败
   45             printf("waitpid call failed\n");
   46             break;
   47         }
   48         sleep(1);
   49     }
   50 }

【linux】进程控制_第19张图片
这里父进程就一直在轮询,多次进行非阻塞等待子进程

8、进程等待总结

1、为了回收子进程资源和获取子进程退出信息,我们需要进行进程等待;
2、进程等待的本质是父进程从子进程的 task_struct(进程控制块PCB)中读取退出信息,然后保存到 status 中;
3、我们可以通过 wait 和 waitpid 系统调用接口对子进程进行进程等待;
4、status参数是一个输出型参数,父进程通过 wait/waitpid 函数将子进程的退出信息写入到 status 中;
5、status以位图方式存储,包括退出状态和退出信号,若退出信号不为0,则退出状态无效;
6、我们可以使用系统提供的宏 WIFEXITED 和WEXITSTATUS 来分别获取 status 中的退出状态和退出信号;
7、进程等待的方式分为阻塞式等待与非阻塞式等待,阻塞式等待用0来标识,非阻塞式等待用宏 WNOHANG 来标识;
8、非阻塞式等待不会等待子进程退出,所以我们需要以轮询的方式来不断获取子进程的退出信息。而父进程也会在轮询期间做其他的工作


四、进程程序替换

上面我们说了,创建一个子进程无非有两个目的:

1、想让子进程执行父进程代码的一部分(截止到进程等待为止,我们的fork创建子进程都是在完成这个目的)

2、为了让子进程执行一个全新的代码(进程程序替换就是:让子进程想办法加载磁盘上指定的程序,执行该指定程序的代码和数据

1、什么是进程程序替换

刚才已经提到了:

让子进程想办法加载磁盘上指定的程序,执行该指定程序的代码和数据
并且,原来进程的task_struct(进程控制块)和mm_struct(进程地址空间)以及进程的pid都不会改变,只是有可能改变页表(按需索取,也有可能页表不会改变)。所以,>进程程序替换并不会创建一个新进程,而是让原来的进程执行我们指定程序的代码和数据

2、进程程序替换原理

原理就是:

用新程序(我们指定的程序)的代码和数据来替换原来进程物理地址中的代码和数据,除了可能改变原来进程页表的映射关系之外,原来进程的task_struct和mm_struct等等内核数据都不会改变

【linux】进程控制_第20张图片

3、进程程序替换操作

(1)exec函数

linux中提供了一系列的exec函数来实现进程程序替换操作,其中有一个系统调用,六个库函数。这六个库函数都是对那个系统调用接口进行的封装等处理:
【linux】进程控制_第21张图片【linux】进程控制_第22张图片

可以看到,man之后,man 2也就是系统调用接口就一个,而man 3库函数有6个。不过我们主要学习的还是库函数的那六个,毕竟就行了封装等处理,使用更加简单、方便

这六个库函数统称为exec函数

#include `
 
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
因为exec函数一旦调用成功,就表示我们已经把新程序(指定程序)的代码和数据给替换到物理地址了。原来进程的代码和数据就不会被执行了,也就不存在有返回值。如果exec函数调用失败,原程序的代码和数据才能够继续往下执行,这个时候exec函数返回值才会被使用,也就是返回-1

(2)exec函数命名理解

l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量 
函数名 参数格式 是否需要带路径 是否使用当前环境变量
execl 列表
execlp 列表
execle 列表 否,需要自己组装环境变量
execv 数组
execvp 数组
execve 数组 否,需要自己组装环境变量

(3)函数的使用

我们上面的六个函数接口其实都是具有一定相对应关系的
我们执行程序分为两个步骤:
1、找到执行程序
2、指定程序执行的方式
而exec函数中,带上p和不带p表示查找程序;l和v来表示指定程序执行方法;e表示指定环境变量

execl

int execl(const char *path, const char *arg, …);
第一个参数:找到要执行的程序地址
剩下参数:按照什么方式执行 (你在linux命令行怎么执行,就怎么传参)
最后的三个点(…):可变参数列表——给函数传递不同个数个参数(参数可以传多个)

    1 #include 
    2 #include 
    3 #include 
    4 #include 
    5 #include 
    6 #include 
    7 #include 
    8 int main()
    9 {
   10     printf("process is running...\n");
   11     execl("/usr/bin/ls","ls","--color=auto",NULL);//这里要注意,   所有execl函数最后都要以NULL结尾,证明我们把参数传完了                                                                                                                      
   12     printf("process  running done...\n");//这里的printf在execl函数之后的,execl执行完之后,代码和数据已经全部覆盖了,这个时候开始执行新的代码了,所以printf就无法执行了
   //只有execl函数调用失败的时候,才打印该printf的内容
   13 
   14 }

【linux】进程控制_第23张图片
还有很多用法: execl(“/usr/bin/ls”,“ls”,“–color=auto”,NULL),我们可以把ls改成mv,pwd…只要路径正确,后面的参数全部都是字符串类型的,内容就是我们的选项,只要最后带上NULL就行

而我们一般进程替换都是子进程执行的,因为进程具有独立性,所以一般都fork创建一个子进程之后,子进程来进行进程替换:

    1 #include 
    2 #include 
    3 #include 
    4 #include 
    5 #include 
    6 #include 
    7 #include 
    8 
    9 int main()
   10 {
   11     pid_t id = fork();
   12     if(id < 0)
   13     {
   14         perror("fork");//打印失败原因
   15         return 1;
   16     } 
   17     else if (id == 0)//子进程
   18     {  
   19         printf("pid: %d, child process is runnning...\n", getpid());
   20         int ret = execl("/usr/bin/ls", "ls", "-l", "-a", "--color=auto", NULL);  //进程程序替换
   21         if(ret < 0)//替换失败执行下面语句
   22         {
   23             printf("process exec failed\n");
   24             exit(1);
   25         }
   26         printf("pid: %d, child process is done...\n", getpid());
   27         return 0;
   28     }
   29     //父进程
   30     int status = 0;
   31     pid_t ret = waitpid(id, &status, 0);  //进程等待
   32     if(ret < 0)
   33     {
   34         perror("waitpid");                                                                                                                                                                                                                 
   35         return 1;                         
   36     }                                   
   37     else                                
   38     {                                   
   39         printf("wait pid: %d, exit signal: %d, exit code: %d\n", ret, (status & 0x7f), (status >> 8 & 0xFF));
   40     }                                 
   41     return 0;                                                                                                        
   42 }

【linux】进程控制_第24张图片
可以看到我们生成的可执行程序与ls -a -l结果是一样的

【linux】进程控制_第25张图片

以前我们只是知道数据被写入会发生写时拷贝,现在代码也会发生写时拷贝,在进程程序替换的时候,代码就会发生写时拷贝

execlp

int execlp(const char *file, const char *arg, …);
这里多了一个字符p,接下来介绍一下:
exec函数带了p,就表示函数不需要知道程序路径,只要告诉函数要替换成为什么内容,函数会自动在环境变量PATH里面找可执行程序,然后进行替换
所以,带了p的exec系列函数,参数不需要路径,只需要替换程序。当然,前提条件是:在环境变量PATH中存在替换程序,不存在也是不能够完成函数调用的!!!
…三个点是可变参数列表:给函数传递不同个数个参数(参数可以传多个)

    1 #include 
    2 #include 
    3 #include 
    4 #include 
    5 #include 
    6 #include 
    7 #include 
    8 
    9 int main()
   10 {
   11     pid_t id = fork();
   12     if(id < 0)
   13     {
   14         perror("fork");
   15         return 1;
   16     } 
   17     else if (id == 0)
   18     {  
   19         printf("pid: %d, child process is runnning...\n", getpid());
   20         execlp("ls", "ls", "-l", "-a", "--color=auto", NULL);  //这里第一个参数就不需要带上路径了,直接给替换程序就行,因为该替换程序在环境变量PATH里面存在                                                                                
   21         exit(-1);                                                                 
   22     }                                                                             
   23     int status = 0;                                                               
   24     pid_t ret = waitpid(id, &status, 0);                                          
   25     if(ret >0)                                                                                                                              
   26     {                                                                                                                                       
   27         printf("wait pid: %d, exit signal: %d, exit code: %d\n", ret, (status & 0x7f), (status >> 8 & 0xFF));                               
   28     }                                                                                                                                       
   29     return 0;                                                                                                                               
   30 }  

【linux】进程控制_第26张图片
可以看到效果还是一样的

execv

int execv(const char *path, char *const argv[]);
这里带了一个v,参数变成了一个指针数组argv
也就是说:带v,可以将所有的执行参数放入数组中,统一传递,不需要使用可变参数的方案

  char* const argv[]={
            (char *) "ls",
            (char*) "-a",
            "-l",//这里是警告,我们强转一下就行,不转也没有事
            "--color=auto",
            NULL
};
execv("/usr/bin/ls",argv);     

【linux】进程控制_第27张图片

execvp

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

这就不用多说了,直接拿捏了

char* const argv[]={
           (char *) "ls",
           (char*) "-a",
           "-l",//这里是警告,我们强转一下就行,不转也没有事
           "--color=auto",
            NULL
};
execv("ls",argv);   

接下来我们讲点其他的:
Makefile:

 1 .PHONY:all
  2 all: mybin points                                                                                                                                                                                                                            
  3 
  4 mybin:mybin.c
  5     gcc -o $@ $^ -std=c99
  6 points:points.c
  7     gcc -o $@ $^ -std=c99
  8 .PHONY:clean
  9 clean:
 10     rm -f points mybin
~

这里我们直接生成两个可执行程序:
【linux】进程控制_第28张图片
接下来我们要让points来调用mybin

 execl("./mybin","mybin",NULL);  

【linux】进程控制_第29张图片

这里就通过一个程序调用起另一个程序了

execle

int execle(const char *path, const char *arg, …,char *const envp[]);
e就是指自定义环境变量
也就是说我们使用带e的函数的时候,可以将环境变量传进来

mybin.c:

                                                                                                                                                                                                                
  1 #include <stdio.h>                                                                                                                                                                                                                           
  2 #include <stdlib.h>
  3 
  4 int main()
  5 {
  6     // 系统就有
  7     printf("PATH:%s\n", getenv("PATH"));
  8     printf("PWD:%s\n", getenv("PWD"));
  9     // 自定义
 10     printf("MYENV:%s\n", getenv("MYENV"));
 11 
 12     printf("我是另一个C程序\n");
 13     printf("我是另一个C程序\n");
 14     printf("我是另一个C程序\n");
 15     printf("我是另一个C程序\n");
 16     printf("我是另一个C程序\n");
 17     printf("我是另一个C程序\n");
 18     printf("我是另一个C程序\n");
 19     printf("我是另一个C程序\n");
 20     printf("我是另一个C程序\n");
 21 
 22     return 0;
 23 }
~

【linux】进程控制_第30张图片
points.c:

char *const envp_[] = {
                (char*)"MYENV=11112222233334444",
                NULL          
           }; 
           execle("./mybin", "mybin", NULL, envp_); //自定义环境变量     

【linux】进程控制_第31张图片

char *const envp_[] = {
                (char*)"MYENV=11112222233334444",
                NULL          
           }; 
           extern char **environ;//系统环境变量   实际上,默认环境变量你不传,子进程也能获取 

【linux】进程控制_第32张图片

extern char **environ;
          //execle("./mybin", "mybin", NULL, envp_); //自定义环境变量
           putenv((char*)"MYENV=4443332211"); //将指定环境变量导入到系统中 environ指向的环境变量表                                                                                                                                            
           execle("./mybin", "mybin", NULL, environ); 

【linux】进程控制_第33张图片

小结

我们前面知道我们写的程序要被加载到内存中,那么谁加载我们的程序到内存呢?
答案就是exec系列的函数
exec系列函数把我们的程序加载到内存中,又因为程序是先被加载,任何被执行的,其中main函数也是函数,也要被调用、执行、传参、加载的。所以,exec系列函数是比main函数先执行的

程序替换的execve系统调用,其他的都是封装,为了让我们有更多的选择性

exec系列的函数可以替换调用任何后端语言

五、实现一个简单的shell

1、初步实现

学习了上面的知识,我们可以自己写一个迷你版的shell,主要包含下面几点:

1、在界面输出提示符
2、从终端获取命令行输入
3、解析命令行输入信息
4、创建子进程
5、进程程序替换
6、进程等待
7、进程终止

【linux】进程控制_第34张图片

#include 
#include 
#include 
#include 
#include 
#include 

#define NUM 1024    //一个命令的最大长度
#define OPT_NUM 64  //一个命令的最多选项

char lineCommand[NUM];
char* argv[OPT_NUM];

int main() {
    while(1) {

        //输出提示符
        printf("[用户名@主机名 当前路径]$ ");
        fflush(stdout);

        //获取输入
        char* ret = fgets(lineCommand, sizeof(lineCommand)-1, stdin);  //最后留一个位置来存放极端情况下的\0
        if( ret == NULL ) {
            perror("fgets");
            exit(1);
        }
        lineCommand[strlen(lineCommand) - 1] = '\0';  //消除命令行中最后的换行符

        //将输入解析为多个字符串存放到argv中,即字符串切割
        argv[0] = strtok(lineCommand, " ");
        int i = 1;
        while(argv[i++] = strtok(NULL, " "));

        //创建子进程
        pid_t id = fork();
        if(id == -1) {
            perror("fork");
            exit(1);
        } else if (id == 0) {  //子进程
            //进程程序替换
            int ret = execvp(argv[0], argv);
            if(ret == -1) {  
                printf("No such file or directory\n");
                exit(1);
            }
        } else {  //父进程
            //进程等待
            pid_t ret = waitpid(id, NULL, 0);
            if(ret == -1){
                perror("wait");
                exit(1);
            }
        } 
    }
    return 1;  //while循环正常情况下不会结束
}

这里shell什么都可以,但是,cd之后,pwd不会改变路径

2、当前路径

在这里插入图片描述

exe表示当前进程执行的是磁盘的哪一个程序
cwd表示当前进程的工作目录——这才是我们说的当前路径

接下来我们就来解释为什么上面我们的shell进行cd之后,pwd没有变化:

myshell 是通过创建子进程的方式去执行命令行中的各种指令的,也就是说,cd 命令是由子进程去执行的,那么自然被改变也是子进程的工作目录,父进程的工作目录不受影响。而当我们使用 PWD 指令来查看当前路径时,cd 指令对应的子进程已经执行完毕退出了。cd和PWD是两条命令。此时 myshell 又会给 PWD 创建一个新的子进程,且这个子进程的工作目录和父进程 myshell 相同,所以 PWD 打印出来的路径不变。

当然,我们也是可以改当前工作目录的,通过chdir系统调用接口来改变一个进程的工作目录
【linux】进程控制_第35张图片
所以,我们的shell迷你版想要cd之后,pwd能够更改路径就需要下面的一段代码:

if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
{                                                                                                       
       if(myargv[1] != NULL) chdir(myargv[1]);
       continue;         
}

3、内建(内置)命令

不需要让我们的子进程来执行,而是让shell自己执行的命令就叫做内建命令

内建命令由shell程序来完成的,它的功能是在bash中实现的,不需要创建子进程来完成,也不用外部程序文件来运行;是通过shell本身来完成内建命令的。
而外部命令则是通过创建子进程,再进行进程程序替换,运行外部程序文件等方法来完成的

type命令

我们可以通过type命令来区分内建命令和外部命令
【linux】进程控制_第36张图片
所以,我们上面的cd命令就是以内建命令的方法来处理的,myshell遇到cd命令是,自己直接来进行cd,改变了进程工作目录,处理完之后continue,并不会创建子进程进行cd。但是,pwd目录我们没有将其处理为内建命令

同时,这也解释了为什么前面的echo $变量可以查看本地变量、echo $?为什么可以获取最近一次的进程退出码了

因为echo就是一个内建命令,由shell直接完成操作,不用创建echo子进程。虽然本地变量只在当前进程有效,但是使用 echo 查看本地变量时,shell 并不会创建子进程,而是直接在当前进程中查找,自然可以找到本地变量;

同理,shell 可以通过进程等待的方式获取上一个子进程的退出状态,然后将其保存在 ? 变量中,当命令行输入 “echo $?” 时,直接输出 ? 变量中的内容,不需要创建子进程,然后将 ? 置为0 (echo 正常退出的退出码)

myshell添加echo的功能代码:

int EXIT_CODE;  //退出码 -- 全局变量

if(argv[0] != NULL && strcmp(argv[0], "echo") == 0)  //处理echo内建命令
{
    if(strcmp(argv[1], "$?") == 0){  //echo $?
        printf("%d\n", EXIT_CODE);
        EXIT_CODE = 0;
    } else {  //echo $变量
        printf("%s\n", argv[1]+1);
    }
    continue;
}

//fork后面的内容
} else {  //父进程
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);   //进程等待
    EXIT_CODE = (status >> 8) & 0xFF;   //获取退出码
    if(ret == -1){
        perror("wait");
        exit(1);
    }
} 

4、最终版本

    1 #include <stdio.h>
    2 #include <string.h>
    3 #include <stdlib.h>
    4 #include <unistd.h>
    5 #include <sys/types.h>
    6 #include <sys/wait.h>
    7 #include <assert.h>
    8 
    9 #define NUM 1024
   10 #define OPT_NUM 64
   11 
   12 char lineCommand[NUM];
   13 char *myargv[OPT_NUM]; //指针数组
   14 int  lastCode = 0;
   15 int  lastSig = 0;
   16 
   17 int main()
   18 {
   19     while(1)
   20     {
   21         // 输出提示符
   22         printf("用户名@主机名 当前路径# ");
   23         fflush(stdout);
   24 
   25         // 获取用户输入, 输入的时候,输入\n
   26         char *s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);
   27         assert(s != NULL);
   28         (void)s;
   29         // 清除最后一个\n , abcd\n
   30         lineCommand[strlen(lineCommand)-1] = 0; // ?
   31         //printf("test : %s\n", lineCommand);
   32         
   33         // "ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->n
   34         // 字符串切割
   35         myargv[0] = strtok(lineCommand, " ");
   36         int i = 1;
   37         if(myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
   38         {
   39             myargv[i++] = (char*)"--color=auto";
   40         }
   41 
   42         // 如果没有子串了,strtok->NULL, myargv[end] = NULL
   43         while(myargv[i++] = strtok(NULL, " "));
   44 
   45         // 如果是cd命令,不需要创建子进程,让shell自己执行对应的命令,本质就是执行系统接口
   46         // 像这种不需要让我们的子进程来执行,而是让shell自己执行的命令 --- 内建/内置命令
   47         if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
   48         {                                                                                                       
   49             if(myargv[1] != NULL) chdir(myargv[1]);
   50             continue;
   51         }
   52         if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0)
   53         {
   54             if(strcmp(myargv[1], "$?") == 0)
   55             {
   56                 printf("%d, %d\n", lastCode, lastSig);
   57             }
   58             else
   59             {
   60                 printf("%s\n", myargv[1]);
   61             }
   62             continue;
   63         }
   64         // 测试是否成功, 条件编译
   65 #ifdef DEBUG
   66         for(int i = 0 ; myargv[i]; i++)
   67         {
   68             printf("myargv[%d]: %s\n", i, myargv[i]);
   69         }
   70 #endif
   71         // 内建命令 --> echo
   72 
   73         // 执行命令
   74         pid_t id = fork();
   75         assert(id != -1);
   76 
   77         if(id == 0)
   78         {
   79             execvp(myargv[0], myargv);
   80             exit(1);
   81         }
   82         int status = 0;
   83         pid_t ret = waitpid(id, &status, 0);
   84         assert(ret > 0);
   85         (void)ret;
   86         lastCode = ((status>>8) & 0xFF);
   87         lastSig = (status & 0x7F);
   88     }
   89 }


你可能感兴趣的:(linux,运维,服务器)