Linux进程控制(详解)

目录

一.进程创建

1.fork函数

2.fork返回值

3.写时拷贝

4.fork常规用法

5.fork调用失败的原因

二.进程终止

1.进程退出场景

2.进程退出码

3.进程正确退出方法

4.进程异常退出 

三.进程等待

1.进程等待的必要性 

2.获取子进程的status 

3.wait方法 

4.waitpid方法

四.进程替换

1.替换原理

2.替换函数 

3.函数解释

4.命名理解                                        

五.一个简易的的shell程序

六.思考函数和进程之间的相似性 

exec/exit就像call/return



一.进程创建

1.fork函数

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

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

 注意: fork之后,父进程和子进程谁先执行完全由调度器决定Linux进程控制(详解)_第1张图片

                                

2.fork返回值

子进程返回0, 父进程返回的是子进程的pid。
(1)为何给子进程返回0,父进程返回子进程的pid

 父:子 = 1:n ; 一个父进程可以创建多个子进程,而一个子进程只能有一个父进程。因此,对于子进程来说,父进程是不需要被标识的;而对于父进程来说,子进程是需要被标识的,因为父进程创建子进程的目的是让其执行任务的,父进程只有知道了子进程的PID才能很好的对该子进程指派任务。 

                                         

(2)为什么fork有两个返回值? 

Linux进程控制(详解)_第2张图片

fork函数内部执行return语句之前,子进程就已经创建完毕了,父子进程都需要return返回。

                                                                        

3.写时拷贝

数据是很多的,不是所有的数据立马被使用,且不是所有的数据都要进行拷贝。但是如果立马独立,就需要将数据全部拷贝,把本来可以在后面拷贝的,甚至不需要拷贝的数据,都拷贝了,就比较浪费时间和空间,所以拷贝的过程不是创建进程时立马做的。

Linux进程控制(详解)_第3张图片
 

(1) 为什么父进程创建子进程时父进程的数据段是只读的而不是可读的?
一旦父进程发现要创建子进程就将可读可写状态设置为只读,将来不管是父进程还是子进程一旦想往数据段写的时候立马会报错OS会识别到,识别到之后OS根据现在的创建完子进程要进行数据写入时,OS立马处理这个错误时就按照写时拷贝的方式处理,处理完毕之后,数据段的只读属性就被去掉了。

                         

(2)为什么要进行写时拷贝? (写时拷贝是OS的内存管理,自动完成)

 进程具有独立性(进程各自的PCB,地址空间,页表)。多个进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程。

                                                 

(3)为什么不在创建子进程的时候就进行数据的拷贝?

 子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间

                         

(4)子进程创建出来的时候会不会立马被调度?

不一定,所以OS在一定时间段内(子进程被创建到被调度),系统可用的内存变少了

(5)代码会不会进行写时拷贝?

90%的情况不会,但是不代表不能,(进程替换时代码写时拷贝)
                         

(6)OS是很节省空间的

Linux进程控制(详解)_第4张图片

                                        

4.fork常规用法

(1)一个进程希望复制自己,使子进程同时执行不同的代码段。例如父进程等待客户端请求,生成子进程来处理请求。

(2)一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数,进程替换。

                                        

5.fork调用失败的原因

(1)系统中有太多的进程,内存空间不足,子进程创建失败。

(2) 实际用户的进程数超过了限制,子进程创建失败。

                        

                

二.进程终止

1.进程退出场景

(1)代码运行完毕,结果正确。

(2)代码运行完毕,结果不正确。

(3)代码异常终止(进程崩溃)。

                                         

2.进程退出码

(1)main函数的退出的时候,return 数字叫做进程的退出码!

[gsx@VM-0-2-centos temp]$ echo $? //查看最近一次进程退出的退出码

Linux进程控制(详解)_第5张图片

                                

(2) 程序运行时main函数是入口(用户级别), main函数也是函数,它也是被调用的,被谁调用的?  OS直接/间接通过系统调用接口调用main函数,返回值最终会返回给OS

                                                 

(3)为什么必须给main函数设置返回值?

程序运行起来,加载内存,形成进程;目的: 完成某种工作,工作是由人发起的,需要知道工作完成的如何,结果怎样。

                                                 

(4)为什么main的return一般写成0  ?

0在函数设计中,一般代表正确非零错误,!0代表失败,每一个数字代表一种失败原因。

