Linux---进程控制

目录

一、进程创建

二、进程终止

三、进程等待

四、进程程序替换


一、进程创建

在计算机中,每一个加载到CPU上运行的程序都可以称为进程。在Linux中,每一个进程都可以通过fork函数创建若干个进程,我们将创建出来的进程称为子进程,将创建这些子进程的进程称为父进程

1.fork函数

#include<unistd.h>  //调用fork函数需要包含的头文件

pid_t  fork(void); //函数原型

返回值:<0表示创建进程失败;创建成功会给子进程返回0,父进程返回子进程的pid。

2.子进程创建示例 & fork函数如何创建进程

1)代码共享

示例:使用fork函数创建一个子进程

#include<iostream>    
#include<unistd.h>    
    
using namespace std;    
    
int main()    
{    
      cout<<"create child before"<<endl;    
      //创建子进程    
      pid_t pid = fork();    
      //代码分流    
      if(pid == 0)    
      {    
            //child    
            cout<<"i am child,pid:"<<getpid()<<endl;    
      }    
      else if(pid > 0)    
      {    
            //father    
            cout<<"i am father,pid:"<<getpid()<<endl;                                                                                                                                              
      }    
      else    
      {    
            //error    
            //cerror("creatre cerror");    
            cout<<"error"<<endl;    
      }    
    
      cout<<"create end"<<endl;    
      return 0;    
}    

运行结果分析:为什么子进程和父进程都会运行“create end”这部分代码?为什么子进程不会执行create child before这段程序?

父进程在创建子进程时,子进程会复制父进程的代码(父子进程拥有完全相同的代码)同时还会复制父进程的PCB信息(PCB信息包含了程序计数器、内存指针、上下文信息等)因此,子进程创建出来后加载到CPU上运行时会接着之后的代码进行运行(父子进程返回值不同,使用if分流后父子进程运行不同部分的代码),当然“create end”部分代码也就被运行了,而之前的“create child before”就不会被运行,这也就是所说的父进程创建子进程代码共享。同时,子进程还会复制父进程的程序地址空间等信息。

2)数据独有

父进程创建子进程,子进程会复制父进程的程序地址空间和页表等信息,使得父子进程具有相同的数据,但是为了保证进程的独立性(各进程间相互独立,一个进程的改变不会影响到另一个进程),当父子进程任意一方对数据进行修改时操作系统会为该进程重新分配一块内存将修改后的数据保存并通过页表映射到内存(原来数据和修改后的数据具有相同的虚拟内存地址但是实际物理内存不同),保证原来数据不会被改变。实现这个操作的关键技术就是写时拷贝技术

写时拷贝技术:通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体如下图:

进程调用fork,当控制转移到内核中的fork代码后,内核会进行如下操作:

①:分配新的内存块和内核数据结构给子进程

②:将父进程部分数据结构内容拷贝至子进程

③:添加子进程到系统进程列表当中

④:fork返回,开始调度

为什么会存在写时拷贝技术,而不是在创建子进程时直接为子进程重新分配内存保存一份数据?

①节约内存空间  ②节约时间,使用fork创建子进程的目的是尽快创建出一个进程,而创建出的进程并不一定会对原来数据进行修改,如果在创建进程时就将数据重新保存一份会浪费一些没必要的时间,需要修改数据时在进行保存不仅节约了时间还节约了空间。

3.fork常规用法

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

一个进程要执行一个不同的程序。

4.fork调用失败的原因

系统中有太多的进程

实际用户的进程数超过了限制

二、进程终止

1.进程退出的三种场景

进程就是运行中的程序,进程终止实际上就是将程序从调度处CPU不在占用CPU时间片运行。对于程序运行来说,无非有两种情况:程序运行完毕退出,程序遇到问题异常退出,而进程退出就可以分为以下三种场景:

1)程序运行完毕且结果正确

2)程序运行完毕且结果不正确

3)程序遇到异常,提前终止退出

2.进程退出的三种方法

1)return退出进程

在写程序时,我们经常使用return作为函数的结束返回函数的返回值,而return还有另外一个作用:在main函数中,return可以终止一个进程,而return后的值就是进程退出码(进程退出码表示进程退出的原因,一般情况下当进程退出码为0时表示程序运行正确,大于0时表示程序运行错误)。

#include<iostream>                                                                                                                                                                         
#include<unistd.h>    
using namespace std;    
    
