Linux]——进程控制

Posted 努力学习的少年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux]——进程控制相关的知识,希望对你有一定的参考价值。

1.fork函数

fork函数是非常重要的函数,它能从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

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

 当进程调用fork时,当控制会转移到内核中的fork代码中后,内核中的fork中实现细节:

 也就是说在fork中return的前面,子进程就已经创建完成了,所以到return那里,子进程和父进程分别返回不同的pid(因为父进程跟子进程的代码是共享的,所以它们会执行相同的代码)。 

所以, fork 之前父进程独立执行, fork 之后,父子两个执行流分别执行。注意, fork之后,谁先执行完全由调度器决定。

 

写时拷贝

概念:

当父进程创建子进程的时候,系统不会立马给子进程创建一块空间,而是让子进程中的数据先指向父进程的数据。当父子进程中有一个要进行写入数据的时候,才创建一定的空间给那个要写入数据的进程,这就是写时拷贝。

父进程创建一个子进程时候:

1.系统会创建相对应的task_struct(进程控制块),mm_struct(进程地址空间),页表等数据结构给子进程。

2.然后父进程会将task_struct(进程控制块),mm_struct(进程地址空间),页表中的大部分数据拷贝给子进程中相对应的task_struct(进程控制块),mm_struct(进程地址空间),页表,也就是说,刚创建子进程的时候,子进程中的数据和代码与父进程都是共享的。包括·数据的虚拟地址都是一样的。所谓的共享就是在虚拟内存中映射到物理内存是相同的一块空间。如下图所示:    

 

  1#include<stdio.h>
  2 #include<sys/types.h>
  3 #include<unistd.h>
  4 #include<stdlib.h>
  5 
  6 int main()
  7 {
  8   int a=10;
  9   pid_t id=fork();
 10   if(id==0)
 11   {
 12     //child
 13    // a=1000;                                                                                                                           
 14     printf("child: a: %d &a: %p\\n",a,&a);
 15     sleep(1);
 16     exit(1);
 17   }
 18   else{
 19     printf("parent: a: %d &a: %p\\n",a,&a);
 20     sleep(1);
 21   }
 22
 23   return 0;
 24 }

 创建的时候,如果不对数据进行修改,子进程和父进程的的数据是一样的,其中a的地址也是一样,这是虚拟地址,是父进程拷贝给子进程的。

3.当父子进程之间其中有一个要对数据段进行写入的时候,系统会开辟一块的物理空间大小给要写入的进程,然后修改页表指向的物理空间,这就是写时拷贝。例如父子进程的a的值指向的同一块物理空间,当子进程要去修改a时,系统会在物理内存中创建一块跟a一样大小的空间给a,然后将子进程的页表指向新的物理空间,然子进程去进行修改。

 我们在将上面的代码在修改一下,在子进程的分流那里,将a改为1000。

  1#include<stdio.h>
  2 #include<sys/types.h>
  3 #include<unistd.h>
  4 #include<stdlib.h>
  5 
  6 int main()
  7 {
  8   int a=10;
  9   pid_t id=fork();
 10   if(id==0)
 11   {
 12     //child
 13     a=1000;                                                                                                                             
 14     printf("child: a: %d &a: %p\\n",a,&a);
 15     sleep(1);          
 16     exit(1);           
 17   }                    
 18   else{                
 19     printf("parent: a: %d &a: %p\\n",a,&a);
 20     sleep(1);          
 21   }                    
 22                        
 23   return 0;            
 24 } 

 我们看到父进程的a是10,子进程的a是1000.此时就发生了写时拷贝。同时它们的的地址都是相同的,这是虚拟地址,虚拟地址映射到物理地址是不相同的。

 

为什么要有写时拷贝

1.进程具有独立性,父子进程也是,当父子进程其中一个进程要写入数据的时候,是不会影响到另一个进程。

2.当子进程创建出来的时候不会立马分配空间给子进程,这可以节约系统内存的空间,因为子进程创建出来的时候,可能不会立马被cpu给调度,所以系统可以合理去利用这些空间去做更有意义的事情,当子进程需要多少空间的时候,才分配多少空间给其中一个子进程

进程退出码

进程退出的场景:

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

2.代码运行完毕,结果不正确,例如:进程申请空间失败,或者打开文件失败,创建子进程失败等原因导致运行结果错误。

在上面中退出的场景中,我们一般不关心代码运行完毕,结果正确。因为运行成功则只有一种情况。而代码运行结束,结果不正确,我们往往是最关心的,因为我们得知道运行结束时,结果不正确的原因有很多种,我们得知道具体失败的原因,好让我们去修改代码。

