linux进程控制

Posted 可乐不解渴

tags:

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

记忆的梗上,谁不有两三朵娉婷,披着情绪的。

进程创建

我们在上一篇进程概念中已经讲过一些进程的创建,在这里我们简单叙述一遍。

fork函数

创建子进程的最为关键的是fork()函数,在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
函数原型:pid_t fork(void);
其实这个pid_t类型原型其实就是unsigned int。
该函数包含在#include<unistd.h>中。

我们的进程调用fork,当控制转移到内核中的fork代码后,内核做:
1、首先先分配新的内存块(PCB)和内核数据结构给子进程;
2、再将父进程部分数据结构内容(如页表、进程地址空间)拷贝至子进程;
3、其次添加子进程到系统进程列表当中;
4、最后fork返回,使用调度器调度。

fork函数返回值

fork函数会返回两个返回值。

  • 一个给父进程返回的是子进程的pid;
  • 另一个返回0值给子进程。

写时拷贝

通常在不写入时,父子代码和数据都是共享的。但当任意一方试图写入,便以写时拷贝的方式使得各自拥有一份副本。
为什么会存在写时拷贝呢?
通常情况子进程再不写入的情况下,如果直接给子进程直接拷贝一份和父进程完全相同的代码和数据时,势必会导致数据的冗余和内存空间的可使用量减少,因为父子进程的数据和代码都一样,那么直接让子进程使用父进程的代码和数据即可,直到要修改时才进行拷贝。而OS使用写时拷贝是因为为了减少内存空间的使用,更加合理的分配内存空间。

fork失败的通常情况

1、系统中有太多的进程,内存空间不足导致创建失败。
2、实际用户创建的进程数量超过了限制。

进程终止

通常情况下我们的进程退出场景有三种:
1、代码出现异常然后终止。
2、代码运行完毕,结果正确。
3、代码运行完毕,结果不正确。
而在我们进程退出的方法也有三种:

  1. 从main中使用return返回退出码。
  2. 调用exit返回退出码。
  3. _exit系统调用返回退出码。
  4. 进程出现异常终止ctrl + c,信号终止。

_exit与exit

这两个函数都包含在#include<unistd.h>
首先我们先说_exit系统调用接口,该接口函数原型为:void _exit(int status);
其中这个status定义了进程的终止状态。
下面我们简单的来使用一下:

#include<iostream>    
using namespace std;    
    
void test()    
{    
  cout<<"hello CSDN";       //故意不换行,不去主动刷新缓冲区                                                                                                                             
  _exit(-1);    
}    
int main()    
{    
  test();    
  return 0;    
}    

该代码是让main函数去调用test()函数让其执行其内部的两行代码。

在上面的动图中我们会发现使用了_exit函数并没有将我们的hello CSDN打印出来,这是为什么呢?
因为我们上面的代码是没有使用换行的,所以没有让缓冲区进行行刷新策略,而执行_exit函数直接将该进程直接终止,导致我们想要打印的话直接丢失掉了。

而exit函数与_exit函数不同的事,它会帮我们将缓冲区里的内容刷新。
还是使用上面的代码,但是函数换成了exit。

#include<iostream>    
using namespace std;    
    
void test()    
{    
  cout<<"hello CSDN";       //故意不换行,不去主动刷新缓冲区                                                                                                                             
  exit(-1);    
}    
int main()    
{    
  test();    
  return 0;    
}    


在上面的图片中我们可以发现使用了exit函数它会将我们缓冲区内的数据刷新出来。

进程等待

在直接我们讲僵尸进程的时候说过,子进程退出,父进程如果不管不顾,就可能造成僵尸进程的问题,进而造成内存泄漏。而父进程派给子进程的任务完成的如何,我们需要知道。如子进程运行完成,结果对还是错,或者是否出现异常导致不正常退出。此时父进程就需要通过进程等待的方式,来回收子进程资源,获取子进程退出信息。

进程等待的方法

进程等待的方式有两种,分别是wait和waitpid。
而它们所在的头文件为#include <sys/types.h>和#include <sys/wait.h>

wait

首先我们先说说wait。其函数原型为:pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。

参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL,否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。这里的status与我们用C语言刷力扣,与上面的函数形参的returnsize是一个道理。

下面我们运用代码来做测试:

#include<iostream>    
#include<unistd.h>    
#include <sys/wait.h>    
#include<sys/types.h>    
using namespace std;    
    
