Linux-进程控制

文章目录

  • 进程创建
    • fock函数
    • 写时拷贝
  • 进程终止
  • 进程等待
  • 进程程序替换
  • 简易shell

进程创建

操作系统允许一个进程创建另一个进程,并且允许子进程继承父进程所拥有的资源,当子进程被终止时,其在父进程处继承的资源应当还给父进程。同时,终止父进程同时也会终止其所有子进程。
注意:Linux操作系统对于终止有子进程的父进程,会把子进程交给1号进程接管。

进程创建:1、命令行启动命令(程序、指令等) 2、通过程序自身,fork出子进程

创建进程的过程:

  1. 操作系统为新进程分配一个唯一的进程标识号,并申请一个空白的PCB,PCB是有限的,若申请失败则创建失败。
  2. 为进程分配资源,此处如果资源不足,进程就会进入等待状态,以等待资源。
  3. 初始化PCB
  4. 如果进程的调度队列能够接纳新进程,那就将进程插入到就绪队列,等待被调度运行。

fock函数

父进程通过调用fork函数创建一个新的运行的子进程。
新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。父进程和新创建的子进程之间的最大区别在于它们有不同的PID

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

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

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器
  1 #include <stdio.h>                                                                                                                                   
  2 #include <unistd.h>
  3 
  4 int main()
  5 {
  6     const char *str = "hello world";
  7 
  8     pid_t pid = fork();
  9     //之后才会运行
 10     if(pid == 0){
 11         while(1){
 12           printf("child: ppid: %d, pid: %d, str: %s\n", getppid(), getpid(), str);
 13           sleep(1);
 14         }
 15     }
 16     else if(pid > 0){
 17         while(1){
 18           printf("father: ppid: %d, pid: %d, str: %s\n", getppid(), getpid(), str);
 19           sleep(1);
 20         }
 21     }
 22     else{
 23         perror("fork");
 24     }
 25     return 0;
 26 }                                                                            

注意:虽然父子进程代码共享,但fork之后才有子进程,所以子进程是执行fork之后的代码。

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

1.为什么fork有两个返回值?
2.一个变量里面,怎么会有两个不同的值,从而让父子进入不同的业务逻辑。
> fork后父进程返回时,本质是把返回值写入变量pid,而此时子进程已经创建好了,必定发生了写时拷贝。
所以这一个变量名,内容是不同的,而本质是父子页表映射数据到了不同的内存区域。所以接下来父子进程读取pid拿到的值就不一样。

Linux-进程控制_第1张图片

写时拷贝

通常,父子代码共享,父子在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
写时拷贝的过程实现是由OS参与完成的。
Linux-进程控制_第2张图片

为什么要有写时拷贝(数据的)?
保证父子进程的“独立性”
1.节省资源。父子进程创建时,拷贝不需要写入修改的数据(只读)是没有意义的,如果直接把数据各自拷贝一份,就浪费了内存和系统资源。
2.提高fork创建的效率。fork时创建数据结构,如果还要将数据拷贝一份,fork效率降低
3.减少fork失败的概率。fork本身就是向系统要更多的资源,而要越多的资源就越容易导致fork失败。

进程终止

进程退出的情况分类:

1.代码跑完,结果正确。退出码:0
2.代码跑完,结果不正确。逻辑问题,但是没有导致程序崩溃。退出码:!0
3.代码没有运行完毕,程序崩溃了,退出码没有意义。

进程常见退出方法:

正常终止(可以通过echo $?查看进程退出码):
1.main函数return
2.任何函数exit
异常退出:
ctrl+c,信号终止

main函数中,return的值(退出码)代表进程退出,结果是否运行正确。0代表成功。而return的0是给系统看的,以此确认进程执行结果是否正确。如果我们想看最近一次执行的一个程序运行结束时的退出码,可以用echo $?来查看
Linux-进程控制_第3张图片
退出码:可以认为定义,也可以使用系统的错误码list
当程序运行失败时,最关心的是失败的原因。而计算机擅长处理整数类型的数据(0, 1, 2, 3…)。 int(整数)-> string(错误码描述)
Linux-进程控制_第4张图片
父进程一般需要知道子进程退出的结果,即进程的退出码。但父进程也可以不关心子进程的运行结果。
进程非正常结束:野指针、/0、越界等,此时退出码无意义。(此时是由信号来终止的)

main函数return。非main函数的return不是终止进程,而是结束函数。例如:

int show()
{
	return 0;
}

int main()
{
	show();
	return 0;
}

这里main函数中调用完show,这个进程并不会终止。

exit:在任何函数中exit都表示直接终止进程
Linux-进程控制_第5张图片Linux-进程控制_第6张图片exit:在退出时会执行用户定义的资源清理函数,包括刷新缓冲区,关闭流等。
_exit:在退出时不会进行后续资源处理,直接终止进程。
Linux-进程控制_第7张图片可以看到使用_exit时,退出码照样是11。

站在OS角度,如何理解进程终止?
核心思想:归还资源
1."释放"曾经为了管理进程所维护的所有的数据结构对象。
2."释放"程序代码和数据占用的内存空间。
3.取消曾经该进程的链接关系。

释放:不是真的把数据结构对象销毁,而是设置为不用状态,然后保存起来。如果不用的对象多了,就有了一个"数据结构的池"。
内存池:先申请分配一定大小的空间,在需要使用时再使用内存池中的空间,就不需要每次需要内存时都进行new/malloc申请空间,提高了用户的效率。
释放数据结构对象:当要创建进程时,需要将内存池中拿出一块空间,并将这块空间强转成task_struct*,再进行访问。但如果每次都要强转就太麻烦。当一个pcb没人用时,可以将该pcb取出并链接到数据结构池中,该过程就是释放不用的数据结构对象,而需要用时再从池中取出,就不用进行强转了。这种释放规则叫做Slab分派器。
释放代码:不是将代码和数据结构清空,而是把内存设置为无效即可。
例如我们在下载电影资源时,所需下载拷进电脑的时间很多,删除却很快,说明写入和删的逻辑是不同的。写入时需要开辟空间,而删的本质是标识数据对应在磁盘上无效,一旦标识无效即意味着可以被覆盖,在写入新数据时,将该无效数据被覆盖也就是被清除了。
Linux-进程控制_第8张图片

进程等待

如果一个父进程终止了,内核会安排init进程成为它的孤儿进程的养父。init进程的PID为1,是在系统启动时由内核创建的,它不会终止,是所有进程的祖先。如果父进程没有回收它的僵死子进程就终止了,那么内核会安排init进程去回收它们。不过,长时间运行的程序,比如shell或者服务器,总是应该回收它们的僵死子进程。即使僵死子进程没有运行,它们依然消耗系统的内存资源。
一个进程可以通过调用waitpid函数来等待它的子进程终止或停止。

等待的必要性:

  1. 回收僵尸,解决内存泄漏。僵尸状态无法被杀死
  2. 父进程需要获取子进程的运行结束状态(不是必须的)
  3. 父进程要尽量晚于子进程退出,可以规范化进行资源回收。(编码相关)

进程等待的方法:
wait/waitpid

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

**wait:**等待任意一个子进程。当子进程退出,wait就可以返回。

#include 
#include 

pid_t wait(int* status);

