Linux操作系统-进程控制

目录

进程地址空间

为什么要有虚拟地址空间?

重新审视fork函数

写时拷贝

fork函数失败

进程终止

进程退出场景

return 与 exit 的区别

exit 与 _exit的区别

进程等待

wait函数

waitpid

进程程序替换

环境变量

获取命令行参数

exec系列的函数


进程地址空间

进程地址空间的部分概念在之前说过

C++内存管理 && 读高质量C++/C编程指南第7章_TangguTae的博客-CSDN博客

先看如下的例子

#include<iostream>

#include<unistd.h>
#include<sys/types.h>
int val = 100;
int main()
{
  pid_t id = fork();//创建进程,从这开始就有两个执行流
  while(true)
  {
    if(id == 0)
    {
      //子进程                                                   
      std::cout<<"child# val is: "<<val<<",val's address: "<<&val<<std::endl;
      sleep(1);
    }
    else if(id > 0)
    {
      //父进程
      val = 10;
      sleep(3);
      std::cout<<"father# val is: "<<val<<",val's address: "<<&val<<std::endl;
    }
    else
    {
      std::cerr<<"fork error!"<<std::endl;
    }
   }
  return 0;
}

运行结果:

父进程和子进程在相同的地址中的内容不一样,只能说明一个问题,两个进程实际指向的并不是同一块内存,准确来说不是物理内存。

这个地址实际上是虚拟地址。

所以说,虽然父子进程返回的地址是一样的,但都是虚拟地址(包括平常用的指针啥的都是一样的),最开始时,没有对全局变量进行修改,父子进程实际的物理地址共用同一块空间(操作系统的优化),一旦发生修改,会新开辟一块空间(写时拷贝)。

虚拟地址空间本身是一个数据结构:

参考源码:

 

 用自己的总结就好比下面

struct mm_struct
{
	unsigned long code_start//代码起始段
	unsigned long code_end
	unsigned long init_data_start//初始化数据
	unsigned long init_data_end
	unsigned long uninit_data_start//未初始化数据
	unsigned long uninit_data_end
	unsigned long heap_start//堆区
	unsigned long heap_brk
	
	……//等等
}

这里面就包含,代码段、数据段、堆、栈等等所定义的不同区域的起始位置和结束位置。

申请空间的本质是:向内存索要空间,得到物理地址,然后在特定区域申请没有被使用的虚拟地址,建立映射关系(硬件MMU负责完成),返回虚拟地址。其中物理地址用户一概不可见

为什么要有虚拟地址空间?

如果我们直接访问物理地址,隐含一些问题

1、野指针的存在,容易随意访问到别的区域,甚至修改。

2、由于内存可能会存在内存碎片,一个进程的数据存放在实际内存中的位置可能不连续的,降低了访问效率。

3、增加了异常越界的概率。

为了避免这种情况引入虚拟地址空间

通过虚拟地址空间,可以将空间连续化处理,并且还可以保护物理内存。

一个进程如果越界访问到别人的空间,首先页表中没有这层映射关系,操作系统就会终止该操作。也表中除了映射关系以外,还有对应的权限管理,如果一个进程越界自己的数据,比如写入到自己字符创数据区域,然而在页表的映射关系中,字符串只有读权限,同样也会在操作系统层面上崩溃。

补充:虚拟内存的优点--参考《Linux-Unix系统编程手册》--20220907

1、进程与进程、进程与内核相互隔离,所以一个进程不能读取或修改另一进程或内核的内存

2、两个或多个进程可以共享内存,即通过页表将两个进程地址空间映射到同一块物理内存当中,使用system V里面的shm系列的函数 和 系统调用mmap来显示的请求内存共享区。这样做可以实现进程间的通信

3、便于保护物理内存,也就是我们上面所说的。

4、一个进程所占用的内存(虚拟内存的大小)能够超出RAM的容量。

5、RAM可以容纳的进程的数量增多了,提高CPU的利用率。

重新审视fork函数

当父进程调用fork函数后,内核需要执行

1、 重新分配内存块和内核数据结构给子进程

2、将父进程部分数据结构内容拷贝至子进程(地址空间、页表映射等等内容)

3、将子进程添加到系统进程列表中