int main()    
{    
  pid_t id=fork();    
  if(id<0)    
  {    
    cerr<<"creater process error"<<endl;    
  }    
  else if(id==0)    
  {    
      sleep(5);    
      cout<<"I am child, pid: "<<getpid()<<" ppid:"<<getppid()<<endl;    
  }    
  else    
  {    
     pid_t ret=wait(NULL);    
    
     cout<<"I am father, pid: "<<getpid()<<" ppid:"<<getppid()<<endl;                                                                                    
     if(ret>0)    
     {    
         cout<<"return child pid:"<<ret<<endl;    
     }    
   }    
  return 0;    
}  


在上面的代码和最后的结果中,我们发现wait成功返回了子进程的pid,这表明等待子进程退出成功。

waitpid

waidpid的函数原型为:pid_ t waitpid(pid_t pid, int *status, int options);

返回值:
1、当正常返回的时候waitpid返回收集到的子进程的进程ID。
2、如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0。
3、如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。

参数:

pid:
pid=-1,等待任一个子进程。与wait等效。
pid>0,等待其进程PID与pid相等的子进程。

status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)。
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)。
status不能简单的当作整形来看待,可以当作是数据结构中的位图来看待,具体细节如下图(只研究status低16比特位):这里它是用整形的2个字节来实现的,高2个字节不看。每一位对应不同的状态。

如果进程是正常终止的话,那么我们怎么得到它的退出码呢?
我们可以利用位操作得到。如:(status>>8)&0xff,而判断该进程是否是正常退出只需要利用status&0x7f来判断。

options:
WNOHANG: 以非阻塞的方式等待。若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的PID。

进程的阻塞方式等待:
阻塞的方式意思就是直到成功才返回。举个例子,比如你找你的好朋友去吃饭,你到了他家楼下等他下楼,那么只有等到他下来,你们才能一起去吃饭。

#include<iostream>
#include<unistd.h>    
#include <sys/wait.h>    
#include<sys/types.h>    
using namespace std;    
    
int main()    
{    
  pid_t id=fork();    
  if(id<0)    
  {    
    cerr<<"creater process error"<<endl;    
  }    
  else if(id==0)    
  {    
      sleep(5);    
      cout<<"I am child id"<<endl;    
  }    
  int status=0;    
  //我们这里以阻塞的方式等待子进程的退出
   pid_t ret=waitpid(id,&status,0);    
   if(ret>0)    
   {    
   	//WIFEXITED(status) 
     if((status&0x7f)==0)    
     {    
     	//WEXITSTATUS(status)
       cout<<"return code:"<<((status>>8)&0xff)<<endl;    
     }    
   }                                                                                                                                                     
  return 0;    
} 

进程的非阻塞方式等待:
而非阻塞的方式意思是:每隔一小会来检测一次进程退出没有,如果没有先返回0代表子进程还在运行,其先去执行别的代码,过会再来检查进程退出没有。
还是上面那个例子:你和你朋友去吃饭,朋友叫你在楼下等他,你每隔一会就打他个电话问他好了没有,如果没有你是不是可以做别的事情,比如刷抖音、听听歌之类,之后会了一定时间的间隔在继续打朋友的电话问他好了没有,一直重复进行类似这种操作。

#include<iostream>                                                                                                                                       
#include<unistd.h>
#include <sys/wait.h>
#include<sys/types.h>
using namespace std;

int main()
{
  pid_t id=fork();
  if(id<0)
  {
    cerr<<"creater process error"<<endl;    
  }    
  else if(id==0)    
  {    
    int count=5;    
    while(count--)    
    {    
      sleep(1);    
      cout<<"I am child id"<<endl;    
    }    
    exit(0);    
  }    
  while(1)    
  {    
    int status=0;                                                                                                                                        
    //以非阻塞的形式去等待子进程的退出,每隔一会来看看子进程退出没有
    pid_t ret=waitpid(id,&status,WNOHANG);
    if(ret>0) 
    {
       if((status&0x7f)==0)
       {
         cout<<"return code:"<<((status>>8)&0xff)<<endl;
         break;
       }
    }
    else if(ret==0) 
    {
      sleep(1);
      cout<<"child has running"<<endl;
    }
  }
  return 0;
}

执行过程如下:

进程程序替换

在这之前我们都只是创建子进程,然后让子进程去执行父进程的部分代码,相当于代码执行的是同一份代码。如果我们不想让子进程执行一丁点与父进程有关的代码呢?这就需要进行进程程序替换了,让子进程去执行另一个与父进程没有任何关系的程序。

