Linux进程控制

Linux进程控制

1.Linux进程创建

1.1 fork()函数的基本了解

在linux中fork()函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程!

  1. fork()函数的头文件#include<unistd.h>
  2. fork()函数原型pid_t fork(void)
  3. fork()函数返回值父程序返回子程序PID,子程序返回0,出错返回-1

1.2 fork()调用期间,内核的操作

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

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

请添加图片描述

当一个进程调用fork之后,父子进程共享代码,子进程写时拷贝父进程数据,每个进程都将可以开始它们自己的执行,看如下程序:

//给了if分流父子进程的fork现象
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
    printf("Now PID:%d\n",getpid());
    pid_t id=fork();
    if(id<0)
    {
        perror("fork error!\n");
        return 1;
    }
    else if(id==0)
    {
        //child
        printf("child pid:%d  return:%d\n",getpid(),id);
        sleep(2);
    }
    else
    {
        //father
        printf("father pid:%d  return:%d\n",getpid(),id);
        sleep(2);
    }
    return 0;
}

请添加图片描述

再来看一个代码现象:

//未给if分流父子进程的fork现象
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
  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,两行after。进程9045先打印before消息,然后它又打印after。另一个after 消息有9046打印的。注意到进程9046没有打印before,为什么呢?如下图所示:

请添加图片描述

上图解析:

  • fork之前,我们是先执行before,所以第一行我们打印除了befor
  • fork执行时,子进程写时拷贝一份父进程数据,fork返回两个返回值分别给父子进程
  • fork之后,父子进程代码共享,但fork返回值有两个,于是执行后续的代码2次,打印两个after

值得注意的是:

  1. fork之后,父子进程谁先执行,完全由调度器决定
  2. fork之后,并不是重新创建一个新的子进程,而是使用老的父进程,也就是我们说的父子进程代码共享,所以进程还是一个,我们说的父子进程只是方便描述
  3. 发生写时拷贝,父子进程的虚拟地址相同,物理地址的映射不同,所以最后父子进程获得的getpid()和pid值是不一样的

1.3 fork()函数的返回值

思考一些问题:

  1. 为何要给子进程返回0,给父进程返回子进程的pid?
  2. 如何理解fork有两个返回值的问题?
  1. 第一个问题:

    • 首先父子进程的 1 : n 的关系,所以在父子进程的立场中,父进程不需要标识,子进程需要标识。其次子进程是要执行任务的,父进程需要区分子进程,所以给父进程返回子进程的pid,因为父进程可以通过这个pid来区分是哪个子进程。给子进程返回0,本质上是因为子进程不需要访问父进程pid,因为子进程也不需要知道父进程pid,子进程不需要管理父进程,任务是给子进程的,它只需要知道自己调用成功了就可以
  2. 第二个问题:

请添加图片描述


1.4 写时拷贝技术

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

请添加图片描述

对于写时拷贝的理解:

  1. 对这里的"共享"怎么理解?
    • 答:父子进程对应的页表指向的是同一块物理内存。当任何一方写入的时候,以便使用写时拷贝的方式生成一份副本
  2. 为何要写时拷贝?
    • 答:进程具有独立性!
  3. 为何不在创建的时候就分开?
    • 答:子进程不一定会使用父进程的所有数据,写入,本质是需要的时候!也就是按需分配,这种方式还做到了一点:延时分配,因为当被创建的时候,不一定被立马调度,如果不立马被调度,那就不需要先给它分配空间。因为要是先给它分配空间了,那也就是在它被调度之前的时间段中,系统可用的内存是变少的,所以延时分配永远可以保证系统可用资源是最大化的!所以延时分配的本质是:可以高效使用任何内存空间!
  4. 为何代码不会写时拷贝?
    • 答:90%的情况不会(但是不代表不能),因为我们学语言到现在,我们要改的永远是数据,我们没有在让程序运行的时候,改程序运行的逻辑

1.5 fork()函数的常规用法

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

