linux之进程控制(万字长文详解)
  1EQmf8Oo0jTP 2023年11月27日 16 0

进程控制

进程创建——fork函数

用于创建子进程的系统调用接口!

image-20221117144516389

这是一个函数函数执行前只有一个父进程,函数执行后有一个父进程一个子进程

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

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

fork后的子进程和父进程的数据使用写实拷贝的方式各自私有

但是进程的代码是各自私有的!可以使用if else的方式进行分流!

#include<stdio.h>
#include <unistd.h>
int main()
{
    pid_t id = fork();
    if(id < 0 )
    {
          perror("fork");
          return 1;
    }
    else if(id == 0)
    {
          while(1)
          {
               printf("我是子进程 ,pid = %d ,ppid = %d\n",getpid(),getppid());
               sleep(1);
          }
          //child
    }                                                                                                     
    else
    {
          //parent
          while(1)
          {
               printf("我是父进程 ,pid = %d ,ppid = %d\n",getpid(),getppid());
               sleep(3);
          }
    }
    return 0;
}

image-20221117161033202

关于fork的返回值

如何理解fork返回之后,给父进程返回子进程的pid 给子进程返回 0 ?

image-20221222153224575

因为父进程具有唯一性,但是子进程没有!所以为了区分子进程,就必须返回子进程的pid!

如何理解fork函数有两个返回值?

用户空间:我们自己写的代码的位置 内核空间:放操作系统代码的位置!

我们在用户空间调用fork,但是fork是一个系统接口!所以fork函数底层的实现是在内核空间!

fork函数的底层实现是非常复杂的!fork的功能也十分的多,分别有

  1. 创建子进程的PCB
  2. 给PCB赋值
  3. 创建子进程的地址空间
  4. 给地址空间赋值
  5. 创建并设置页表!
  6. 进子程放入进程队列里面!
  7. 等待

image-20221222161032667

在内核空间fork进程里面父子进程其实就已经分流了!——所以在用户空间才会看到有两个返回值!

但是谁先返回谁后返回就是看系统调度了!

如何理解同一个id,怎么回保存不同的两个值让if 和 eles同时执行?

因为fork调用完毕之后,会return值!返回的本质就是对id写入!

因为进程具有独立性!父子进程谁先返回!谁就先发生写实拷贝!

image-20221222161817754

所以出现了同一个id,地址虽然是一样的!但是内容却不一样!

返回后无论是父进程还是子进程返回后往后执行!其实都对if else进程判断!

fork常规用法

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

fork调用失败的原因

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制 可以写一个死循环自己测试!

进程终止

退出码

我们写c/c++的时候,往往会在的最后写一个return 0;

int main()
{
       return 0;
}

为什么要返回 -0?

语言上我们称这个返回0 ,但是在系统上我们称为==进程退出的试试,对应的退出码!==

==退出码的意义是用来标定进程执行的结果是否正确!==

什么意思呢?

我们写代码是为了某件事情!例如像是下面的我们想求一个1——100的求和!

#include <stdio.h>      
int addToTarget(int from,int to)      
{      
       int sum = 0;      
       for(int i =from;i<=to;i++)      
       {      
           sum +=i;      
       }      
       return sum;      
}      
int main()      
{      
       int num = addToTarget(1,100);      
       if(num == 5050)    
           return 0;                                                               
       else     
           return 1;    
                                    
}     

我们该如何去判断程序退出呢?——通过打印出结果?这样不可靠!因为如果是一个更加复杂的计算或者程序,我们是没有能力去判断程序的正确与否的!

所以才要有退出码!!——通过计算机来判断!

但是我们又要如何的去获得进程的返回值呢?

通过环境变量来获得!

环境变量 该变量永远记录==最近一个进程==在命令行中执行完毕时对应的退出码!即main函数内的返回值!

image-20221225114544133

我们就可以知道一个进程究竟是否成功执行了!

我们再尝试的将程序修改错误

image-20221225114859782

我们可以如愿的得到一个1的退出码!

==但是为什么后面的?这个环境变量又变成了0了呢?==

==因为echo也是一个进程!它能够正常的打印就说明程序能够正常的执行!所以退出码就为0,这0是echo的退出码!==