int add(int a,int b)    
{    
  //这里的return是终止函数,进程并没有退出,返回的是函数返回值     
  return a+b;    
}    
int main()    
{    
  add(1,2);//调用add结束程序并不会结束,表示非main函数中的return不能退出进程    
    
  cout<<"should run"<<endl;    
  //进程退出,退出码为0    
  return 0;    
}    

2)_exit函数

#include <unistd.h>

void _exit(int status);

参数:status 定义了进程的终止状态,父进程通过wait来获取该值

说明:虽然statusint,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255

#include<iostream>    
#include<unistd.h>                                                                                                                                                                         
using namespace std;    
    
int main()    
{    
  pid_t pid = fork();    
  if(pid == 0)    
  {    
    for(int i = 0;i < 20;i++)    
    {    
      cout<<"child"<<endl;    
      if(i == 3)    
        _exit(-1);    
      sleep(1);    
    }    
    cout<<"这里的代码就不会被执行了"<<endl;    
  }    
  else if(pid > 0)    
  {    
    while(1)    
    {    
      sleep(6);    
      cout<<"father"<<endl;    
    }    
  }    
    
  return 0;    
}    

3)exit函数

#include <unistd.h>

void exit(int status);

#include<iostream>    
#include<stdlib.h>    
#include<unistd.h>    
using namespace std;    
    
int main()    
{    
  pid_t pid = fork();    
  if(pid == 0)    
  {    
    for(int i = 0;i < 20;i++)    
    {    
      cout<<"child"<<endl;    
      if(i == 3)    
        exit(-1);    
      sleep(1);    
    }    
    cout<<"这里的代码就不会被执行了"<<endl;    
  }    
  else if(pid > 0)    
  {    
    while(1)    
    {    
      sleep(6);                                                                                                                                                                            
      cout<<"father"<<endl;    
    }    
  }    
    
  return 0;    
}    

4)exit函数和_exit函数的区别

printf打印数据时,实际上是将数据交给显示器处理。如果需要打印的数据过多时,频繁的将数据交给显示器处理会造成巨大的效率的损耗,因此在使用printf函数时默认是将所有需要打印的数据积攒到一起,最后一起交给显示器处理。为了满足使用者的一些需求,可以将急需打印的数据从缓冲区刷新到显示器进行打印,其中C语言中的\n和c++中的endl都有刷新缓冲区的作用。而exit函数和_exit函数的最大区别就是exit函数可以刷新缓冲区而_exit函数不会刷新缓冲区。

三、进程等待

1.进程等待的必要性

①之前讲过,子进程退出,父进程如果不管不顾,就可能造成僵尸进程的问题,进而造成内存泄漏。

②另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。

③最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。

⑤父进程通过进程等待的方式,回收子

2.进程等待的方法

1)wait方法

#include<sys/types.h>

#include<sys/wait.h>

pid_t wait(int*status);

返回值:成功返回子进程pid,失败返回-1

参数:输出型参数,获取子进程退出状态,如果不关心子进程退出状态可以设为空。

#include<iostream>    
#include<sys/wait.h>    
#include<stdlib.h>                                                                                                                                                                         
#include<unistd.h>    
using namespace std;    
    
int main()    
{    
  //创建子进程    
  pid_t pid = fork();    
  if(pid == 0)    
  {    
    //child    
    cout<<"i am child"<<endl;    
    sleep(5);    
  }    
  else if(pid > 0)    
  {    
    //parent    
    while(1)    
    {    
      cout<<"i am parent,i am wait"<<endl;    
      pid_t id = wait(NULL);    
      if(id > 0)    
      {    
        //success    
        cout<<"等待成功:"<<id<<endl;    
        sleep(3);    
      }    
      else    
      {    
        //failuar    
        cout<<"失败"<<endl;    
        sleep(3);    
      }    
    }    
  }    
  else    
  {    
    //error
    cout<<"error"<<endl;
    exit(-1);
  }
  return 0;
}                  

注意:wait数组阻塞等待,如果没有进程退出,会一直处于等待状态关注子是否有子进程退出。同时,wait可以等待任意一个进程。

2)waitpid方法

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

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

参数: pid: Pid = -1,等待任一个子进程,与wait等效。 Pid>0.等待其进程IDpid相等的子进程。

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

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

如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。

如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。 如果不存在该子进程,则立即出错返回。

3.获取子进程退出状态

waitwaitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。

如果传递NULL,表示不关心子进程的退出状态信息。 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status16比特位):

1)使用wait/waitpid进行进程等待时,如果程正常退出,status的16位中的低8位为0(低8位表示进程是否正常退出,如果为0表示正常退出)。可以通过(status>>8)&0xff获取进程退出状态(进程退出码)