1.6 fork()函数调用失败的原因

  1. 系统中有太多的进程:内存空间上的不支持
  2. 实际用户的进程数超过了限制:操作系统上的不支持

2.Linux进程终止

2.1 进程退出的三种情况

  1. 代码正常运行完毕,结果正确
  2. 代码正常运行完毕,结果不正确
  3. 代码运行异常

请添加图片描述


2.2 进程退出的常见方式

  1. 正常终止(可以通过 echo $? 查看最近一次进程退出码):
    1. 从main函数中return返回
    2. 调用exit()函数返回
    3. 调用_exit()函数返回
  2. 异常退出:ctrl+c,信号中止

Linux下错误码打印方法:

#include<stdio.h>
#include<string.h>
int main()
{
    int i=0;//这里这样写是因为不支持C99,需要在编译的时候加个-std=c99
    for(i=0;i<150;i++)
    {
        printf("错误码序号:%d,错误信息:%s\n",i,streror(i));
    }
    return 0;
}

请添加图片描述

注:Linux下只有133个错误码,每个错误码都有对应的错误信息!


2.3 exit()和_exit()的区别

两者的区别在于,exit会在进程结束做一些收尾工作(比如刷新缓冲区),而_exit不会,看下面的对比:

请添加图片描述

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
void show()
{
    printf("我是show()函数!");
    exit(10);
}
int main()
{
    show();
    printf("我是main()函数!");
    return 0;
}

请添加图片描述

我们把exit()函数换成_exit()函数来看看效果:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
void show()
{
    printf("我是show()函数!");
    _exit(10);
}
int main()
{
    show();
    printf("我是main()函数!");
    return 0;
}

请添加图片描述

从上面来个代码效果和剖析函数原型我们可以得出结论:

  1. exit最后也调用了_exit,但exit还做了其他工作:
    • 执行用户通过 atexit或on_exit定义的清理函数
    • 关闭所有打开的流,所有的缓存数据均被写入
    • 调用_exit
  2. exit和return的区别:
    • exit是终止整个进程,任何地方调用都会终止整个进程
    • return是终止函数,等同于exit(n)

2.4 关于退出码的一些思考

由退出码想到了一系列问题:

  1. 进程异常退出,退出码还有意义吗?
    • 没有意义!说简单点,到异常的地方就已经被终止了,根本没有执行 return
  2. 进程终止了,操作系统在做什么?
    • 释放曾经申请的数据结构,释放曾经申请的内存,从各种队列等数据中移除
  3. 为何要有进程等待?
    • 回收子进程资源
    • 获取子进程退出信息

3.Linux进程等待

3.1 进程为什么要等待

  1. 回收僵尸进程,解决内存泄漏(僵尸进程是杀不死了,kill -9不能干掉僵尸进程)
  2. 获取子进程的运行结束状态(交给子进程的工作做的怎么样了)
  3. 父进程晚于子进程退出,规范化的进行资源回收
  • 等待的本质也是管理的一种方式,OS的核心就在于管理二字
  • 简而言之,进程等待的原因:获取子进程的信息和防止内存泄漏

3.2 等待函数wait与waitpid的理解

我们用man 2 wait来看一下wait和waitpid函数的官方文档:
请添加图片描述

补充:在普通状态下用man -2 wait查看,在vim下用! man 2 wait可以快速查看

wait与waitpid理解:

  1. 头文件:#include<sys/types.h>和#include<sys/wait.h>
  2. wait返回值:等待成功返回子进程pid,等待失败返回-1
  3. waitpid返回值:等待成功返回子进程的pid,如果指定了WNOHANG选项,且子进程正在运行(没有已退出的子进程可收集)则返回0,等待失败返回-1

请添加图片描述


3.3 wait()的使用

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
 成功返回被等待进程pid,失败返回-1。
参数:
 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

wait的使用案例:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
 