要如何设定main函数的退出码?

  • 如果不关系进程的退出码的时候,return 0就可以!
  • 如果需要关系的时候,我们应该返回特定的数据的来表明特定的错误!

一般我们不关心进程执行成功后的退出码!

但是我们关心的是进程执行错误后的退出码!

所以我们用0表示成功!用非0表示错误!

用不同的非0来表示不同的错误!

==一般而每个不同的退出码都应该都对应的退出描述!==——方便排查的人看懂!

1,可以自定义 2,可以使用系统的映射关系(不太频繁使用除非调用了太多的库函数)

==strerror函数就是c语言提供的将退出码转换成字符串描述的一个函数!==

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

image-20221225142533726

==可以发现每一个错误都有一个错误的原因!==——我们日常在linux下面使用的指令也是c语言的写的一个程序!其退出码和转换出相应的错误表达就是使用的是strerror!

image-20221225142925684

退出的情况

进程退出有这几种情况!

1.代码跑完了,结果正确——return 0;

2.代码跑完了,结果正确——return 非0//退出码就是在这情况起作用!

3.代码没跑完,程序异常了——这时候退出码就没有意义了!

第三种情况例如:除0或者野指针都会发生

进程如何退出

1.main函数返回!

2.任意地方调用exit!

exit

image-20221225143719642

这个函数的作用就使得==进程终止!==——传入的就是退出码!

#include <stdio.h>    
#include <stdlib.h>                                                             
int main()    
{    
       printf("hello world\n");    
       exit(12);  
}

image-20221225144129568

#include <stdio.h>
#include <stdlib.h>
int addToTarget(int from,int to)      
{      
       int sum = 0;      
       for(int i =from;i<to;i++)      
       {      
           sum +=i;      
       }      
       printf("%d\n",sum);      
       exit(21);      
}      
      
int main()      
{      
       printf("hello world\n");      
       int num = addToTarget(1,100);      
       exit(12);   
}

image-20221225145008597

==这个函数的作用就是终止进程!无论在哪里只要遇到了就是进程终止!==

  1. 使用_exit退出!

image-20221225145245960

==_exit是一个系统调用!——使用方法和exit是相同的!==

#include <stdio.h>    
#include <string.h>    
#include <stdlib.h>    
#include<unistd.h>                                                                      
int addToTarget(int from,int to)    
{    
    int sum = 0;    
    for(int i =from;i<to;i++)    
    {    
        sum +=i;    
    }    
    printf("%d\n",sum);    
    _exit(31);    
}    
    
int main()    
{    
    printf("hello world\n");    
    int num = addToTarget(1,100);    
    exit(12);    
}

image-20221225145642969

==那么_exit和exit有什么区别呢?==

exit是属于库函数!——c语言提供

_exit是属于系统调佣——系统提供的!

库函数是在操作系统之上的!所以其实exit函数的本质就是封装了_exit !里面肯定是调用了 _exit的!

那么实际上内部究竟是哪里不同呢?

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

这个代码因为printf后面没有加上 \n所以会导致先停止两秒 然后在进程退出的时候才会被打印出来!为什么前两秒乜有刷新出来呢呢?因为此时没有\n所以导致了数据在缓冲区里,==直到进程终止后刷新了一次缓冲区==,所以hello world才打印出来了!

image-20221225150448809

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

image-20221225150536260

==换成_exit后就什么也没有打印出来了!==

这是为什么?从上面我们可以看出来其实就是==_exit它没有在进程终止之后去刷新缓冲区!==

总结

exit 终止进程 主动刷新缓冲区!

_exit 终止进程 不会刷新缓冲区

==那么这个缓冲区实际上是在哪里呢?==

image-20221225152715423

所以==缓冲区一定不会早操作系统里面!==实际上是在用户空间!——是一个用户级的缓冲区!(基础IO相关)

==所以导致了exit能够看到这个数据!但是_exit看不到这个数据==

image-20221225153447258

进程等待

进程等待的必要性

linux进程有一种状态!——Z状态(僵尸状态)这个状态是为了子进程终止后,等待父进程/操作系统来获取它的退出的信息!

但是有时候父进程一直执行不退出,导致了子进程一直处于僵尸状态,占用系统资源!此时连kill -9这个指令都没有办法,所以我们必须使用一些办法来释放僵尸状态!——我们可以通过进程等待方式来用解决僵尸进程的问题!