2)使用wait/waitpid进行进程等待时,如果程异常退出,status的16位中的高8位不使用,低7位用来表示终止信号,第8位表示core dump标志。可以通过(status&0x7f)查看是否正常退出

3)当status为0时表示进程正常退出且没有错误。

4.使用waitpid进程阻塞、非阻塞等待代码实现

1)阻塞等待

#include<iostream>    
#include<unistd.h>    
#include<stdlib.h>    
#include <sys/wait.h>    
using namespace std;    
int main()    
{    
  //使用fork创建进程    
  pid_t pid = fork();    
  if(pid == 0)    
  {    
    //child    
    for(int i = 0;i < 5;i++)    
    {    
      cout<<"i am child,pid:"<<getpid()<<endl;                                                                                                                                             
      sleep(1);    
    }    
    //进程退出,退出码为0,正常退出    
    exit(0);    
  }    
  else if(pid > 0)    
  {    
    cout<<"i am father,i am wait"<<endl;    
    //阻塞等待    
    int status;    
    while(1)    
    {    
      pid_t id = waitpid(pid,&status,0);    
      if(id > 0)    
      {    
        cout<<"wait successful,id:"<<id<<endl;    
      }    
      sleep(1);    
      //在子进程退出前,这段代码不会被执行    
      cout<<"should not run"<<endl;    
    }    
  }    
  return 0;    
} 

2)非阻塞等待

int main()    
{    
  //使用fork创建进程    
  pid_t pid = fork();    
  if(pid == 0)    
  {    
    //child    
    for(int i = 0;i < 5;i++)    
    {    
      cout<<"i am child,pid:"<<getpid()<<endl;    
      sleep(1);    
    }    
    //进程退出,退出码为0,正常退出    
    exit(0);    
  }    
  else if(pid > 0)    
  {    
    cout<<"i am father,i am wait"<<endl;    
    //非阻塞等待    
    int status;    
    while(1)    
    {    
      pid_t id = waitpid(pid,&status,WNOHANG);    
      if(id > 0)    
      {    
        cout<<"wait successful,id:"<<id<<endl;    
      }    
      else if(id == 0)    
      {    
        cout<<"no process exit"<<endl;    
      }    
      sleep(1);    
      //在子进程退出前,这段代码不会被执行    
      cout<<"should run"<<endl;                                                                                                                                                            
    }    
  }    
  return 0;    
}    

四、进程程序替换

父进程使用fork函数创建子进程后,子进程和父进程代码共享,子进程执行的还是父进程的代码 ,只不过是通过if分流后的代码块。实际开发中,经常需要子进程执行其他程序,也就是程序替换。程序替换通常使用exec类函数,当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

1.6个进程程序替换的函数

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

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

这些函数如果替换成功,则执行新的程序,不在返回执行原来的程序。当替换失败时,返回-1,因此使用exec函数只有替换失败时会有返回值。

2.函数解释

l(list) : 表示参数采用列表

v(vector) : 参数用数组

p(path) : 有p自动搜索环境变量PATH

e(env) : 表示自己维护环境变量

exec函数族

3.程序替换代码

1)替换系统程序

#include <unistd.h>
int main()
{
     char *const argv[] = {"ps", "-ef", NULL};
     char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};

     execl("/bin/ps", "ps", "-ef", NULL);

     // 带p的,可以使用环境变量PATH,无需写全路径
     execlp("ps", "ps", "-ef", NULL);

     // 带e的,需要自己组装环境变量
     execle("ps", "ps", "-ef", NULL, envp);

     execv("/bin/ps", argv);
 
     // 带p的,可以使用环境变量PATH,无需写全路径
     execvp("ps", argv);

     // 带e的,需要自己组装环境变量
     execve("/bin/ps", argv, envp);

     exit(0);
}

2)替换自己写的程序

//替换程序
#include<iostream>                                                                                                                                                                         
using namespace std;    
    
int main()    
{    
  cout<<"i am new function"<<endl;    
  cout<<"exit"<<endl;    
  cout<<"----------"<<endl;    
  return 0;    
} 

//被替换程序
#include<iostream>    
using namespace std;    
#include<unistd.h>                                                                                                                                                                         
int main()    
{    
  cout<<"begin"<<endl;    
  execl("./testexec","./testexec",NULL);    
    
  cout<<"should not run"<<endl;    
    
  return 0;    
} 
  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

疯狂嘚程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值