Linux进程控制

一、进程终止

0x01 进程退出场景

①代码运行完毕,结果正确,退出码为0
②代码运行完毕,结果不正确,退出码!0,但是有多种可能
③代码异常终止

0x02 退出码

#include       
int main()    
{    
  printf("hello world\n");    
  return 123;                                                                      
}

Linux进程控制_第1张图片

 ①echo $?输出最近一次进程退出时的退出码
 ②第一次输出的退出码123,是main函数的return值
 ③第二次就是正常的退出码

0x03 退出码对应的错误原因

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

Linux进程控制_第2张图片

0x04 代码异常终止

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

此时,相当于vs中的程序奔溃,退出码也就变得没有意义了 

0x05 进程退出的方式

 ①main函数return,代表进程退出
 ②exit在任意地方调用,都代表终止进程,参数是退出码

#include                                                               
int fun()    
{    
  exit(123);    
  printf("hello world\n");    
  return 1;    
}      
int main()    
{    
    fun();    
    for(int i = 0;i < 100;i++)    
    {    
        printf("%d: %s\n",i,strerror(i));    
    }    
    return 0;    
}

 

#include    
#include    
#include    
#include      
int main()    
{    
  printf("hello world");//数据时被暂时保存在输出缓冲区中  
  sleep(4);                                     
  exit(EXIT_SUCCESS); //exit 或者main return 本身就会要求系统进行缓冲区刷新   
  //return 0;    
} 

//运行代码
[wh@bogon lesson11]$ ./myproc
hello world[wh@bogon lesson11]$

③_exit终止进程,强制终止进程,不会进行进程的后续收尾工作,比如刷新缓冲区(用户级缓冲区)

#include    
#include    
#include    
#include      
int main()    
{    
  printf("hello world");    
  sleep(4);    
  _exit(12);                                                                     
  // exit(EXIT_SUCCESS);    
  //return 0;    
} 

④_exit()与exit的区别  

Linux进程控制_第3张图片

0x06 进程退出,操作系统层面做了什么?

 系统层面,少了一个进程,释放了 PCB,mm_struct,页表和各种映射关系,代码,数据和申请的空间 

二、进程等待

0x01 进程等待是什么

fork()之后,子进程是为了帮助父进程完成某种任务,而父进程要知道子进程做的怎么样,所以让父进程fork()之后,需要通过wait/waitpid等待子进程退出信息

0x02 为什么要让父进程等待呢?

①通过获取子进程退出信息,能够得知子进程的执行结果
②可以保证时序的问题,子进程先退出,父进程后退出
③进程退出的时候会先进入僵尸状态,会造成内存泄漏的问题,需要通过父进程wait,释放该子进程占用的资源

0x03 如何进行进程等待?

 方法一 wait方法:

#include
#include
pid_t wait(int*status);

返回值: 成功返回被等待进程pid,失败返回-1。

参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

#include    
#include    
#include    
#include    
#include    
#include    
    
int main()    
{    
  pid_t id = fork();    
  if(id == 0)    
  {    
    int cnt = 5;    
    while(cnt)    
    {    
      printf("child[%d] is running: cnt is : %d\n",getpid() ,cnt);    
      cnt--;    
      sleep(1);    
    }    
    exit(0);    
  }    
   sleep(10);    
   pid_t ret = wait(NULL);    
   if(ret > 0)    
   {    
      printf("parent wait: %d,success\n",ret);    
   }    
  else    
  {    
      printf("parent wait failed\n");    
  }    
  sleep(10);    
}

Linux进程控制_第4张图片

 Linux进程控制_第5张图片

 子进程先执行5s,然后退出,父进程也同时执行5s,此时因为子进程已经退出,所以是僵尸进程,再5s之后,父进程等待成功,释放子进程的资源,然后又休眠10s,父进程进入睡眠状态,之后父进程结束

方法二 waitpid方法:  