4、fork返回,调度器开始调度

其中默认情况下,父子进程共享代码,但是数据各自私有一份。

代码共享:所有代码共享,不过一般都是从fork之后开始执行。之所以代码能共享,因为代码不可被修改的,代码分为两份浪费空间。

数据为什么不共享?

进程之间具有独立性、修改数据会相互影响。

写时拷贝

上面一个例子说道父子进程最开始时可以看到同样的数据,一旦发生修改,各自的的内容又不相同

原因当父进程创建子进程时(fork),代码段和数据段是只读的,且此时父进程和子进程的代码和数据是共享的

当父进程和子进程任意一个需要写入操作时,将会发生拷贝,并将数据段变为可读可写的状态,这个过程就叫做写时拷贝。

 计算机通过这样的方式,不立即进行数据的拷贝,节约空间和时间。

fork函数失败

1、系统中有太多的进程,资源不够

2、进程的数量超出了限制

进程终止

进程退出场景

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

2、代码运行完毕,结果不正确

3、代码异常终止(ctrl c发信号终止进程)

进程退出后会有进程退出码

1、从main函数返回(常见的return 0就是进程正常结束后所返回0,退出码也就为0)

2、调用exit函数

3、_exit函数

查看退出码的方法:echo $?

为什么main函数要return 0,而不是return其他值

原因:C++/C在函数设计中0一般代表正确,而非0错误所对应一种错误的原因。

例如

正常退出,return 0,得到的进退出码为0. 

ctrl c异常终止程序,得到进程退出码非0.

return 与 exit 的区别

return 是终止函数,在main函数下是终止进程。

而exit是直接终止进程。

exit 与 _exit的区别

_exit直接进入内核干掉进程

exit会做一些清理工作 ,例如刷新缓冲区、关闭流等

进程等待

子进程先退出,父进程如果不管不顾,子进程会变成僵尸进程,从而引起系统资源的泄漏。解决办法是父进程进行等待。

wait函数

看一下库里面是如何描述的

wait是系统调用函数,当子进程退出时,父进程调用wait函数让系统去释放资源,并且父进程可以获取到子进程的退出状态。

int* status是输出型参数,当父进程不需要关心子进程退出的状态时,可以设为空。

返回值:当等待成功,返回子进程pid,否则返回-1

测试:

#include<iostream>  
  
#include<unistd.h>  
#include<sys/types.h>  
int main()  
{  
  pid_t id = fork();//创建进程  
  while(true)  
  {  
    if(id == 0)  
    {                                               
      //子进程                                 
      int count = 0;  
      while(true)
      {

        if(++count == 5)//count等于5的时候结束进程
          exit(0);
        std::cout<<"i am child ..."<<std::endl;
        sleep(1); 
      }
    }                                               
    else if(id > 0)                                 
    {
      //父进程
      sleep(1);
      std::cout<<"i am father ..."<<std::endl;
    }                                               
    else                                            
    {                                               
      std::cerr<<"fork error!"<<std::endl;          
    }                                               
   }                                                
  return 0;  
}  

当父进程没有等待时,子进程变成了僵尸状态。

修改代码:

#include<iostream>

#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
  pid_t id = fork();//创建进程
  while(true)
  {
    if(id == 0)
    {
      //子进程                               
      int count = 0;
      while(true)
      {

        if(++count == 5)
          exit(0);
        std::cout<<"i am child ..."<<std::endl;
        sleep(1);
      }
    }
    else if(id > 0)
    {
      //父进程
      sleep(1);
      std::cout<<"i am father ..."<<std::endl;
      std::cout<<"wait begin "<<std::endl;
      pid_t ret = wait(nullptr);//阻塞等待
      std::cout<<"wait end "<<",return val is: "<<ret<<std::endl;
    }                                                                 
    else                                                              
    {                                                                 
      std::cerr<<"fork error!"<<std::endl;                            
    }                                                                 
   }                                                                  
  return 0;
}

wait等待是一个阻塞等待过程,父进程会在wait函数这阻塞住。

当成功等到子进程退出后,得到的返回值为子进程的pid,通过ps axj 查看进程状态可以看到此时已经没有僵尸状态的子进程了。后续没有子进程的时候wait会出错,返回值为-1.

注意