父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。

父进程可以通过进程等待的方式,==来回收子进程资源==,==获取子进程的退出信息!==——这就是进程为什么要等待的原因

进程等待的方法

wait函数

image-20221225155127703

wait如果等待成功,返回的就是子进程的pid,如果等待失败就会返回 -1

#include <stdio.h>
#include<unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()    
{    
       pid_t id = fork();    
       if(id == 0)    
       {    
           //子进程    
           int cnt = 10;    
           while(cnt)    
           {    
               printf("i am child pid = %d ,ppid = %d\n",getpid(),getppid());               --cnt;
               sleep(1); 
           }                
           exit(0);
       }               
       sleep(15);
       pid_t ret  =wait(NULL);
       if(id > 0)                 
       {             
           printf("wait succeeful :%d\n",ret);
       }   
       sleep(5)
}

image-20221225162111772

等到子进程执行完毕后!还要等待父进程5秒钟才能回收!——这5秒的时间都是在等待父进程醒来的僵尸状态,然后就只剩下父进程了!

waitpid

image-20221225163619473

pid_t waitpid(pid_t pid,int* status,int optios);

这个函数的如果成功返回这个子进程的pid ,如果失败返回 -1,如果子进程没有改变状态放回 0

第一个参数 pid_t pid就是要等待的子进程的pid!

第三个参数是用来说明在什么时候等待!——0就表示在阻塞时候等待!

第二分个参数 status是一个输出型参数!==就是通过等待拿到子进程的退出结果==!

#include <stdio.h>
#include<unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()    
{    
    pid_t id = fork();    
    if(id == 0)    
    {    
          //子进&status,0程    
          int cnt = 5;    
          while(cnt)    
          {    
               printf("i am child pid = %d ,ppid = %d\n",getpid(),getppid());    
               --cnt;    
               sleep(1);    
          }    
          exit(10);    
    }                                                                                   
    int status = 0;    
    pid_t ret  =waitpid(id,&status,0);    
    if(id > 0)    
    {    
          printf("wait succeeful :%d,ret :%d\n",ret,status);    
    }    
    sleep(5); 
    return 0;
}

image-20221225172359389

为什么ret的值为什么是2560呢?

==原因是因为status这个值不是整体被使用的!==——是按照特定的位图结构来进行设置不同的值!

进程退出一共有三种情况!

1,代码玩,结果对!

2,代码完,结果错!

3,代码没跑完!出异常!

那如何只使用status这个值来完整的表达这三种情况!

image-20221225173600337

int总共有32个bit位,但是只用到其中的16个bit位!

15——8这个8个bit位保存的是进程的退出码!——表示结果是否正确!

7——1 是进程终止的信号!(例如kill,或者除0,野指针,导致进程终止,也是因为操作系统发出来信号让进程终止!)表示就是是否正常结束

==所以判断进程 是否执行成功,首先就是要判断0——7位来判断子进程是否正常的结束,默认情况下为0 就是正常的!然后才去判断结果是否正常!==

所以正常的获取办法是

printf("wait succeeful :%d,sign number : %d,child exit code  :%d\n",ret,(status & 0x7F),((status >> 8)& 0xFF) );

status & 0x7F (0x7F 就是 二进制表示 1111111) 与前7位按位与获得前七位的值!