pid_t ret = waitpid(id,NULL,0);  //等待执行一个进程
pid_t ret = waitpid(-1,NULL,0);  //等待任意一个子进程,等价于wait
pid_t waitpid(pid_t pid, int *status, int options);

返回值:

pid > 0: 返回子进程的pid
pid < 0: 等待失败

参数1:  

pid:
pid = -1,表示等待任意一个子进程,等价于wait
pid > 0,等待指定的子进程

参数2:

status:
①是一个输出型参数,有操作系统填充
②如果传递NULL.表示不关心子进程的退出状态信息
③否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程
④父进程拿到的status结果,一定和子进程如何退出强相关
⑤子进程的退出结果,共有三种,代码异常终止则会收到某种信号,如果没有收到信号说明没有问题,之后就会返回退出码给父进程,最终会让父进程通过status得到子进程执行的结果

Linux进程控制_第6张图片

①status表示的共32bit,但是高16bit没有用到,只使用低16个bit
②低8bit:表示某种信号,如果收到这种信号,则表示异常终止,未收到则表示正常运行,一般情况下值为0,但是这8bit也只用到了前7bit
③高8bit:表示子进程返回的退出状态  

 查看status所收到的信息:

#include    
#include    
#include    
#include    
#include    
    
int main()    
{    
  pid_t id = fork();    
  if(id == 0)    
  {    
    int cnt = 5;    
    while(cnt)    
    {    
      printf("child[%d] is running: cnt is : %d\n",getpid() ,cnt);    
      cnt--;    
      sleep(1);    
    }    
    exit(12);
  }
  int status = 0;
  pid_t ret = waitpid(id,&status,0);

  if(ret > 0)
   {
      printf("parent wait: %d,success,status exit code: %d,status exit signal: %d\n",ret,(status>>8)&0xFF,status&0x7F);
//status exit code:因为是高8位,所以先右移8位,然后与0xFF按位与
//status exit signal:因为是低8位,但是只用到了前7位,所以与0x7F按位与
   }
  else
  {
      printf("parent wait failed\n");
  }                                                                                }

Linux进程控制_第7张图片

status收到的退出码为12,终止信号为0,则表示代码跑完,但结果不正确  

当然还有俩种情况分别是:

①代码运行完毕,结果正确,比如status收到的退出码为0,并且正常运行 

Linux进程控制_第8张图片

 ②代码异常终止:比如代码没有跑完,但是子进程被干掉,此时会收到终止信号,并且退出码已经不重要了

 当然想要获取退出码还有一种方式:

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

#include
#include
#include
#include
#include

int main()
{
  pid_t id = fork();
  if(id == 0)
  {
    int cnt = 5;
    while(cnt)
    {
      printf("child[%d] is running: cnt is : %d\n",getpid() ,cnt);
      cnt--;
      sleep(1);
    }
    exit(12);
  }
  int status = 0;
  pid_t ret = waitpid(id,&status,0);
  if(ret > 0)
  {
        if(WIFEXITED(status))//没有收到任何终止信号
       {
            printf("exit code: %d\n",WEXITSTATUS(status));//正常结束,获取对应的退出码
        }
   }
}

 参数3:

options:
0:默认行为,表示阻塞等待,子进程没返回,父进程什么也没做,一直等子进程 WNOHANG: 设置等待方式为非阻塞等待

阻塞等待: 张三找李四跑步,李四在学习,让张三等30分钟,张三此时让李四电话不要挂,通过电话知道李四的状态,等学习完在电话中说一声再挂电话,李四不学完,张三不走,主打一个死等,即子进程不退出,父进程不返回
非阻塞等待: 张三找李四跑步,李四在学习,让张三等30分钟,张三2分钟一个电话,检测李四学没学完,不会因为李四这次没学完而把自己卡住,一次等待可能需要多次检测,是基于非阻塞等待的轮询方案  


非阻塞等待:
①如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
②如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。 如果不存在该子进程,则立即出错返回。 

阻塞等待了是不是意味着父进程不被调度了?  