Linux进程控制(详解)_第6张图片

 注意: 退出码都有对应的字符串含义,帮助用户确认执行失败的原因,而这些退出码具体代表什么含义是人为规定的,不同环境下相同的退出码的字符串含义可能不同。 

                        

3.进程正确退出方法

(1)从main函数返回,return

只有main函数中的return代表进程退出

                                 

(2)exit函数

exit函数可以在代码中的任何地方退出进程,并且exit函数在退出进程前会做一系列工作:

  1. 执行用户通过atexit或on_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入。
  3. 调用_exit函数终止进程

Linux进程控制(详解)_第7张图片

                                        

(3)_exit函数 

_exit函数退出进程的方法并不经常使用,_exit函数也可以在代码中的任何地方退出进程,但是_exit函数会直接终止进程,并不会在退出进程前会做任何收尾工作

 Linux进程控制(详解)_第8张图片

                                                        

(4)return、exit和_exit之间的区别与联系 

①区别

只有main函数当中的return才退出进程,其他函数中return叫终止函数,而exit函数和_exit函数在代码中的任何地方使用都可以起到退出进程的作用。

exit会释放进程曾经占用的资源,比如:缓冲区;_exit直接终止进程,不会做任何收尾工作!


Linux进程控制(详解)_第9张图片

                        

②联系 

 执行return num等同于执行exit(num),因为调用main函数运行结束后,会将main函数的返回值当做exit的参数来调用exit函数,使用exit函数退出进程前,exit函数会先执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再调用_exit函数终止进程

                                                

(5)进程终止了,操作系统做了什么?

释放曾经申请的数据结构,释放曾经申请的内存,从各种队列等数据结构中移除
 

                                        

4.进程异常退出 

(1)向进发送信号导致进程异常退出。

(2)代码错误导致进程运行时异常退出(野指针访问,除0错误: 本质还是发送信号杀掉进程)

Linux进程控制(详解)_第10张图片

 进程异常退出了,退出码还有意义吗?  没有任何意义,最大的意义是为什么异常了。
 

                                

                                

三.进程等待

1.进程等待的必要性 

(1)子进程退出,父进程如果不读取子进程的退出信息,子进程就会变成僵尸进程,进而造成内存泄漏。
(2)进程一旦变成僵尸进程,那么就算是kill -9命令也无法将其杀死,因为谁也无法杀死一个已经死去的进程。
(3)对于一个进程来说,最关心自己的就是其父进程,因为父进程需要知道自己派给子进程的任务完成的如何。
(4)父进程需要通过进程等待的方式,回收子进程资源,获取子进程的退出信息。
                        

2.获取子进程的status 

(1)wait和waitpid函数,都有一个status参数,该参数是一个输出型参数,由OS填充。
(2)如果status参数传递NULL,表示不关心子进程的退出状态信息。
(3)否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
(4)status不能简单的当作整形来看待,可以当作位图来看待(只研究status低16比特位)

Linux进程控制(详解)_第11张图片

在status的低16比特位当中,如果进程正常终止,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志 

                                        

(5)根据status获取进程的退出码/终止信号

①位操作 

exitcode = (status >> 8) & 0xFF; //退出码
exitsignal = status & 0x7F;      //终止信号

②OS的两个宏来获取退出码和退出信号。 

  • WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号
  • WEXITSTATUS(status):用于获取进程的退出码。

                                                                

3.wait方法 

函数:        pid_t  wait(int*status);
返回值:成功返回被等待进程pid ,失败返回 -1
参数:    输出型参数,获取子进程退出状态, 不关心则可以设置成为 NUL
特点:       等待任意子进程
#include
#include
#include
#include
#include

int main()
{

 //wait测试代码 
   pid_t id = fork();
   if(id == 0){ //child
     int count =0 ;
     while(count < 5){
       printf("I am child , pid:%d  ppid:%d \n",getpid(), getppid());
       count++;
       sleep(1);
     }
     exit(10);
   }
   else{ //father
       printf("I am father , pid:%d  ppid:%d \n",getpid(), getppid());

       int status = 0;
       pid_t ret = wait(&status);
       if(ret >= 0){
         printf("wait success!,%d\n",ret);
         
         printf("位操作:\n");
         printf("exit code:%d\n" ,(status>>8)&0xff);
         printf("exit singal:%d\n" , status&0x7f);

         printf("通过宏判断:\n");
         if(WIFEXITED(status)){ //exit normal
           printf("exit code:%d \n" , WEXITSTATUS(status));
         }
       }
      
       printf("Father run done ...\n");
       sleep(5);
   }

  return 0;
}