返回值:成功则返回被等待进程pid,失败返回-1
参数:输出型参数,获取子进程退出状态,不关心则可以设置为NULL
  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <sys/types.h>                                                                                                                               
  5 #include <sys/wait.h>
  6 
  7 int main()
  8 {
  9     pid_t id = fork();
 10     if(id < 0){
 11         perror("fork");
 12         return 1;//自定义
 13     }
 14     else if(id == 0){
 15         //child
 16         int count = 5;
 17         while(count){
 18             printf("child is running: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
 19             sleep(1);
 20         }
 21         printf("child quit...\n");
 22         exit(0);
 23     }
 24     else{
 25         printf("father is waiting...\n");
 26         pid_t ret = wait(NULL);
 27         printf("father is wait done, ret: %d\n", ret);                                                                                               
 28     }
 29 }

Linux-进程控制_第9张图片

  int main()
  {
      pid_t id = fork();
      if(id < 0){
          perror("fork");
          return 1;//自定义
      }
      else if(id == 0){
          //child
          int count = 5;
          while(count){
              printf("child is running: %d, ppid: %d,pid:%d\n", count-    -, getppid(), getpid());
              sleep(1);
          }
          printf("child quit...\n");
          exit(0);
      }
      else{
          printf("father is waiting...\n");
          sleep(10);
          pid_t ret = wait(NULL);
          printf("father is wait done, ret: %d\n", ret);
          sleep(3);
          printf("father quit...\n");
      }
  
      return 0;
  }                                                       

可以看到,5s后子进程变为僵死状态,再过5s后子进程被回收。
Linux-进程控制_第10张图片
一般而言,我们需要fork之后,让父进程等待。

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。

默认options设置为0,是阻塞式等待。

  int main()
  {
      pid_t id = fork();
      if(id == 0){
          int count = 5;
          while(count){
              printf("child is runing: %d, ppid: %d, pid:%d\n", count--    , getppid(),getpid());                                             
              sleep(1);
          }
          printf("child quit...\n");
          exit(0);
      }
      //father
      sleep(8);
      pid_t ret = waitpid(id, NULL, 0);
      printf("father wait done, ret : %d\n", ret);
      sleep(3);
  }

Linux-进程控制_第11张图片
获取子进程status
wait和waitpid都有一个status参数,该参数是一个输出型参数,由操作系统填充。如果传递NULL,则表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的推出信息反馈给父进程。status不能简单地当作整型来看待,可以当作位图来看待。(之研究status低16bit位)
Linux-进程控制_第12张图片

正常终止:

int main()
{
    pid_t id = fork();
    if(id == 0){
        int count = 5;                                                                              
        while(count){
            printf("child is runing: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
            sleep(1);
        }
        printf("child quit...\n");
        exit(123);
    }
    //father
    int status = 0;
    pid_t ret = waitpid(-1, &status, 0);
    int code = (status >> 8) & 0xFF;
    printf("father wait done, ret : %d\n, exit code: %d\n", ret, code);
    if(code == 0){
        printf("漂亮,事情办成了!\n");
    } 
    else{
        printf("完了,需要重来了!\n");
    }
}   

Linux-进程控制_第13张图片
注意:不能定义全局变量code来拿到子进程的退出结果,因为父子进程是独立的。当写入变量时,会进行写时拷贝,此时父进程看不到该变量,也就无法取得子进程退出状态。

子进程虽然已经结束了,但子进程还是僵尸,子进程数据结构并没有完全被释放,当进程退出时,如task_struct里会被填上子进程退出时的退出码,所以waitpid拿到的status的值,是通过task_struct内部拿到的。

异常终止:
一般进程提前终止,本质是该进程收到了os发送的信号。
此时status的低7位标识当前进程退出时的终止信号。
信号是从1开始的,也就是说如果检测到低7位全是0,那就是正常终止,此时的退出状态才有意义。

	  int main()
    8 {
    9     pid_t id = fork();
   10     if(id == 0){
   11         int count = 5;
   12         while(count){
   13             printf("child is runing: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
   14             sleep(1);
E> 15             int *p = 0x12345;
   16             *p = 100;                                                                               
   17         }
   18         printf("child quit...\n");
   19         exit(10);
   20     }
   21     //father
   22     int status = 0;
   23     pid_t ret = waitpid(-1, &status, 0);
   24     int code = (status >> 8) & 0xFF;
   25     int sig = status & 0x7F; //0111 1111
   26     printf("father wait done, ret : %d\n, exit code: %d, sig: %d\n", ret, code, sig);
      }

野指针操作,程序崩溃,sigle不为0,父进程得知子进程异常终止。
在这里插入图片描述
Linux-进程控制_第14张图片
完整的等待过程:

  7 int main()  
  8 {  
  9     pid_t id = fork();  
 10     if(id == 0){  
 11         int count = 5;  
 12         while(count){  
 13             printf("child is runing: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());  
 14             sleep(1);
 15         }
 16         printf("child quit...\n");
 17         exit(10);
 18     }
 19     int status = 0;
 20     pid_t ret = waitpid(id, &status, 0);
 21     if(ret > 0){
 22         printf("wait success!\n");                                                                    
 23         if((status & 0x7F) == 0){
 24             printf("process quit normal!\n");
 25             printf("exit code: %d\n", (status>>8)&0xFF);
 26         }
 27         else{
 28             printf("process quit error!\n");
 29             printf("sig: %d\n", status&0x7F);
 30         }                                                                                             
 31     }
	}

Linux-进程控制_第15张图片
系统提供了一堆的宏(函数),可以用来判断退出码,退出状态。
Linux-进程控制_第16张图片

  7 int main()
  8 {
  9     pid_t id = fork();
 10     if(id == 0){
 11         int count = 5;
 12         while(count){
 13             printf("child is runing: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
 14             sleep(1);
 15         }
 16         printf("child quit...\n");
 17         exit(10);
 18     }
 19     int status = 0;                                                                                   
 20     pid_t ret = waitpid(id, &status, 0);
 21     if(ret > 0){
 22         printf("wait success!\n");
 23         if(WIFEXITED(status)){
 24             printf("process quit normal!\n");
 25             printf("exit code: %d\n", WEXITSTATUS(status));
 26         }
 27         else{
 28             printf("process quit error!\n");
 29         }
 30     }       
 	 }

Linux-进程控制_第17张图片
如果options传WNOHANG,等待方式为非阻塞,如果传0,默认是阻塞的。我们目前所调用的函数(都是单执行流,简单),都是阻塞函数。阻塞等待:调用方一直在等待,期间不做任何事。而非阻塞是在不断检测状态。
非阻塞轮询方案:父进程多次调用waitpid,检测子进程的运行状态,最终如果检测到了子进程的退出状态,waitpid才成功返回,而在此之前都是失败返回。
注意:waitpid的失败返回有两种意思:1、并不是真正失败,仅仅是对方的状态还没有达到预期(下次再检测)。2、真的失败了

阻塞等待中,是父进程在等待,子进程在跑代码。
“等”:将当前进程放入等待队列,并将进程状态设置为非R状态。
唤醒进程->等待队列->运行队列->R
当我们运行过多程序,计算机卡住时,有可能就是因为运行进程太多,导致OS把进程放入等待队列。

非阻塞等待:

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <sys/types.h>
  5 #include <sys/wait.h>
  6 
  7 int main()
  8 {
  9 
 10     pid_t id = fork();
 11     if(id == 0){
 12         int count = 3;                                                                                                                              
 13         while(count){
 14             printf("child is running: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
 15             sleep(1);                                
 16         }                               
 17         printf("child quit..\n");
 18         exit(10);                              
 19     }                                
 20     int status = 0;    
 21     while(1){                           
 22         pid_t ret = waitpid(id, &status, WNOHANG);
 23         if(ret == 0){
 24             printf("wait next!\n");                                
 25             printf("father do other thing!\n");                  
 26         }                                                 
 27         else if(ret > 0){             
 28             printf("wait success, ret: %d, code: %d\n", ret, WEXITSTATUS(status));
 29             break;                            
 30         }                                                                                                          
 31         else{         
 32             printf("wait failed\n");
 32             printf("wait failed\n");
 33             break;
 34         }
 35     }
 36 }  

进程程序替换

创建子进程的目的有:1.执行父进程的部分代码。2.执行其它的代码。
进行进程替换的目的就是让子进程执行其它程序的代码。

子进程不改变进程内核的数据结构,只修改部分的页表数据,然后将新程序的代码和数据加载到内存,重新构建映射关系,和父进程彻底脱离关系,就是进程替换。
在进行程序替换的时候,没有创建新的进程。子进程的pid没改变。
Linux-进程控制_第18张图片
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

如何进行进程替换?

替换函数:
Linux-进程控制_第19张图片
execl:
其中l代表list,列表

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

Linux-进程控制_第20张图片

  int main()
  {
      printf("my process begin\n");
      execl("/usr/bin/ls", "ls", "-a", "-l", "-i", NULL);
      printf("my process end!\n");                                                                      
  
      return 0;
  }

Linux-进程控制_第21张图片
exec* 程序替换,一旦替换完成,原程序后面的代码就不再执行,所以end也没有输出,返回值也没有意义。所以exec*函数不用考虑返回值,只要返回。
exec是特殊的加载器,当要运行软件时,可以直接将进程读取进内存。

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <sys/types.h>
  5 #include <sys/wait.h>
  6 
  7 int main()
  8 {
  9     pid_t id = fork();
 10     if(id == 0){
 11         //child
 12         printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());
 13         execl("/usr/bin/ls", "ls", "-l", NULL);
 14         exit(1);
 15     }
 16 
 17     int count = 3;
 18     while(count){
 19         printf("I am father, pid: %d\n", getpid());
 20         sleep(1);
 21         count--;                                                                                                                                    
 22     }
 23     int status = 0;
 24     //father
 25     pid_t ret = waitpid(id, &status, 0);
 26     if(ret > 0){
 27         printf("child status -> sig: %d, code: %d\n", status&0x7F, (status >> 8) & 0xFF);
 28     }
 29     else{
 30         printf("wait error!\n");
 31     }
 32     return 0;
	 }

这样让创建子进程后让子进程去执行新的程序(没有创建新进程),父进程,父进程得到结果,检测命令并回收子进程的退出信息
这里的退出码code是子进程的退出码,但是是执行完ls后的退出码。
Linux-进程控制_第22张图片

其它接口:

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

将child中代码改为:

char *const my_argv[]={                                                          
   "ls",                                                                        
   "-l",                                                                        
   "-a",                                                                        
   "-i",                                                                        
   NULL                                                                         
};                                                                               
execv("/usr/bin/ls", my_argv);  

结果同上

命名理解:

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

execlp:
含有p:可以自动搜索环境变量PATH(系统的命令才可以找到,或者把自己的命令导入到PATH中),不用写路径

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

例如:

execlp("ls", "ls", "-l", "-a", NULL);
execlp("top", "top", NULL);

execvp类似,只不过后面是用一个指针数组传参

execle:
e:传入默认的或者自定义的环境变量给目标可执行程序

int execle(const char *path, const char *arg, ...,char *const envp[]);
   20         char *const my_env[] = {                                                                  
E> 21             "MYENV=helloworld!",                                                                
   22             NULL                                                                                  
   23         };                                                                                        
   24         execle("./mycmd", "mycmd", NULL, my_env);                                                   
   25         exit(1);                                                                      
   26     }    

Linux-进程控制_第23张图片
在exec_cmd中调用execl传入环境变量MYENV的值,mycmd中接收并打印出。如果单独运mycmd依旧为空。
Linux-进程控制_第24张图片
exec_cmd能执行系统的命令,也可以执行自己写的命令。
如果想要跨语言之间耦合,如C语言想调C++的代码,就可以exec这样的程序替换。

execve:

 int execve(const char *path, char *const argv[], char *const envp[]);
   20         char *const my_argv[] = {                                                     
W> 21             "mycmd",                                                                  
   22             NULL                                                                      
   23         };                                                                            
   24         char *const my_env[] = {                                                      
W> 25             "MYENV=helloworld!",                                                      
   26             NULL                                                                      
   27         };                                                                            
   28         execve("./mycmd", my_argv, my_env);   

也可以将main函数中的环境变量参数env传入,但需要导出环境变量export MYENV=helloworld。main函数可以获得这个环境变量,并把这个环境变量导给子进程。

1.什么是程序替换:通过exec系列的函数,让特定进程去加载磁盘中的其它程序,以达到运行的目的,期间不创建新的进程。
2.为什么要程序替换:子进程执行新的程序的需求。
3.如何进行程序替换:原理->进程地址空间的问题->磁盘换入程序到内存->对可执行程序的理解(exe 文件) exec*
4.后续:a.exec*只要返回了,就说明出错了。b.各种借口的理解:l,v,p,e

简易shell

用户在命令行输入某些命令,交给shell解释器。shell解释器解释命令时,并不是自己解释,而是调用fork创建子进程,子进程再执行命令(其实就是OS执行命令),OS再把结果通过shell解释器返回给用户。而子进程是通过exec系列函数执行命令的。

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

#define NUM 128
#define SIZE 32

char command_line[NUM];
char *command_parse[SIZE];

int main()
{
    while(1){
        memset(command_line, '\0', sizeof(command_line));
        printf("[ymz@myhost 我的shell]$ ");
        fflush(stdout);
        //1. 数据读取
        if(fgets(command_line, NUM-1, stdin)){
            command_line[strlen(command_line) - 1] = '\0';
            //ls -a -l -i
            //2. 字符串(命令行数据分析)
            int index = 0;
            command_parse[index] = strtok(command_line, " ");
            while(1){
                index++;
                command_parse[index] = strtok(NULL, " ");
                if(command_parse[index] == NULL){
                    break;
                }
            }
            //3. 判断命令
            //a. 内置命令
            //b. 第三方命令
            if(strcmp(command_parse[0], "cd") == 0 && chdir(command_parse[1]) == 0){
                continue;
            }
            //4. 执行非内置命令
            if(fork() == 0){
                //子进程
                execvp(command_parse[0], command_parse);
                exit(1);
            }
            int status = 0;
            pid_t ret = waitpid(-1, &status, 0);
            if(ret > 0 && WIFEXITED(status)){
                printf("Exit Code: %d\n", WEXITSTATUS(status));
            }
        }
        
    }
    return 0;
}

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