阻塞本质:进程的PCB被放入了等待队列,并将进程的状态改为S状态
返回的本质:进程的PCB从等待队列拿到运行队列,从而被CPU调度 

基于非阻塞的轮询方案:  

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

int main()
{
  pid_t id = fork();
  if(id == 0)
  {
    int cnt = 10;
    while(cnt)
    {
      printf("child[%d] is running: cnt is : %d\n",getpid() ,cnt);
      cnt--;
      sleep(1);
    }
    exit(12);
  }
  int status = 0;
  while(true)
  {
      pid_t ret = waitpid(id,&status,WNOHANG);
      if(ret == 0)
      {
      	//子进程没有退出,但waitpid等待成功,需要父进程重复进行等待
        printf("Do parent things\n");
      }
      else if(ret > 0)
      {
      	//子进程退出,waitpid也成功等待,获取到对应的结果
        printf("parent wait: %d,success,status exit code: %d,status exit signal: %d\n",ret,(status>>8)&0xFF,status&0x7F);                                                                                          
        break;
      }
      else{ 
      	//ret < 0
		//等待结果
        perror("waitpid");
        break;
      }
      sleep(1);
  }
}

Linux进程控制_第9张图片

父进程没有等到子进程的退出信息,所以一直在做自己的事情,等子进程退出时,父进程成功等待,获得了子进程的退出信息 

三、程序替换

0x01 什么叫程序替换?

进程不变,仅仅替换当前进程的代码和数据的技术

0x02 程序替换例子

#include    
#include    
#include    
#include    
#include    
int main()    
{    
  printf("I am a process,pid: %d\n",getpid());    
    
  execl("/usr/bin/ls","ls","-a","-l",NULL);                                            
  printf("hello world\n");    
}

Linux进程控制_第10张图片

此时就会有一个问题,为什么"hello world"没有被执行呢? 而且第一句printf却被执行了呢?

因为"hello world"被execl进程了程序替换,而第一句printf是先执行,之后才进行的程序替换

0x03 程序替换的本质

程序替换的本质就是把程序的进程代码+数据,加载进特定进程的上下文中
通俗的讲,比如:C/C++程序要运行,必须先加载到内存中,那么如何加载呢?是通过加载器,也就可以理解为通过exec系列的程序替换函数将程序从磁盘加载到内存

0x04 创建子进程的目的

之前我们创建子进程的目的即是让子进程执行父进程代码的一部分,但是如果我们想让子进程执行一个全新的程序呢?

此时用到的即是程序替换

#include
#include
#include
#include
#include
#include
int main()
{
  pid_t id = fork();
 if(id == 0)
 {
  printf("I am a process,pid: %d\n",getpid());

  execl("/usr/bin/ls","ls","-a","-l",NULL);
  printf("hello world\n");
  exit(0);
 }
 while(1)
 {
    printf("I am a parent\n");
    sleep(1);
 }                        
 //parent
 waitpid(id,NULL,0);
 printf("success\n");
}

Linux进程控制_第11张图片

首先执行父进程,之后子进程,子进程进行了程序替换,然后一直执行着父进程 此时我们会有一个疑问:不是说父子代码是共享的吗?为什么他们执行出的结果不一样? 因为程序替换会更改代码区的代码,此时就会发生写实拷贝,进程具有独立性,俩个进程之间互不干扰  

0x05 exec系列函数

 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[]);//系统调用

理解: 

l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量  

Linux进程控制_第12张图片 

0x06 exec系列返回问题

只要进程的程序替换成功,就不会执行后续代码,意味着exec系列函数,成功的时候,不需要返回值检测 当然只要execl系列返回了,就一定是因为调用失败了

#include    
#include    
#include    
#include    
#include    
#include    
    
int main()    
{    
  printf("I am a process,pid: %d\n",getpid());    
  execl("/usr/bin/lsss","ls","-a","-l",NULL);    
  printf("hello world\n");                                                           
} 

 

0x07 execl

Linux进程控制_第13张图片

