linux进程控制(代码片段)

可乐不解渴 可乐不解渴     2023-01-09     808

关键词:

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

进程创建

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

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系统:进程控制(代码片段)

进程控制1.进程创建fork函数是代码创建进程的一种返方式,它从已存在的进程中创建一个新进程,原进程为父进程,新进程叫做子进程。NAMEfork-createachildprocessSYNOPSIS#include<unistd.h>pid_tfork(void);DESCRIPTIONfork()createsanewp... 查看详情

linux进程详解二:进程控制(代码片段)

【Linux】进程详解二:进程控制文章目录【Linux】进程详解二:进程控制前言一、进程创建1.fork()函数的认识1.1.利用系统调用fork()创建进程1.2.fork()在内核中都干了什么?1.3.父子进程的关系2.fork()函数的返回值1.`fork()... 查看详情

linux--进程控制(代码片段)

进程控制创建进程fork函数初始写时拷贝fork常规用法fork调用失败的原因进程终止进程退出场景进程常见退出方法正常终止异常退出_exit函数return退出进程终止时的OS进程等待进程等待的必要性进程的等待方法(由父进程调用)waitwaitp... 查看详情

linux之进程控制详解(代码片段)

进程控制文章目录进程控制进程创建fork函数初识fork函数返回值写时拷贝fork常规用法fork调用失败的原因进程终止进程退出场景进程常见退出方法main函数返回exit_exit进程等待进程等待的方法wait方法waitpid方法进程程序替换替换原... 查看详情

linux进程控制(代码片段)

文章目录一.进程创建写时拷贝fork常规用法fork调用失败的原因二.进程终止进程退出场景进程常见退出方法三.进程等待进程等待必要性进程等待的方法wait方法waitpid方法获取子进程status非阻塞式等待进程程序替换替换原理替换函... 查看详情

进程控制(linux)(代码片段)

进程控制进程创建fork函数fork函数返回值写时拷贝fork常规用法fork调用失败的原因进程终止进程退出情况进程等待进程等待的必要性进程等待的方法获取子进程status1、wait2、waitpid方法进程替换替换原理替换函数函数解释总结我们... 查看详情

linux入门:进程控制(代码片段)

进程控制1️⃣进程的创建fork函数初识fork函数的返回值写时拷贝fork函数常规用法fork函数失败的原因2️⃣进程的终止进程退出的场景进程常见退出方法_exit函数exit函数return退出3️⃣进程等待进程等待的必要性进程等待的方法wait... 查看详情

linux入门:进程控制(代码片段)

进程控制1️⃣进程的创建fork函数初识fork函数的返回值写时拷贝fork函数常规用法fork函数失败的原因2️⃣进程的终止进程退出的场景进程常见退出方法_exit函数exit函数return退出3️⃣进程等待进程等待的必要性进程等待的方法wait... 查看详情

linux进程控制(精讲)(代码片段)

文章目录一、进程创建fork函数初识fork函数返回值写时拷贝fork常规用法fork调用失败的原因二、进程终止进程退出场景进程退出码进程正常退出return退出exit函数_exit函数return、exit和_exit之间的区别与联系进程异常退出三、进程等... 查看详情

linux进程控制(代码片段)

目录进程创建fork函数初识fork函数返回值写时拷贝fork常规用法fork调用失败的原因进程终止进程退出的情况分类退出码进程常见退出方法_exit函数exit函数_exit和exit的区别进程等待进程等待必要性进程等待的方法wait方法waitpid方法获... 查看详情

linux进程控制(代码片段)

目录进程创建fork函数初识fork函数返回值写时拷贝fork常规用法fork调用失败的原因进程终止进程退出的情况分类退出码进程常见退出方法_exit函数exit函数_exit和exit的区别进程等待进程等待必要性进程等待的方法wait方法waitpid方法获... 查看详情

linux]——进程控制(代码片段)

1.fork函数fork函数是非常重要的函数,它能从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。#include<unistd.h>pid_tfork(void);返回值:子进程中返回0,父进程返回子进程id,出错返回-1 当... 查看详情

linux进程控制(代码片段)

文章目录前言一,进程创建1.1fork函数初识1.2虚拟地址空间1.3其他概念二,进程终止2.1进程退出的场景2.2进程常见的退出方法2.2.1exit函数和_exit函数的原理2.2.2exit函数和_exit函数的区别三,进程等待3.1进程等待的必要性3... 查看详情

《linux从0到99》八进程控制(代码片段)

LINUX进程控制1.进程创建(fork)2.进程终止01_exit函数02exit函数03atexit函数04return退出3.进程等待01进程等待的必要性02进程等待的方法a)wait函数b)waitpid函数03获取子进程status5.进程替换01进程替换原理02进程替换函数6.利用所学... 查看详情

linux-进程控制(代码片段)

作为操作系统对于其中的进程控制,对于一个多进程少CPU的情况,操作系统要有一个组织调度进程的算法来合理分配资源。进程调度队列NICE与PRI运行队列-runqueue优先级过期队列和活跃队列active指针与expired指针进程创建for... 查看详情

linux--进程控制(代码片段)

前言:    这篇文章主要是讲解Linux下的进程控制,我们会学习到进程等待,进程程序替换,微型shell,重新认识shell运行原理。最后也编写了一个属于我们自己的shell,尽管功能不够齐全,但是还是感觉挺... 查看详情

[linux]linux进程控制超详细解析(代码片段)

在上篇文章中,我们了解了进程中的相关概念,在这篇文章中,就要对进程的控制做一详解目录1.进程创建1.1fork()函数1.2关于写时拷贝2.进程终止2.1进程退出的场景2.2常见的退出方法2.2.1main函数中return2.2.2使用exit或_exit... 查看详情

linux中的进程及进程控制(代码片段)

一、整体大纲二、基础知识1.进程相关概念1)程序和进程   程序,是指编译好的二进制文件,在磁盘上,不占用系统资源(cpu、内存、打开的文件、设备、锁....)   进程,是一个抽象的概念,与操作系统原理... 查看详情