((status >> 8)& 0xFF (x0FF 就是二进制表示 11111111)就是首先 将后8位移动到前面8位的位置!然后11111111 与前8位按位与获得 后8位的值!

image-20221225174540847

==信号为 0说明正常的退出!退出码为 10 就是我们设置的退出码!==

我们可以来看一下异常退出的结果!

int a = 1;
a/=0; //在子进程的那部分加上这个代码!
int* p = NULL;
*p = 100;//这个是野指针错误!

image-20221225175543711

==信号为8 我们可以使用 kill -l 的指令来看这个信号是什么意思! 8就是以意味这浮点错误!信号为 11说明是段错误==

==code为 0是因为 一旦信号异常就不会去管起前 7位了!此时code为几都不重要!==

==信号是否为0 代表进程是否正常的退出!==

进程僵尸了之后 它的代码和数据是可以被释放掉的!因此此时已经不在运行了!但是它的PCB一定要被保留下来!

因为子进程的退出码和信号都是保留在PCB里面的!

PCB内核里面的代码

image-20221225203255773

这样子父进程调用waitpid之后就可以获取到信号和退出退出码了!——因为waitpid是系统调用!所以一旦执行就会以操作系统的身份去执行操作系统的代码!PCB就是在操作系统内部的!获得子进程信号和退出码填进status里面!

image-20221225203203035

==等待的本质就是检测子进程的退出信息并且将子进程的退出信息通过status拿回来!==

==但是我们一般使用使用的时候如果让我们自己去进行位移操作也是很麻烦的!==

所以linux给我们提供了几个宏函数让我们用来读取进程状态和退出信号!

WIFEXITED(status)

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

WEXITSTATUS(status)

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

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include<unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进&status,0程    
        int cnt = 5;    
        while(cnt)    
        {    
            printf("i am child pid = %d ,ppid = %d\n",getpid(),getppid());    
            --cnt;    
            sleep(1);    
        }    
        exit(10);    
    }    
    int status = 0;    
    pid_t ret  =waitpid(id,&status,0);    
    if(ret > 0)    
    {    
        if(WIFEXITED(status))    //判断进程是否正常退出!
        {    
            printf("exit code :%d \n",WEXITSTATUS(status)); //获取进程退出信息!(获得退出码!)   
        }                                                                                                                    
        else    
        {    
            printf("child exit no normal!\n");
        }                                            
    }
    sleep(5);

    return 0;
}

image-20230103155605309

总结!

  1. 进程退出变成僵尸!——会把自己的退出结果写入到自己的task_struct里面去!

  2. wait/waitpid是一个系统调用!——是以OS的身份去读取子进程的task_struct

  3. 即退出的时候操纵系统要从子进程的task_struct里面读取退出信息!

  4. image-20230103160727946

    task_struct内核里面的退出信息!

非阻塞等待

上面的方法如果子进程一直不退出!那么==父进程就必须一直卡在哪里==等待着它退出——这就是==阻塞式等待!==

如果我们在某一次突然发现子进程没有像我们想预想的一样退出,但是我们==又不想等待想要继续执行父进程后面的代码==,只是想==时不时查询看一下子进程到底退出了没有!==——这既是==非阻塞等待!==

检测状态!如果子进程没有就绪!那么就直接返回!——每一次都是此非阻塞等待!

多次非阻塞等待——就是轮询!

pid_t waitpid(pid_t pid,int* status,WNOHANG);

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

一般 0就是阻塞等待!WNOHANG就是非阻塞等着!

#include <stdio.h>
#include <string.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 cnt = 5;    
        while(cnt)    
        {    
            printf("i am child pid = %d ,ppid = %d\n",getpid(),getppid());    
            --cnt;    
            //int a = 0;
            //a = 1/a;
            sleep(1);    
        }    
        exit(10);    
            
    }    
    //parent    
    int status = 0;    
    while(1)//要加上循环才是轮询!否则只是非阻塞等待!
    {    
        pid_t ret = waitpid(id,&status,WNOHANG);//非阻塞状态!    
        if(ret == 0)    
        {                                                                                                                                                  
            //子进程没有退出,waitpid没有等待失败!仅仅是检测到子进程没有退出!    
            printf("wait done but the child is runing .....\n");    
            sleep(1);    
        }    
        else if(ret > 0)     
        {
            //waitpid调用成功!且子进程退出!
            printf("wait succeeful :%d,sign number : %d,child exit code  :%d\n",ret,(status & 0x7F),((status >> 8)& 0xFF) );
            break;
        }
        else 
        {
            //waitpid调用失败!
            printf("waitpid call failed\n");
            break;
        }
    }
    return 0;
}

image-20230103165213063

非阻塞等待的意义!

在非阻塞等待的是,不会占用父进程的所有的精力!

可以在轮询期间做一些其他的事情!

例如下面的例子!

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include<unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#define NUM 10 
typedef void(*func_t)();//函数指针!