当运行完一个程序的时候,我们要查看该程序是否运行正确,我们可以通过查看进程的退出码去判断运行该程序是否正确。如果正确,那么会返回一个我们在main函数中的return的值,或者exit中的值。如果程序运行的不正确,那么可以根据退出码去判断错误的原因。

下面我们将程序运行成功时的进程退出码设置为1:main函数return 后面就是进程退出码

 int main()
 {
   printf("hello Linux\\n");
  return 1;                                                                                                                             
 }

运行结束时,在查看进程退出码。 

 echo $?:显示最近的进程退出码 

如果进程失败的话,那么它会进程退出码将不是return返回的值,而是其他非0的数字,不同非0的数字代表的是程序失败的不同的原因。

我们来看一下c语言的默认的程序退出码代表的是什么含义。

下面这段代码可以打印c语言程序的进程退出码中的数字代表的是什么含义:

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

 在c语言中的进程退出码有134个,我只是截取了其中的一部分,其中0默认程序运行成功,其他数字代表的是程序运行结果失败的原因。例如;5代表的是输入输出错误。12代表的是malloc空间失败等等。我们都可以通过不同的进程退出码去确定运行结果不正确的原因。

 进程除了上面那两种情况外,还可能

程序异常退出

进程代码还么没跑完就提前退出了,这种情况叫异常退出

例如,ctrl+c,或者指针越界都会导致程序提前退出。

提前退出的的进程退出码我们不关心。我们需要关心的是进程收到什么信号。

kill -l可以查看各种退出信号。

进程退出的方法

正常退出

1.main函数的return

int main
{
     printf("hello Linux\\n");                  
    return 0;                                                                                                  
     printf("hello world\\n");                                                                                                            
 }

 当进程遇到在main函数的时候遇到return的时候,进程就一样退出了,不会在执行以下代码,所以hello world不会被打印出来,如果进程没有遇到错误,则返回的是进程后面的值。

2.调用_exit

void test()
 {          
   printf("hello test\\n");                                                          
  _exit(1);                                           
 }     

int main()
{
   test();
    printf("hello linux\\n");
   return 10;
}

 当进程遇到_exit时就退出,如果运行成功,则返回括号里面的退出码。并且不论是在哪里,则进程都会退出,而return只在main函数里面才会退出。

        

3.调用exit.

exit跟_exit一样,进程不论在哪个位置遇到,则直接退出进程。

exit 最后也会调用 exit, 但在调用 exit 之前,还做了其他工作:
1. 执行用户通过 atexit on_exit 定义的清理函数。
2. 关闭所有打开的流,所有的缓存数据均被写入
3. 调用 _exit

 

 

异常退出

ctrl+ c

进程等待

等待分为阻塞等待非阻塞等待

阻塞等待:如果条件不满足的话,则会一直等,不会做任何事情。

非阻塞等待:也是等,并不会因为条件不满足,而“卡住”,它会继续忙着自己的事情。

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

操作系统需要释放掉进程的pcb,mm_struct,想对应的页表,代码和数据申请的空间也被释放掉,回收资源,各种队列中移除该进程的数据结构,其中,要释放掉进程pcb之前,需要先等待父进程的来读取退出信息,才能够被释放掉。

进程等待的方法

waitpid/wait

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值 : 成功返回被等待进程pid ,失败返回 -1
参数 : 输出型参数,获取子进程退出状态, 不关心则可以设置成为 NULL

父进程通过等待的方式,回收子进程的资源,获取子进程退出信息。

返回值 :当正常返回的时候 waitpid 返回收集到的子进程的进程 ID;如果调用中出错 , 则返回 -1。
参数,pid:pid=-1时,等待任意一个进程,pid>0,等待相对应的pid的进程。
   54 int main()
   55 {
   56   pid_t id=fork();
   57 
   58   if(id==0)
   59   {
   60     //child
   61     printf("i am child process.ppid:%d pid:%d\\n",getppid(),getpid());
   62     sleep(1);
   63   }
   64   else{
   65     int status=0;
   66    wait(&status);
   67     printf("i am parent process. ppid: %d pid: %d\\n",getppid(),getpid());
   69     sleep(1);
   70   }                                                                                                                                   
   71   return 0;
   72 }          

当父进程运行到wait时,父进程一直在等子进程退出,然后将子进程的退出状态传给status,当子进程退出之前,父进程就一直在那里等,不会做任何事情,直到子进程结束后,才会运行接下来的代码。