回收资源是操作系统进行回收的,父进程只是触发了回收的动作。

waitpid

waitpid同样也是等待进程退出。与wait有些区别

第一个参数是进程的ID,第二个参数也是一个输出型参数status,第三个参数是waitpid的选项

进程pid的值不同时效果也不一样,看官方解释

需要注意的是:

当pid = -1 的时候表示等待任意进程

当pid > 0 的时候表示等待指定pid的进程

options选择等待的方式, 默认设置为0表示阻塞等待,设置为WNOHONG表示非阻塞等待。

status,输出型参数,操作系统会将退出信息通过这个参数返回给父进程。设置为空可忽略此参数

status总共32位,但是只关心他的低16位

status返回值有两种情况,一个是正常退出时,低16位的高八位就是进程退出码。当进程是被信号所终止时,低16位中的低七位就是被哪个信号所杀

所以我们要获取进程退出的状态就可以用如下两种代码获取

pid_t ret = waitpid(id,&status,0);//阻塞等待,非阻塞等待可以把0改为WNOHONG
int res = (status>>8)&0xff;//正常退出
std::cout<<"eixt code is "<<res<<std::endl;
//-------------------------------------
pid_t ret = waitpid(id,&status,0);//阻塞等待
int res = status&0x7f;//被信号所杀
std::cout<<"signal code is "<<res<<std::endl;

进程程序替换

环境变量

环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。

这个参数种类很多,用env可以查看当前系统的环境变量,常见的USER,PATH,PWD,SHELL、HOME等等。

现在只关心路径PATH,当我们输入一些命令行的命令时,系统是如何工作的?

1、找到这条命令                                 2、运行它

其实cd、pwd、ls、ps等等命令都是一个程序,但是为什么不需要指定路径就可以运行呢?或者说我们运行我们自己写的程序需要加上./xxx才能运行?

其实就是环境变量PATH的作用。

$PATH 查看所有的路径

当我们输入ls的时候,系统默认从上面路径去找这条命令。

如果我们自己写的程序也想不加./直接运行,可以将我们的可执行程序放在这些路径下,或者添加一条路径

sudo cp xxx /usr/bin 或者 export PATH=$PATH: 当前路径

获取命令行参数

ls 这样的命令可以在后面带选项,例如 ls -l,ls -al等等

程序的main函数不一定是void形参,还是可以带很多参数的

//argc代表argv的大小,argv是一个指针数组
int main(int argc, char* argv[])
{
}

测试:

#include <stdio.h>
int main(int argc, char* argv[])
{
  for(int i = 0; i < argc; i++)
  {
    printf("argv[%d] = %s\n",i,argv[i]);
  }                                          
  return 0;
}
//注意:编译的时候用 gcc main.c -std=c99

 argc默认大小为1,argv默认第一个参数是程序名称。

exec系列的函数

之前有说过,bash进程作为命令行解释器,当输入命令时,创建子进程去执行对应的程序,其中的原理就是通过exec系列的函数将程序替换,从而让子进程不在执行父进程相同的程序,而是去执行对应的程序。

总共有六种exec开头的函数,头文件unistd.h

execl与execv

l:表示列表的形式,表示可以传递多个参数,用列表的形式

v:代表数组的形式

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

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

#include <stdio.h>
#include<unistd.h>
int main()
{
  printf("running before ...\n");
  //以列表形式替换
  execl("/usr/bin/ls","ls","-a","-l",NULL);//后面一定要有个NULL作为结束标志
  printf("running after ...\n");                   
  return 0;
}

这段代码是将execl后面的代码全都替换了,并去执行ls -a -l命令

效果和直接在命令行上面输入 ls -a -l效果是一样的 

execv则需要定义一个指针数组,char* argv[] = {"ls","-a", "-l",NULL}。效果都是一样的

execlp、execle与execvp、execve 

后面的一个字母p代表可以自动搜索环境变量,e表示自己维护环境变量,需要传递。

#include <stdio.h>
#include<unistd.h>
int main()
{
  printf("running before ...\n");
  execlp("ls","ls","-a","-l",NULL);//无需写全路径      
  printf("running after ...\n");
  return 0;
}

带p的无需写全路径,自己回去环境变量里面找。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值