Linux进程控制(详解)_第12张图片                                 

 使用监控脚本对进程进行实时监控,查看进程的状态和退出情况:

[gsx@VM-0-2-centos temp]$ while :; do ps axj | head -1 && ps axj | grep myproc | grep -v grep;echo "--------------------";sleep 1;done 

Linux进程控制(详解)_第13张图片

当子进程退出后,父进程读取了子进程的退出信息,子进程也就不会变成僵尸进程了。 

                                

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: 输出型参数
③options : 当设置为 WNOHANG pid 指定的子进程没有结束,则 waitpid() 函数返回 0 ,不予以等待。若正常结束,则返回该子进程的ID
特点:可以等待指定进程

(1)单进程阻塞式等待

  1 #include                                                                         
  2 #include
  3 #include
  4 #include
  5 #include
  6 
  7 
  8 int main()
  9 { 
 10     pid_t id = fork();
 11     if (id == 0){ //child          
 12       int count = 5;
 13       while (count--){
 14         printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());
 15         sleep(1);
 16       }
 17 
 18       exit(10);
 19     }
 20 
 21     //father           
 22     int status = 0;
 23     pid_t ret = waitpid(id, &status, 0);
 24     if (ret >= 0){
 25       //wait success                                                                      
 26       printf("wait success...\n");
 27       if (WIFEXITED(status)){
 28         //exit normal                                 
 29         printf("exit code:%d\n", WEXITSTATUS(status));
 30       }
 31       else{
 32         //signal killed                              
 33         printf(" siganl %d\n", status & 0x7F);
 34       }
 35     }
 36 
 37     printf("father done ...\n");
 38     sleep(3);
 39 
 40     return 0;
 41 }

 Linux进程控制(详解)_第14张图片

                                 

(2)单进程非阻塞轮询式等待

①当子进程未退出时,父进程都在一直等待子进程退出,在等待期间,父进程不能做任何事情,这种等待叫做阻塞等待。

②子进程未退出时父进程可以做一些自己的事情,当子进程退出时再读取子进程的退出信息,即非阻塞等待;通过传递参数WNOHANG我们轮询式检测子进程是否退出,没有退出就做自己的事情。

③父进程每隔一段时间就去查看子进程是否退出,若未退出,则父进程去忙自己的事情,过一段时间再来查看,直到子进程退出后读取子进程的退出信息。

  1 #include 
  2 #include 
  3 #include 
  4 #include 
  5 #include 
  6 
  7 int main()
  8 {
  9     pid_t id = fork();
 10     if (id == 0){ //child
 11       int count = 3;                                                                      
 12       while (count--){
 13         printf("child do something...PID:%d, PPID:%d\n", getpid(), getppid());
 14         sleep(3);
 15       }
 16 
 17       exit(10);
 18     }
 19 
 20     //father   轮询检测
 21     while (1){
 22       int status = 0;
 23       pid_t ret = waitpid(id, &status, WNOHANG);
 24       if (ret > 0){
 25         printf("wait child success...\n");
 26         printf("exit code:%d\n", WEXITSTATUS(status));
 27         break;
 28       }
 29       else if (ret == 0){
 30         printf("father do other things...\n");
 31         sleep(1);
 32       }                                                                                   
 33       else{
 34         printf("waitpid error...\n");
 35         break;
 36       }
 37     }
 38 
 39     return 0;
 40 }

Linux进程控制(详解)_第15张图片