pid_ t waitpid (pid_t pid,  int *status,  int options );

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

pid:传要等待进程的pid,传-1 ,等待任意一个子进程。

status:下面详解,

options

阻塞进程之间传0即可。

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

 

status的构成

status是一个int型的数字,他不能简单看作整形来看待,而是应该用位图来看待,它是有32个bit位,但是我们只关心它的低16个bit位,高16个bit位我们不关心。

当进程正常退出的时候:status的8到15位代表的是进程退出码,0到7位是进程的终止信号,此时是没收到任何信号,所以为0.

当进程异常退出的时候

也就是说status的8到15位代表的是进程退出码,0到7位是进程的终止信号。

所以进程退出码=(status>>8)&0xFF

进程的终止信号=(status&0f7F); 

 

 

 上面是status的基本组成,但是我们一般不用

((status>>8)&0xFF)去获得进程退出码

 或者用status&0xFF去判断进程是否异常。

我们有两个接口:

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

  

进程等待的意义

首先,子进程退出,父进程如果不管不顾,就可能造成 僵尸进程 ’的问题,进而造成内存泄漏,其中的子进程的task_struct还没有被释放掉。然后, 父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。所以进程等待是为了释放子进程的空间,和读取子进程的退出信息。


 进程的程序替换

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

 

#include <unistd.h>`
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[]);

这些函数我们只需要记住exec,然后在记住后面加的字符代表的含义即可:

l(list) : 表示参数采用列表,执行的方式用参数一个一个的传进去,最后注意传一个NULL
v(vector) : 参数用数组,先把参数放在数组里,后把数组传给函数即可
p(path) : p 自动搜索环境变量 PATH,直接传文件名,不需要传路径
e(env) : 表示自己维护环境变量。

 进程替换是直接将该进程的所有代码进行替换,不论是execlp之前的代码还是之后的代码。所以替换后的printf(“hello sjp\\n")这句代码就不会被执行。

 

 

 

 enevp可以将自己写的环境变量替换掉默认的环境变量。

 

 getenv:获取进程环境变量的内容,如果没有则,返回(null);头文件为stdlib.h

事实上 , 只有 execve 是真正的系统调用 , 其它五个函数最终都调用 execve, 所以 execve man 手册 第 2 , 其它函数在 man手册第 3 节。这些函数之间的关系如下图所示。

简易的shell 

由上面的知识,我们可以知道,fork+exec是混合起来用时可以调用起另一个程序的。所以我想用制作一个shell来加深我们对前面知识的理解。

思路:

1. 获取命令行
2. 解析命令行
3. 建立一个子进程( fork
4. 替换子进程( execvp
5. 父进程等待子进程退出( wait
    1 #include<stdio.h>
    2 #include<sys/types.h>
    3 #include<sys/wait.h>
    4 #include<string.h>                                                                                                                    
    5 #include<unistd.h>
    6 #include<stdlib.h>
    7 
    8 
    9 #define LEN 1024
   10 #define NUM 32
   11  int main()
   12 {
   13   char cmd[LEN];
   14   char* myarg[NUM];
   15   while(1)
   16   {
   17     printf("[sjp myshell]#");
   18     fgets(cmd,LEN,stdin);//接收一个字符串
   19       
   20     pid_t id=fork();
   21     myarg[0]=strtok(cmd," ");//遇到空格就返回一个字符串,然后存贮在myarg中
   22     cmd[strlen(cmd)-1]='\\0';//将字符串最后的字符\\n换成\\0
   23     int i=1;
W> 24     while(myarg[i]=strtok(NULL," "))
   25     {
   26       i++;
   27     }
 n 28 
   29     if(id==0)
   30     {
   31       //child
   32        execvp(myarg[0],myarg);//将程序进行替换
   33        exit(20);
   34     }
   35     int status=0;
W> 36     pid_t ret=waitpid(id,&status,0);  
   37   }
   38 
   39                                                                                                                                       
   40   return 0;
   41 }

 strtok(str," ") : 遍历字符串str,遇到空格就返回空格前面的子字符串的地址,如果要在继续拆

                                则把前面的改为strtok( NULL," ")即可,遇到\\0就返回。

 好啦,今天的分享就到这里,感谢你的阅读和支持~

 

        

以上是关于Linux]——进程控制的主要内容,如果未能解决你的问题,请参考以下文章

linux c 退出进程的代码

Linux - 进程控制 代码(C)

Linux下的进程控制块—task_struct

Linux 如何查看进程和控制进程

Linux系统:进程控制

Linux]——进程控制