int main()          
{                    
   pid_t id = fork();
   if(id == 0)
   {
     int count = 0;
     while(count < 5)
     {         
       printf("I am child, pid: %d, ppid: %d\n",getpid(),getppid());
       sleep(1);     
       count++;
     }                                                              
     exit(0);   
   }           
   else
   {
       printf("I am father, pid: %d, ppid: %d\n",getpid(),getppid());
       pid_t ret = wait(NULL);
       if(ret >= 0)                                                                                     
       {                                                             
         printf("wait child success!, %d\n",ret);
       }           
       printf("Father running...");
       sleep(5);
   }
   return 0;
}

请添加图片描述

  1. 结论:由此可见,在父进程等待的时候,子进程在运行,重要的是,在子进程运行结束后没有看到Z状态进程,这是因为父进程在等待子进程结束,然后回收子进程
  2. 一点小问题:
    • 在子进程运行期间,父进程wait的时候,父进程在做什么?
      • 就是在 “等” 什么也没干,就是在等子进程退出,这种等子进程退出的过程叫做阻塞等待
      • 因为父子谁先运行不确定,但是wait之后,大部分情况都是子进程先退出,父进程读取子进程退出信息,父进程才退出。建议大家以后一定要让父进程等待子进程退出,如果不等的话,一定会导致僵尸进程的问题

补充:上面ps命令下面的########的显示方法

ps ajx | grep test(代码文件名称) | grep -v grep; sleep 1; echo "###########"; done
//这个代码里的while、do、done是shell编程里使用的,自行了解就行了

3.4 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相等的子进程
    
 status:
 	WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
 	WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
        
 options:
 	WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID
  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞
  • 如果不存在该子进程,则立即出错返回
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
 #include<sys/wait.h>
#include<sys/types.h>

int main()
{                    
   pid_t id = fork();
   if(id == 0)
   {
      int count = 0;
      while(count < 5)
      {         
       	 printf("I am child, pid: %d, ppid: %d\n",getpid(),getppid());
       	 sleep(1);     
         count++;
      }                                                              
      exit(0);   
   }           
   else
   {
       printf("I am father, pid: %d, ppid: %d\n",getpid(),getppid());
      //pid_t ret = wait(NULL);
       int status = 0;                                                                          
       pid_t ret = waitpid(id, &status, 0);
       if(ret >= 0)
       {              
         printf("wait child success!, %d\n",ret);
       } 
       printf("Father running...");
       sleep(5);
    }
   return 0;
}

请添加图片描述

结论:

  1. 由此可见,waitpid和wait没有什么区别
  2. 进程等待成功是否意味着子进程运行成功?
    • 绝对不是,进程等待成功只意味着子进程退出了

3.5 进程子状态的获取:int* status

我们接下来看看status表示的是什么:

请添加图片描述

由此可见,status表示的不是退出码,这个数字很奇怪,那它到底表示的是什么呢?

参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,我们就可以设定这个参数为NULL

关于status参数的理解:

  1. wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充

  2. 如果传递NULL,表示不关心子进程的退出状态信息

  3. 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程

  4. status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)

请添加图片描述

关于0xff与0x7f的使用理解:

  1. 0xff:通常用来取得低八位
    • f二进制是:1111,即0xff为:0000 0000 1111 1111
    • 这里的status>>8,取status的后8位,后八位全是0,而&操作,二进制位同真为真
    • 如果(status>>8)&0xff结果为0,则表示后8位全是0,代码运行结果正确
    • 如果(status>>8)&0xff结果不为0,则表示后8位不全是0,代码运行结果不正确
  2. 0x7f:通常用来取得低七位
    • 7二进制是:0111,即0x7f为:0000 0000 0111 1111
    • 异常终止高八位没用,所以这里不需要将status>>8
    • status&0x7f,就是将status的第七位与111 1111进行&操作,从而得到终止信号

请添加图片描述


3.6 status的验证

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