func_t handlerTask[NUM];//函数指针数组!
void task1()
{
    printf("handler task1\n");
}
void task2()
{
    printf("handler task2\n");
}
void task3()
{
    printf("handler task3\n");
}
void loadTask()
{
    memset(handlerTask,0,sizeof(handlerTask));
    handlerTask[0] = task1;
    handlerTask[1] = task2;
    handlerTask[2] = task3;

}                   
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进程                                                                                                                                             
        int cnt = 5;
        while(cnt)
        {
            printf("i am child pid = %d ,ppid = %d\n",getpid(),getppid());
            --cnt;
            sleep(1);
        }
        exit(10);

    }
    //parent
    loadTask();
    int status = 0;
    while(1)
    {
        pid_t ret = waitpid(id,&status,WNOHANG);//非阻塞状态!
        if(ret == 0)
        {
            //子进程没有退出,waitpid没有等待失败!仅仅是检测到子进程没有退出!
            printf("wait done but the child is runing .....\n");
            sleep(1);
            for(int i = 0;handlerTask[i] != NULL;i++)
            {
                handlerTask[i]();//采用回调的方式,执行我们想让父进程在空闲的时候做的事情!

            }
        }
        else if(ret > 0) 
        {
            //waitpid调用成功!且子进程退出!                                                                                                              
            printf("wait succeeful :%d,sign number : %d,child exit code  :%d\n",ret,(status & 0x7F),((status >> 8)& 0xFF) );
            break;
        }
        else 
        {
            //waitpid调用失败!
            printf("waitpid call failed\n");
            break;
        }
    }
    return 0;
}

image-20230103171024305

进程程序替换——重点

创建子进程的目的是什么呢?

  • 想让子进程执行父进程代码的一部分!——就是执行父进程对应的磁盘代码的一部分!
  • ==想让子进程执行一个全新的程序!==——让子进程加载磁盘上指定的程序!执行程序的代码和数据!==这就是进程的程序替换!==

替换函数——初识execl

#include <unistd.h>`
int execl(const char *path, const char *arg, ...);

==函数的作用就是将程序加载到内存中,让指定进程进行执行!==

image-20230103173914601

#include <stdio.h>      
#include <unistd.h>      
int main()      
{      
       printf("procee is running\n");      
      
       //加载程序!      
       execl("/usr/bin/ls"/*找到程序*/,"ls","--color=auto","-a","-l",NULL);                 
       //所有exec的接口都必须以NULL结尾!表示传参结束!      
      
      
       printf("process is done\n");      
       return 0;      
}      

image-20230103174859995

==成功的调用了ls命令!==——但是奇怪了为什么 没有process is done 打印出来呢?

这就要说到进程替换的原理了!

进程替换原理

image-20230104161923227

程序替换的本质就是将==指定的程序的代码和数据加载到指定的位置!==(指定的位置就是用来覆盖进程自己的代码和数据!)

==进程替换的时候没有创建新的进程!==——因为没有创建新的PCB!

所以我们就可以解释为什么process is done 没有被打印出来了!

因为调用exec函数后原来的代码和数据全都被覆盖掉了!原来的printf指令也是属于代码和数据!

被覆盖掉了!自然就无法执行了!

如果函数调用失败!——即没有替换成功!那么就不会发生替换!

image-20230104162706320

exec系列的函数只会出错的时候返回!——成功之所以不返回!是因为一旦函数执行成功本进程的后续所有代码就被替换了!都没有意义了!

进程替换演示

一般进程替换我们都是使用子进程来调用其他程序!

#include <stdio.h>    
#include <unistd.h>    
#include <stdlib.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
int main()    
{    
    printf("process is running....\n");    
    pid_t id = fork();    
    if(id == 0)    
    {    
        sleep(1);    
        execl("/usr/bin/ls","ls","-a","-l","--color=auto",NULL);                   
        exit(1);    
    }    
    int status = 0;    
    pid_t ret = waitpid(id,&status,0);    
    if(ret > 0)    
    {    
        printf("wait success exit code: %d,sig :%d\n",(status>>8)&0xFF,status &0x7F);
    }  
    return 0;
}

image-20230104164245680

==我们发现子进程的程序替换是不会影响父进程的!==——进程具有独立性!

==因为一旦使用fork之后父子进程都会有自己的task_struct!,然后一旦调用exec函数就会导致子进程的数据发生改变,从而发生写实拷贝!在内存里面给子进程重新开辟一个新的物理内存!而父进程的数据不受任何影响!==

替换函数——重点