(3)多个子进程等待 

 先创建多个子进程,让子进程做自己的事,当子进程退出时 , 父进程通过子进程的pid依次获取退出信息,回收资源。

  1 #include 
  2 #include 
  3 #include 
  4 #include 
  5 #include 
  6 
  7 int main()
  8 {
  9     //waitpid 多进程 
 10     pid_t  ids[10];
 11     for(int i = 0; i < 10; i++){
 12         pid_t id = fork();
 13         if(id == 0 ){ //child
 14             int count = 5;                                                                
 15             while(count > 0){
 16                 printf("child %d do something!: %d, %d\n",i, getpid(), getppid());
 17                 sleep(1);
 18                 count--;
 19             }
 20             exit(i);
 21         }
 22   
 23         //father
 24         ids[i] = id;
 25     }
 26   
 27     int count = 0;
 28     while(count < 10){
 29         int status = 0;
 30         pid_t ret = waitpid(ids[count], &status, 0);
 31         if(ret >= 0){
 32             printf("wait child success!, %d\n", ret);                                     
 33             if(WIFEXITED(status)){ //这种方式是推荐的
 34                 printf("child exit code : %d\n",WEXITSTATUS(status));
 35             }
 36             else{                                                                       
 37                 printf("child not exit normal!\n");
 38             }
 39 
 40            // printf("status: %d\n", status);
 41            // printf("child get signal: %d\n", status&0x7F);
 42            // printf("child exit code : %d\n", (status>>8)&0xFF);
 43         }
 44         count++;
 45     }
 46 
 47   return 0;
 48 }

Linux进程控制(详解)_第16张图片

                

                        

                         

四.进程替换

1.替换原理

fork 创建子进程后执行的是和父进程相同的程序 ( 但有可能执行不同的代码分支 ), 子进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种exec 函数时 , 该进程的用户空间代码和数据完全被新程序替换 , 从新程序的启动例程开始执行

(1)进程程序替换时,有没有创建新的进程? 没有!

进程程序替换之后,该进程对应的PCB、进程地址空间以及页表等数据结构都没有发生改变,只是进程在物理内存当中的数据和代码发生了改变,所以并没有创建新的进程,而且进程程序替换前后该进程的pid并没有改变。

                                        

(2)子进程进行进程程序替换后,会影响父进程的代码和数据吗?

子进程刚被创建时,与父进程共享代码和数据,但当子进程需要进行进程程序替换时,也就意味着子进程需要对其数据和代码进行写入操作,这时便需要将父子进程共享的代码和数据进行写时拷贝,此后父子进程的代码和数据也就分离了,因此子进程进行程序替换后不会影响父进程的代码和数据。

Linux进程控制(详解)_第17张图片
 

                        

2.替换函数 

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

第一个参数是你要执行程序的路径 ;第二个参数是你要怎么执行,以NULL结尾表示你已经把想要传的参数传完了。

示例: execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);

Linux进程控制(详解)_第18张图片                         

(2) int execlp(const char *file   ,  const char *arg  ,   ...);
第一个参数是要执行程序的名字,第二个参数是可变参数列表,以NULL结尾。
示例: execlp("ls", "ls", "-a", "-i", "-l", NULL);

                                 

(3)int execle(const char *path , const char *arg ,   ...  ,  char *const envp[]);
第一个参数是要执行程序的路径,第二个参数是可变参数列表,第三个参数是你自己设置的环境变量。

 代码测试:

Linux进程控制(详解)_第19张图片

 Linux进程控制(详解)_第20张图片

结果: 

 Linux进程控制(详解)_第21张图片

① 这里我们在test.c传递的环境变量通过execle函数到达mytest.c ,其实如果我们不传环境变量,OS默认会传给mytest.c系统的环境变量,我们传了,OS就不传了。

②Makefile一次生成多个可执行程序 

Linux进程控制(详解)_第22张图片

(4) int execv(const char *path  , char *const argv[]  );
第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾.
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execv("/usr/bin/ls", myargv);

                                            

(5)int execvp(const char *file  ,  char* const argv[]);
第一个参数是要执行程序的名字,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾。
char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execvp("ls", myargv);
(6) int execve(const char *path  , char *const argv[]  , char *const envp[]);
第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾,第三个参数是你自己设置的环境变量。
char* myargv[] = { "mytest", NULL };
char* myenvp[] = { "MYVAL=100", NULL };
execve("./mytest", myargv, myenvp);

                        

3.函数解释

  • 这些函数如果调用成功,则加载指定的程序并从启动代码开始执行,不再返回。
  • 如果调用出错,则返回-1。

(1)exec系列函数只要返回了,就意味着调用失败。

(2)使用的execl就可以称之为:linux下的加载器所采用的的底层技术,execl本质就是把一个程序run起来
 

                                

4.命名理解

这六个exec系列函数的函数名都以exec开头,其后缀的含义如下:

l(list):表示参数采用列表的形式列出。
v(vector):表示参数采用数组的形式。
p(path):表示能自动搜索环境变量PATH,进行程序查找。
e(env):表示可以传入自己设置的环境变量

事实上,只有execve才是真正的系统调用,其它五个函数最终都是调用的execve,所以execve在man手册的第2节,而其它五个函数在man手册的第3节,也就是说其他五个函数实际上是对系统调用execve进行了封装,以满足不同用户的不同调用场景的。

下图为exec系列函数族之间的关系:
Linux进程控制(详解)_第23张图片

                                        

五.一个简易的的shell程序

 shell也就是命令行解释器,其运行原理就是:当有命令需要执行时,shell创建子进程,让子进程执行命令,而shell只需等待子进程退出即可

 Linux进程控制(详解)_第24张图片

shell执行的步骤: 

  1. 获取命令行。
  2. 解析命令行。
  3. 创建子进程。
  4. 替换子进程。
  5. 等待子进程退出。
    1 #include                                                                       
    2 #include 
    3 #include 
    4 #include 
    5 #include 
    6 #include 
    7 #include 
    8 
    9 #define LEN 1024 //输入命令最大长度
   10 #define NUM 32 //命令拆分后的最大个数
   11 
   12 int main()
   13 {
   14   char cmd[LEN]; //存储命令
   15   char* myargv[NUM]; //存储命令拆分后的结果
   16   char hostname[32]; //主机名
   17   char pwd[128]; //当前目录
   18 
   19 
   20   while (1){
   21     //获取命令提示信息
   22     struct passwd* pass = getpwuid(getuid());
   23     gethostname(hostname, sizeof(hostname)-1); //获取主机名
   24     getcwd(pwd, sizeof(pwd)-1);//获取绝对地址
   25     int len = strlen(pwd);
   26     char* p = pwd + len - 1; //获取当前目录
   27     while (*p != '/'){
   28       p--;
   29     }
   30     p++;
   31 
   32     //打印命令提示信息                                                                  
   33     printf("[%s@%s %s]$ ", pass->pw_name, hostname, p);
   34     
   35     //从标准输入读取命令
   36     fgets(cmd, LEN, stdin);
   37     cmd[strlen(cmd) - 1] = '\0';
   38 
   39     //拆分命令
   40     myargv[0] = strtok(cmd, " ");
   41     int i = 1;
   42     while (myargv[i] = strtok(NULL, " ")){                                              
   43       i++;
   44     }
   45 
   46     pid_t id = fork(); //创建子进程执行命令
   47     if (id == 0){
   48       //child
   49       execvp(myargv[0], myargv); //进行程序替换
   50       exit(1);
   51     }
   52 
   53     //父进程等待获取子进程的退出信息
   54     int status = 0;
   55     pid_t ret = waitpid(id, &status, 0);
   56     if (ret > 0){
   57       printf("exit code:%d\n", WEXITSTATUS(status)); 
   58     }
   59   }
   60   return 0;
   61 }
                

运行结果: 

Linux进程控制(详解)_第25张图片

六.思考函数和进程之间的相似性 

exec/exit就像call/return

(1) 一个 C 程序由很多函数组成,一个函数可以调用另外一个函数,同时传递给它一些参数
(2) 被调用的函数执行一定的操作,然后返回一个值
(3) 每个函数都有他的局部变量
(4) 不同的函数通过call/return 系统进行通信。
这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础 。Linux 鼓励将这种应用于程 序之内的模式扩展到程序之间。

 Linux进程控制(详解)_第26张图片

(5)一个程序可以fork创建子进程,然后使用exec系列函数将子进程的代码和数据替换为另一个程序的代码和数据,之后子进程就用该程序的数据执行该程序的代码,从而达到程序之间相互调用的效果
                                                
(6)这个被调用的程序执行一定的操作,然后通过exit(n) 来返回;调用它的进程可以通过wait/waitpid 来获取 exit 的返回值,通过返回值判断程序执行的怎么样。

                         

(7)进程替换可以执行不同语言的代码

Linux进程控制(详解)_第27张图片

                         

 ①shell脚本程序

Linux进程控制(详解)_第28张图片

                                 

 ②C++程序,经过编译形成可执行程序

Linux进程控制(详解)_第29张图片

                                 

③python程序 

Linux进程控制(详解)_第30张图片

你可能感兴趣的:(Linux系统,linux,服务器,进程)