int main()
{
   pid_t id = fork();
   if(id == 0)
   {
      int count = 0;
      while(count < 5)
      {
        printf("I am child, pid: %d, ppid: %d\n",getpid(),getppid());
        sleep(1);
        count++;
      }
      exit(77);
   }
   else
   {
      printf("I am father, pid: %d, ppid: %d\n",getpid(),getppid());
      //pid_t ret = wait(NULL);
      int status = 0;                                                                          
      pid_t ret = waitpid(id, &status, 0);
      if(ret >= 0)
      {
         printf("wait child success!, %d\n",ret);
         printf("status: %d\n",status);
         printf("child exit code: %d\n",(status>>8)&0xFF);
      }
      printf("Father running...\n");
      sleep(2);
    }
    return 0;
}

请添加图片描述

  1. 我们通过第9行代码获取到了子进程退出的退出码exit(77)
  2. 进程异常的时候,本质是进程运行的时候出现了某种错误,导致进程收到信号!

那么我们怎么知道我们收到信号了呢?我们接着看:

请添加图片描述

由此可见,我们没有收到任何信号,因为信号里没有0号信号!

请添加图片描述

由上图,我在进程运行的时候用2号信号把子进程给kill了,子进程立马终止然后传递过去了2号信号。且退出码为0(我刚才说过,如果进程出现异常,退出码没有任何意义!)

上面都是提到的单进程,接下来我就写一个多进程执行的,前面没有说waitpid的第一个参数,其实第一个参数在单进程的时候就标识的那个参数,多参数的时候就可以指定一个参数,也就是说一个waitpid只能等一个子进程:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
 
int main()
{
   pid_t ids[3];
   for(int i = 0; i < 3; i++)
   {  
      pid_t id = fork();
      if(id == 0)       
      {  
        int count = 3;                                               
        while(count > 0)
        {         
           printf("child do something!: %d, %d\n",getpid(),getppid());
           sleep(1);
           count--;
        }     
        exit(i);  
      }                                                                                    
      //father
      ids[i] = id;
  }               

  int count = 0;     
  while(count < 3)
  {
     int status = 0;
     pid_t ret = waitpid(ids[count], &status, 0);
     if(ret >= 0)
     {
        printf("wait child success!, %d\n",ret);
        printf("status: %d\n",status);
        printf("child exit code: %d\n",(status>>8)&0xFF);
        printf("child get signal: %d\n",status&0x7F);
     }
     count++;
  }
  return 0;
}

请添加图片描述

  • 其实我们很少用到创建多进程的场景
  • 在最开始我们说waitpid的时候给了两个宏,也就是说,我们可以不适用位操作,直接使用宏即可,如下代码:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
 
int main()
{
    pid_t ids[3];
    for(int i = 0; i < 3; i++)
    {
      pid_t id = fork();
      if(id == 0)
      {
         int count = 3;
         while(count > 0)
        {
          printf("child do something!: %d, %d\n",getpid(),getppid());
          sleep(1);
          count--;
        }
        exit(i);
      }                                                                                        
      //father
      ids[i] = id;
    }
 
   int count = 0;
   while(count < 3)
   {                                                                                          
      int status = 0;
      pid_t ret = waitpid(ids[count], &status, 0);
      if(ret >= 0)
      {
         printf("wait child success!, %d\n",ret);
         if(WIFEXITED(status))//正常退出
         {
           printf("child exit code: %d\n",WEXITSTATUS(status));
         }
         else
         {
           //不正常退出
           printf("child not exit normal!\n");
         }
        // printf("status: %d\n",status);
        // printf("child exit code: %d\n",(status>>8)&0xFF);
        // printf("child get signal: %d\n",status&0x7F);
      }
      count++;
  }
  return 0;
}

请添加图片描述


3.7 进程阻塞等待与非阻塞等待的区别

  • 阻塞等待:父进程一直等子进程,什么也不干
  • 非阻塞等待:就是父进程在等待的同时也在做自己的事,在子进程退出后再去读取子进程的退出信息

请添加图片描述

请添加图片描述

我们首先来剖析一下waitpid返回值的文档:

请添加图片描述

这里我解释一下waitpid()的返回值:如果成功的话,返回等待子进程的退出码,如果WNOHANG被指定的、并且指定的子进程是存在的、并且这个子进程的状态没有改变,就返回0,否则的话就返回-1