而我们要将一个可执行程序运行起来变成进程,第一件是一定是创建PCB,然后构建页表,第二件事是将对应的代码和数据加载到内存当中,那么最后将两份资源进行对接即可。那么我们可不可以不要父进程里的东西,而是直接从磁盘中重新加载一个全新的程序,将新的程序的代码和数据加载给子进程,相当于是子进程在进行写时拷贝,将父进程的代码和数据覆盖掉。

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

在使用前我们得先了解一下程序替换函数有如下几个:

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[]);

这些函数的头文件都为#include <unistd.h>

这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
如果调用出错则返回-1,否则就是成功的,它将不会执行父进程的代码,所以exec函数只有出错的返回值而没有成功的返回值。

exec系列函数命名理解

这些函数原型看起来很容易混,但只要掌握了规律就很好记。
l(list) : 表示参数采用列表。
v(vector) : 参数用数组。
p(path) : 有p自动搜索环境变量PATH。
e(env) : 表示自己维护环境变量。

下面我们自己来写一些代码来运用这些函数。

exec系列函数使用

execl

由于该函数名字上没有带p,所以第一个参数需要我们手动的添加要执行的程序的路径,第二个参数开始表示我们想怎么执行这个程序,最后我们必须保证以NULL结尾。

#include<stdio.h>    
#include<unistd.h>    
    
int main()    
{    
  printf("I am a process\\n");    
  sleep(3);    
  execl("/usr/bin/ls","ls","-al",NULL);       
  printf("error\\n");    
  return 0;    
} 

执行结果如下:我们会发现execl函数下的printf并没有被打印,这就表明是替换成功的,如果替换失败了,那么就会执行printf这行代码。

execlp

由于这个函数名带p,所以我们就不需要给具体的路径了,它可以使用环境变量PATH,无需写全路径。这里我们就可以写相对路径,后面的参数和之前一样想怎么执行这个程序。

#include<stdio.h>    
#include<unistd.h>    
    
int main()    
{    
  printf("I am a process\\n");    
  sleep(3);    
  execlp("ls","ls","-al",NULL);    
                                                       
  printf("error\\n");                                                                           
  return 0;                                                                                    
}   

结果和刚刚的一样,如下图所示:

execle

这个函数名称又带l又带e,所以按照上面的理解,e表示我们需要自己去给环境变量。
这里我们用该函数去调用自己写的另一个c++程序。

  #include<stdio.h>    
  #include<unistd.h>    
      
  int main()    
  {    
    printf("I am a process\\n");    
    sleep(3);        
	 char*myenv[]={"myenv=youcanseeme",
	"PATH=/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/ZJ/.local/bin:/home/ZJ/bin",
	 NULL};    
      
    execle("./cmd","cmd",NULL,myenv);    
    printf("error\\n");    
    return 0;    
  } 


其中上面调用getenv来获取环境变量,其中从父进程的execlp函数传过来的myenv会将系统的环境变量覆盖掉。然后我们想要打印某个名字环境变量必须是传进来里名称有的,如果没有则会输出null。=号前面即是环境变量的名称。
结果如下图所示:

execv

在上面我们运用的都是带l的,由于有l就不可能会有v,在这里是互斥的,并且函数名并没有带p所以我们必须将要执行的程序的路径写全。
然后运用一个char类型的指针数组在内部存储我们要怎么执行这个程序,然后将这个数组的名称放入函数的参数中即可。

  #include<stdio.h>    
  #include<unistd.h>    
  int main()    
  {    
    printf("I am a process\\n");    
    sleep(3);    
    printf("I am a execv\\n");
  	char *arr[]={"ls","-al",NULL};                                                                                                                       
    execv("/usr/bin/ls",arr);   
    printf("error\\n");       
    return 0;                
  }

结果如下图所示:

execvp

同理带p就可以不需要写绝对路径了,当然写全也不会有问题。

  #include<stdio.h>    
  #include<unistd.h>        
  int main()    
  {    
    printf("I am a process\\n");    
    sleep(3);    
    printf("I am a execvp\\n");
    char *arr[]={"ls","-al",NULL};                                                                                                                       
    execvp("ls",arr);     
    printf("error\\n");       
    return 0;                
  }  

结果如下图所示:

在上面我明明写了有6个exec系列函数但我偏偏将他们分开两种格式来写。因为在只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册第2节,其它的函数在man手册第3节。而这些函数之间的关系如下图所示:

如果小伙伴还没看懂可以在评论区留言,我会在评论区给你解答!
如有错误之处还请各位指出!!!
那本篇文章就到这里啦,下次再见啦!

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

linux c 退出进程的代码

Linux - 进程控制 代码(C)

Linux下的进程控制块—task_struct

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

Linux系统:进程控制

Linux]——进程控制