int execlp(const char *file, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execle(const char *path, const char *arg, ...,char *const envp[]);

除了上面的哪一个替换函数还有剩下4种替换函数!

image-20230104165247156

int execl(const char *path, const char *arg, ...);

上面我们使用的这个函数后面的 l就是list的的意思——是指将指令参数一个个的传过去!

execlp
int execlp(const char *file, const char *arg, ...);

p:就是path

带p字符的函数不用告诉其程序的路径!只要说程序是谁!——==那么这个函数就会在环境变量PATH里面去进行可执行程序的查找!==

#include <stdio.h>    
#include <unistd.h>    
#include <stdlib.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
int main()    
{    
       printf("process is running....\n");    
       pid_t id = fork();    
       if(id == 0)    
       {    
           sleep(1);    
           execlp("ls","ls","-a","-l","--color=auto",NULL);//第一个ls是程序名!第二个ls是指令!     
           exit(1);    
       }    
       int status = 0;    
       pid_t ret = waitpid(id,&status,0);    
       if(ret > 0)    
       {    
           printf("wait success exit code: %d,sig :%d\n",(status>>8)&0xFF,status &0x7F);
       }  
       return 0;
}

image-20230104165942205

==程序照样正常执行!==

execv
int execv(const char *path, char *const argv[]);

v的意思是vector的意思!——相比execl可以将所有的执行参数都统一放入一个指针数组中!进行统一的传递!

#include <stdio.h>    
#include <unistd.h>    
#include <stdlib.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
int main()    
{    
       printf("process is running....\n");    
       pid_t id = fork();    
       if(id == 0)    
       {    
           sleep(1);    
           char* const argv_[]{
               "ls",
               "-a",
               "-l",
               "--color=auto",
               NULL
           };//也要以NULL结尾!
           execv("/usr/bin/ls",argv_);
           exit(1);    
       }    
       int status = 0;    
       pid_t ret = waitpid(id,&status,0);    
       if(ret > 0)    
       {    
           printf("wait success exit code: %d,sig :%d\n",(status>>8)&0xFF,status &0x7F);
       }  
       return 0;
}
execvp
int execvp(const char *file, char *const argv[]);

和上面的带p的一样就是可以不带路径!

这个exec函数会自动的在环境变量中找程序,同时使用的是v的数组传参!

#include <stdio.h>    
#include <unistd.h>    
#include <stdlib.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
int main()    
{    
       printf("process is running....\n");    
       pid_t id = fork();    
       if(id == 0)    
       {    
           sleep(1);    
           char* const argv_[]{
               "ls",
               "-a",
               "-l",
               "--color=auto",
               NULL
           };//也要以NULL结尾!
           execv("ls",argv_);//不用找路径!
           exit(1);    
       }    
       int status = 0;    
       pid_t ret = waitpid(id,&status,0);    
       if(ret > 0)    
       {    
           printf("wait success exit code: %d,sig :%d\n",(status>>8)&0xFF,status &0x7F);
       }  
       return 0;
}

虽然刚刚我们演示的都是系统自带的指令!

但是我们也可以去调用我们自己写的程序!

execl("./mybin","mybin",NULL);//使用我们自己的的程序路径!

==而且这个程序替换不仅仅可以执行相同语言的程序!==

==其他语言写的的程序也是可以可以执行的!==——因为替换的时候是系统给我们进行替换的!

image-20230104173505242

==像是java/Python写的程序都是可以调用的!==

==我们可以使用程序替换,调用任何语言对应的可执行文件!==

execle
int execle(const char *path, const char *arg, ...,char *const envp[]);

e就是自定义环境变量!

我们可以在执行我们的进程的时候将环境变量给传进去!

#include <stdio.h>    
#include <unistd.h>    
#include <stdlib.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
int main()    
{    
    printf("process is running....\n");    
    pid_t id = fork();    
    if(id == 0)    
    {    
        sleep(1);    
        char* const envp_[] = {
            "MYENV=112233445566"
        };//我们在自己进程里面定义的环境变量!
        execle("./mybib","mybin",NULL,envp_);
        extern char** envron;
        //execle("./mybib","mybin“,NULL,envron);//也可以传系统给我们的环境变量!
        exit(1);    
    }    
    int status = 0;    
    pid_t ret = waitpid(id,&status,0);    
    if(ret > 0)    
    {    
        printf("wait success exit code: %d,sig :%d\n",(status>>8)&0xFF,status &0x7F);
    }  
    return 0;
}

//mybin.c
#include <stdio.h>    
#include <stdlib.h>    
int main()    
{    
    printf("PATH:%s\n",getenv("PATH"));    
    printf("PWD:%s\n",getenv("PWD"));    
    printf("MYENV:%s\n",getenv("MYENV"));    
    printf("这是我的程序!\n");                                                                                                                              
    printf("这是我的程序!\n");    
    printf("这是我的程序!\n");    
    printf("这是我的程序!\n");    
    printf("这是我的程序!\n");    
    printf("这是我的程序!\n");    
    return 0;    
}  

image-20230104180145230

上面的是通过execle调用的进程!因为使用的是我们自己定义的环境变量!

下面的mybin使用的是系统自带的环境变量!

==我们也可以使用putenv将我们自己的变量导入到环境变量里面!==

putenv((char*)"MYENV=44332211");
extern char** environ;
execle("./mybib","mybin“,NULL,envron);//也可以传系统给我们的环境变量!

image-20230104223825172

execve

这个是真正的系统调用接口!

image-20230104230655463

属于的是2号手册——即系统调用的接口!

==上面调用的所有接口本质都是这一个接口的封装!目的是为了给我们更多选择更加方便的使用!==

exec函数总结

在linux中将程序加载到内存中的都是使用的是exec*系列的接口!——这些接口又称为加载器!

==我们在命令行上面使用 ./xxx 来运行程序也是通过调用这一系列的接口的!==

image-20230104230118041

==main函数的三个参数的都是由exec这个接口传参过来了的!==

==上面的exec函数有的虽然没有env参数,但是可以通过eviron这个第三方变量来获取环境变量!==

exec函数的应用

#include <stdio.h>    
#include <unistd.h>    
#include <stdlib.h>    
#include <sys/types.h>    
#include <sys/wait.h>    
int main(int argc,char* argv[])    
{    
    printf("process is running....\n");    
    pid_t id = fork();    
    if(id == 0)    
    {    
        execvp(argv[1],&argv[1]);
        exit(1);    
    }    
    int status = 0;    
    pid_t ret = waitpid(id,&status,0);    
    if(ret > 0)    
    {    
        printf("wait success exit code: %d,sig :%d\n",(status>>8)&0xFF,status &0x7F);
    }  
    return 0;
}

image-20230104232223140

我们可以在后面带上各种指令!——通过这一点我们就可以自己写一个简易的shell!

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <assert.h>
#include <string.h>
#define NUM 1024
#define OPT_NUM 64
char LineCommand[NUM];
char* myargv[OPT_NUM];

int main()
{
     while(1)
     {
           //打印输出提示符      
           printf("用户名@主机名 当前路径# ");      
           fflush(stdout);//刷新缓冲区!      

           //获取用户输入,我们输入的时候会自己输入\n      
           char* s = fgets(LineCommand,sizeof(LineCommand)-1,stdin);      
           (void)s;      
           assert (s != NULL);      
           LineCommand[strlen(LineCommand)-1] = 0;//这是为了清楚最后一个\n!      

           //"ls -a -l" -> "ls" "-a" "-l" 切割字串!      
           myargv[0] = strtok(LineCommand," ");      
           int i = 1;
           if(myargv[0] != NULL && strcmp(myargv[0],"ls") ==0)
           {
               myargv[i++] = (char*)"--color=auto";//自动给ls加上颜色
           } 
           //strtok遇到空的返回值为NULL 同时要myargv要以NULL结尾!      
           while(myargv[i++] = strtok(NULL," "));      
           //执行命令!——让子进程执行!
           pid_t id = fork();
           assert(id != -1);
           if(id == 0)
           {
               execvp(myargv[0],myargv);
               exit(1);
           }
           waitpid(id,NULL,0);
     }
}

上面写的简易的shell有个问题!那就是当我们使用cd的时候却无法切换路径!

image-20230105102133160

这是为什么呢?——首先我们要理解一个概念==”当前路径“==

什么叫当前路径?我们可以先写一个小进程来进行测试

在linux里面,当一个进程运行的时候,会自动的在/proc/下面形成以这个进程的pid为名字的目录!image-20230105103057753

这个文件里面包含了进程的所有属性!

image-20230105103511911

==那么这个cwd是可以修改的吗?==——可以系统为我们提供了一个函数

chdir

image-20230105103818132

这个函数的作用就是更改当前的工作路径(cwd)!

使用也很简单!只要把要切换到的路径的给当做参数传上去就可以!

#include <stdio.h>      
#include <unistd.h>      
int main()      
{      
       chdir("/home/xxx");                                                 
       while(1)      
       {      
           printf("我是一个进程!:%d\n",getpid());      
           sleep(1);      
       }      
       return 0;
}

image-20230105104355180

我们可以看到cwd成功的切换了!

==那我们自己的shell为什么cd的时候路径没有发生变化呢?==

因为这个当前路径的目录是==只属于当前这个进程的!==

但是我们执行shell指令的时候!我们都是使用==fork创建子进程来替我们执行==的!

是子进程的执行的cd,我们使用cd本质改变的是子进程的cwd!不是我们父进程的cwd!

但是子进程执行完毕就没有了,我们仍然使用的是父进程!

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <assert.h>
#include <string.h>
#define NUM 1024
#define OPT_NUM 64
char LineCommand[NUM];
char* myargv[OPT_NUM];

int main()
{
    while(1)
    {
        printf("用户名@主机名 当前路径# ");
        fflush(stdout);
        char* s = fgets(LineCommand,sizeof(LineCommand)-1,stdin);
        (void)s;
        assert (s != NULL);
        LineCommand[strlen(LineCommand)-1] = 0;
        myargv[0] = strtok(LineCommand," ");
        int i = 1;
        if(myargv[0] != NULL && strcmp(myargv[0],"ls")==0)               
        {
            myargv[i++] = (char*)"--color=auto";
        }
        while(myargv[i++] = strtok(NULL," "));
        //如果是cd 指令就不需要创建子进程,让shell自己执行对应的命令!,本质就是调>      用系统接口!
        if(myargv[0] != NULL  && strcmp(myargv[0],"cd")==0)
        {
            if(myargv[1] != NULL)
            {                                                           
                chdir(myargv[1]);
            }
            continue;//使用完后就不需要用到子进程了所以直接continue
        }
        pid_t id = fork();
        assert(id != -1);
        if(id == 0)
        {
            execvp(myargv[0],myargv);
            exit(1);
        }
        waitpid(id,NULL,0);
    }
    return 0;
}


==这种不需要让子进程执行!让shell自己执行的命令我们——我们叫做内建/内置命令!==

==我们一直使用的echo也是一个内置命令!所以既可以查找到环境变量,也可以查找的我们本地变量!==——所以我们也可以自己写一个简单的echo出来!

int main()
{
       while(1)
       {
           printf("用户名@主机名 当前路径# ");
           fflush(stdout);
           char* s = fgets(LineCommand,sizeof(LineCommand)-1,stdin);
           (void)s;
           assert (s != NULL);
           LineCommand[strlen(LineCommand)-1] = 0;
           myargv[0] = strtok(LineCommand," ");                                                                   
           int i = 1;
           if(myargv[0] != NULL && strcmp(myargv[0],"ls")==0)
           {
               myargv[i++] = (char*)"--color=auto";
           }
           if(myargv[0] != NULL  && strcmp(myargv[0],"cd")==0)
           {
               if(myargv[1] != NULL)
               {
                   chdir(myargv[1]);
               }
               continue;
           }
           //自建命令!——echo
           if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0],"echo")==0 )
           {
               //获取进程结束的结果!
               if(strcmp(myargv[1],"$?")==0)
               {
                   printf("%d,%d\n",lastCode,lastSig);
               }
               else                
               {
                   printf("%s\n",myargv[1]);
               }
               continue;
           }

           pid_t id = fork();
           assert(id != -1);
           if(id == 0)
           {
               execvp(myargv[0],myargv);
               exit(1);
           }
           //获得进程接收的结果!
           int status = 0;
           pid_t ret =  waitpid(id,&status,0);
           assert(ret > 0);
           lastCode = ((status>>8)&0xFF);
           lastSig = status& 0x7F;
       }
       return 0;
}
【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月27日 0

暂无评论

推荐阅读
1EQmf8Oo0jTP