这里先说一个细节:waitpid的返回值要么大于0要么小于0这两个状态,要是设置成非阻塞就很有可能出现第三个状态,就是调用waitpid调用成功了,但是子进程并没有退出,没有退出的话调用waitpid检车的时候,就相当于,我等它的时候,他没有退出,但是我waitpid调用成功了,因为状态没有变,所以我直接返回了,就相当于我就进行了一次检测。所以如果waitpid的返回值是0的话就证明waitpid调用是成功的,只不过被等的那个子进程没有退出罢了

#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 count = 0;
      while(count < 5)
      {
         printf("I am child, pid: %d, ppid: %d\n",getpid(), getppid());
         sleep(1);
         count++;
      }
    exit(1);                                                                                 
   }

  int status = 0;
  pid_t ret = waitpid(id, &status, WNOHANG);
  if(ret > 0)
  {
     printf("wait success!\n");
     printf("exit code: %d\n",WEXITSTATUS(status));
  }
   printf("ret: %d\n", ret);
   return 0;
}

请添加图片描述

  1. 由此可见,此时waitpid的返回值立马就是0,但是子进程还在,而父进程立马就退出了,此时子进程的ppid立马改为了1,那么子进程也就变成了孤儿进程
  2. 所以我们可以得到一个结论,如果我们以非阻塞的方式进行等待的时候,此时我们就不应该只等待一次,而是让父进程不断的轮询式的等待
#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 count = 0;
      while(count < 5)
      {
        printf("I am child, pid: %d, ppid: %d\n",getpid(), getppid());
        sleep(1);
        count++;
      }
      exit(1);
   }
   while(1)
   {
      int status = 0;                                                                          
      pid_t ret = waitpid(id, &status, WNOHANG);
      if(ret > 0)
      {
        printf("wait success!\n");
        printf("exit code: %d\n",WEXITSTATUS(status));
        break;
      }
     else if(ret == 0)
     {
        printf("father do other things!\n");
        sleep(1);
     }
     else
     {
       printf("waitpid error!\n");
       break;
     }
  }
   return 0;
}

请添加图片描述

由此可见,在子进程在做自己的事情的时候,父进程并不是刻意的去等待,而是父进程也在做自己的事情,它们两个之间并不会相互影响,只不过每隔1秒进行一次检测,当子进程运行结束之后,父进程获取子进程相关的退出信息

这种检测方案叫做非阻塞接口轮询检测方案!


4.Linux进程替换

4.1 进程替换原理

  • 磁盘中保存一程序的代码和数据,程序替换就是,将磁盘中保存的新程序的代码和数据替换进程中的程序和代码。从新程序代码开始执行
  • 注意:程序替换没有创建新进程,所以该进程的pid和数据结构并没有改变

请添加图片描述


4.2 替换函数

在Linux中其实有六种以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
所以exec函数只有出错的返回值而没有成功的返回值
    
//命名理解
//这些函数原型看起来很容易混,但只要掌握了规律就很好记
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量

请添加图片描述

请添加图片描述

补充:execl系列的函数,根本就不需要判断返回值,因为只要是返回了就是失败!所以我们一般在程序替换后加上exit(1),也就是说只要你成功了,那你就被替换掉了,只要失败了就不往后走了,就终止进程

请添加图片描述

接下来我先来用几个,其实剩下的都是非常相似的:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>                                                                           
int main()
{
  printf("I am a process!\n");
  sleep(2);
  execl("/usr/bin/ls","ls", "-a", "-i", "-l", NULL);
  return 0;
}

请添加图片描述

  • 我们可以看到,我们利用execl成功的调用起了ls命令
  • 我们得到一个结论,我的进程可以把别人的程序调用起来