#include    
#include    
#include    
#include    
#include                                                                   
int main()    
{    
  if(fork() == 0)    
  {    
      printf("hello world\n");    
      execl("/usr/bin/ls","ls","-a","-l",NULL);    
      printf("end\n");    
      exit(1);    
  }     
  pid_t ret = waitpid(-1,NULL,0);    
  if(ret > 0)    
  {    
    printf("wait child success\n");    
  }    
}

Linux进程控制_第14张图片

0x08 execv

  #include    
  #include    
  #include    
  #include    
  #include    
  int main()    
  {    
    if(fork() == 0)    
    {    
        printf("hello world\n");    
        char* argv[] = { "ls","-a","-l",NULL };                             
        execv("/usr/bin/ls",argv);    
        printf("end\n");    
        exit(1);    
    }     
    pid_t ret = waitpid(-1,NULL,0);    
    if(ret > 0)    
    {    
      printf("wait child success\n");    
    }    
  }  

Linux进程控制_第15张图片

 只是将execl的可变式列表变成了指针数组

0x09 execlp

#include    
#include    
#include    
#include    
#include    
int main()    
{    
  if(fork() == 0)    
  {    
      printf("hello world\n");    
      execlp("ls","ls","-a","-l","-d",NULL);                                           
      printf("end\n");    
      exit(1);    
  }      
  pid_t ret = waitpid(-1,NULL,0);    
  if(ret > 0)    
  {    
    printf("wait child success\n");    
  }    
} 

Linux进程控制_第16张图片

l: 表示采用列表的形式
p:自动搜索环境变量PATH,不用再写明路径  

0X10 execvp

 

  #include    
  #include    
  #include    
  #include    
  #include    
  int main()    
  {    
    if(fork() == 0)    
    {    
        printf("hello world\n");    
        char* argv[] = { "ls","-a","-l","-d",NULL };                                               		 
        execvp("ls",argv);    
        printf("end\n");    
        exit(1);    
    }      
    pid_t ret = waitpid(-1,NULL,0);    
    if(ret > 0)    
    {    
      printf("wait child success\n");    
    }    
  }

Linux进程控制_第17张图片

v:指针数组的形式
p:不用指明详细路径  

0x11 用一个程序去调用另一个程序

 

//myproc.c,用这个程序去调用myexe
#include    
#include    
#include    
#include    
#include    
int main()    
{    
  if(fork() == 0)    
  {    
      printf("hello world\n");    
      execl("./myexe","myexe",NULL);    
      printf("end\n");    
      exit(1);    
  }          
  pid_t ret = waitpid(-1,NULL,0);    
  if(ret > 0)    
  {    
    printf("wait child success\n");    
  }    
}  

//myexe.c
#include      
int main()    
{    
  printf("hello test\n");   
  return 0;    
}

一次形成俩个可执行程序:

因为伪目标所以总是被执行,但是没有依赖方法,所以并不会形成all,但是有依赖方法,Makefile在执行的时候一定是想先形成的是all,先形成all,那么就要先形成myexe 和myproc,但是又没有依赖方法,并不会再形成all

//Makefile
.PHONY:all    
all:myproc myexe    
myproc:myproc.c    
  gcc -o $@ $^    
myexe:myexe.c    
  gcc -o $@ $^    
.PHONY:clean    
clean:    
  rm -f myproc myexe  

0x12 execle

  #include    
  #include    
  #include    
  #include    
  #include    
  int main()    
  {    
    if(fork() == 0)    
    {    
        printf("hello world\n");    
        char* env[] = {"MYENV = hello wrold","MYENV1 = HELLO WORLD",NULL};   
        execle("./myexe","myexe",NULL,env);   
        printf("end\n");    
        exit(1);    
    }        
    pid_t ret = waitpid(-1,NULL,0);    
    if(ret > 0)    
    {    
      printf("wait child success\n");    
    }    
  } 

 Linux进程控制_第18张图片

 ①当只执行./myexe的时候,输出的是系统的环境变量
 ②当通过myproc来调用myexe的时候,使用的就是我们导入的环境变量(比如上面的例子)

