0
点赞
收藏
分享

微信扫一扫

linux之进程控制(万字长文详解)

进程控制

进程创建——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 ?

fork常规用法

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

fork调用失败的原因

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

进程终止

退出码

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

int main()
{
       return 0;
}

为什么要返回 -0?

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

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

==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.代码没跑完,程序异常了——这时候退出码就没有意义了!

进程如何退出

1.main函数返回!

2.任意地方调用exit!

进程等待

进程等待的必要性

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这个值不是整体被使用的!==——是按照特定的位图结构来进行设置不同的值!

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内核里面的退出信息!

非阻塞等待

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

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

非阻塞等待的意义!

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

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

例如下面的例子!

#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的时候路径没有发生变化呢?==

举报

相关推荐

0 条评论