我们学语言的时候肯定都听说过一句话,任何程序要被运行之前,必须要先从磁盘中加载到内存当中(因为冯诺依曼体系是这么决定的,因为磁盘属于外设)那么我们就有了以下一些疑问了?

  1. 那么程序如何被加载的呢?
    • 我们刚才用的execl就可以称之为叫做Linux下的加载器所用的底层技术
  2. 当前进程在进行程序替换的时候,有没有创建新的进程?
    • 没有!也就是说,我们在进行程序替换的时候,没有进行任何的程序创建。有的人可能认为不对啊!这里执行的代码和数据都已经被替换掉了,那怎么能是没创建新进程呢?
    • 其实衡量一个进程是进程,并不是根据这个进程执行什么代码、访问什么数据决定的,衡量一个进程是进程是由它在内核中的相关数据结构决定的,而其中我们在进行程序替换时PCB、虚拟地址空间、页表这三种结构是没有发生质的变化的。我们只是把老的代码用新的磁盘上的文件的代码和数据进行了替换,仅此而已
    • 所以这也就印证:进程不等价于程序,进程要比程序大的多
  3. 进程替换之后如果还有代码会执行么?
    • 不会!因为已经被替换了,进程程序替换,一经替换,绝不返回,后续代码不会执行
  4. 如果程序替换失败呢?
    • 程序替换失败后,程序后续并不会受到影响!也就是说,一旦替换失败,后面的代码正常运行

5.shell的实现原理

5.1 对shell的理解

  • shell叫做命令行解释器,它的作用是将你输入的命令交给bash去执行,而bash本身不会亲自给你去实现命令,而是会创建一个子进程去帮你执行,因为进程和进程之间是由独立性的,你运行的命令如果没有BUG还好,有BUG的话,这个BUG命令如果bash亲自执行,可能会将bash搞挂了,bash挂了,就没有办法给用户提供新的命令行解释服务了,所以这种事一般由子进程去做,子进程挂掉也不影响,首先不影响父进程bash,而且运行后的结果不管对还是不对,父进程都可以拿到结果

shell的简单实现方法:

  1. 简单的shell,它的根本原理其中一定要有fork()这样的调用,这是其一
  2. 其二就是我们创建出来的子进程,我们不是为了让子进程帮我们去执行解释器部分的代码,它的任务只是执行命令,所以也就是创建子进程,让子进程去执行一个全新的程序(程序替换)

请添加图片描述


5.2 shell的实现

用下图的时间轴来表示事件的发生次序,其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束

请添加图片描述

实现流程:

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork)
  4. 替换子进程(execvp)
  5. 父进程等待子进程退出(wait)

实现代码:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<string.h>
#define LEN 1024
#define NUM 32
 
int main()
{
   char cmd[LEN];
   char* myarg[NUM];
   while(1)
   {
      printf("[牟建波@my-centos_mc dir]$ ");
      fgets(cmd, LEN, stdin);
     // 我们创建出来的子进程要执行命令(命令再cmd中)
     // 要执行命令就要将一个个命令拆开才可以调用
     // 所以要解析字符串
     //
     // 将最后一个命令的\n去掉(换成\0就行了) 
     cmd[strlen(cmd) - 1] = '\0';
     myarg[0] = strtok(cmd, " ");
     int i = 1;
     while(myarg[i] = strtok(NULL, " "))
     {
        i++;
     }           

     pid_t id = fork();
     if(id == 0)//child
     {
        execvp(myarg[0], myarg);
        exit(-1);//随便写的
     }
     int status = 0;
     pid_t ret = waitpid(id, &status, 0);
     if(ret > 0)
     {
        printf("exit code: %d\n", WEXITSTATUS(status));
     }
   }
  return 0;
}

请添加图片描述

对于实现shell后的一些结论:

  1. 我们说shell就是一个进程,现在就可以理解了,当我们 ./test 的时候 test 变成进程了,一直在运行,所以shell是在系统启动的时候就由某些任务将其进行启动
  2. 系统启动以及用户登录的时候,某些登陆软件会自动调用bash程序,将其运行起来变成进程
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

「已注销」

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

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

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

打赏作者

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

抵扣说明:

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

余额充值