0x13 execve

  #include    
  #include    
  #include    
  #include    
  #include    
  int main()    
  {    
    if(fork() == 0)    
    {                                                                                  
        printf("hello world\n");    
        char* env[] = {"MYENV = hello wrold","MYENV1 = HELLO WORLD",NULL};   
        char* argv[] = {"myexe",NULL};    
        execve("./myexe",argv,env);    
        printf("end\n");    
        exit(1);    
    } 
    pid_t ret = waitpid(-1,NULL,0);    
    if(ret > 0)    
    {    
      printf("wait child success\n");    
    }    
  } 

Linux进程控制_第19张图片

 e:就是自己添加环境变量
 v:数组

四、制作一个简易版的shell

0x01 打印提示符

#include
#include    
    
#define NUM 128    
int main()    
{    
  char command[NUM];    
  for(;;)    
  {    
     command[0] = '\0';    
     printf("[yh@Linux yh_shell]$");    
     fflush(stdout);    
     fgets(command,NUM,stdin);//获取命令字符串    
     command[strlen(command)-1] = '\0';    
     printf("echo: %s\n",command);                                           
  }    
}

0x02 解析命令字符串

       const char* sep = " ";    
       argv[0] = strtok(command,sep);    
       int i = 1;    
       while(argv[i] = strtok(NULL,sep))    
       {    
          i++;    
       }    
       for(i = 0;argv[i];i++)    
       {    
          printf("argv[%d],%s\n",i,argv[i]);   
       }  

0x03 执行第三方命令

       if(fork() == 0)    
       {    
          execvp(argv[0],argv);    
          exit(1);    
       }    
       waitpid(-1,NULL,0);  

Linux进程控制_第20张图片

0x04 为什么cd ..,返回上一级路径,却没有变化?

 Linux进程控制_第21张图片

 父进程路径:

 

①因为执行cd ..,是子进程在执行,执行回退的并非是shell,而我们是想让父进程bash去执行回退

②fork()要执行的命令是第三方命令,而我们想以内建的方式运行,即不创建子进程,而是让父进程shell自己执行,相当于调用了自己的一个函数

0x05 进行修改

chdir 是C语言中的一个系统调用函数(同cd),用于改变当前工作目录,其参数为Path 目标目录,可以是绝对目录或相对目录。(参考自百度)

	//检测命令是否需要shell本身执行的,内建命令
   	if(strcmp(argv[0],"cd") == 0)    
       {    
          if(argv[1] != NULL)    
          {    
            chdir(argv[1]);    
            continue;    
          }    
       } 

Linux进程控制_第22张图片

0x06 整体代码

 #include
  #include
  #include
  #include
  #include
  #include                                                         
  
  #define NUM 128
  #define CMD_NUM 64
  int main()
  {
    char command[NUM];
    for(;;)
    {
       command[0] = '\0';
       char* argv[CMD_NUM] = {NULL};
       printf("[yh@Linux yh_shell]$");
       fflush(stdout);
       fgets(command,NUM,stdin);
       command[strlen(command)-1] = '\0';
  //     printf("echo: %s\n",command);
  
       const char* sep = " ";
       argv[0] = strtok(command,sep);
       int i = 1;
       while(argv[i] = strtok(NULL,sep))
       {
          i++;
       }
  //     for(i = 0;argv[i];i++)
  //     {
  //        printf("argv[%d],%s\n",i,argv[i]);
  //     }
  
       if(strcmp(argv[0],"cd") == 0)
       {
          if(argv[1] != NULL)
          {
            chdir(argv[1]);
            continue;
          }
       }
	  if(fork() == 0)
       {
          execvp(argv[0],argv);
          exit(1);
       }
       waitpid(-1,NULL,0);
    }
  }            

 

你可能感兴趣的:(【Linux学习】,linux,centos)