进程及进程地址空间

进程理解

概念:进程是程序的一个执行实例,其实启动一个程序(静态)本质就是启动了一个进程(动态),进程具有独立性。

用户角度:进程=代码+数据+内核数据结构(PCB结构体+页表+操作系统分配的地址空间)。

内核角度:承担分配系统资源的基本实体。

        Linux是可以同时加载多个程序的,即Linux是可能存在大量进程的,因此必须要对大量的进程进行管理,而管理的方式便是“先描述,再组织”。为了描述进程,每个进程都有一个属于自己的PCB结构体,全称process_control_block,也称进程控制块。在不同的操作系统中,PCB的具体名字是不同的,Linux中为struct task_struct{ };它会被加载到内存中,并且携带着进程的信息。

task_struct内容分类

        将进程的具体信息抽象成为task_struct之后,对进程的管理就变成了对进程PCB结构体链表的增删查改。操作系统和CPU运行某个进程的本质就是从运行队列中挑选一个task_struct来执行该进程对应的代码。

创建进程

        Linux中通过fork()系统调用从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。fork()之后要用if分流,子进程返回0,父进程返回子进程的pid,失败时返回-1。pid就是进程标识符。

fork常规用法

一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子 进程来处理请求。 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

fork调用失败的原因

系统中有太多的进程

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

        进程调用fork,当控制转移到内核中的fork代码后,内核做如下几件事:

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

        2、将父进程部分数据结构内容拷贝至子进程。

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

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

        fork之后,系统内多了一个进程,要给子进程创建对应的内核数据结构,必须子进程自己独有,因为进程具有独立性。理论上,子进程也要有自己的代码和数据,可是一般而言,创建子进程的并没有加载过程,因此,子进程只能“使用”父进程的代码和数据。具体“使用”的策略是代码部分为只读,对应数据部分为写时拷贝。

具体原因

1、当真正使用时再进行对应资源分配,是高效使用内存的一种表现。

2、操作系统无法在代码执行前预知哪些空间会被拷贝。

具体例子:

#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<iostream>
int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        std:: cout << "我是子进程" << "pid为: " << getpid() << std::endl;
    }
    else if(id < 0)
    {
        //创建子进程失败
        perror("fork");
        exit(-1);//退出码设置为-1;
    }
    else
    {
       //父进程
       waitpid(id, nullptr, 0);//等待子进程退出,父进程回收子进程资源 
       std:: cout << "我是父进程" << "pid为: " << getpid() << std::endl;
    }
    return 0;
}

        此处父进程和子进程的调度顺序并不是绝对的,而是要看操作系统的调度策略。

进程状态

概念:进程状态分为新建状态,运行状态,阻塞状态,挂起状态和退出状态。

        首先,操作系统管理的资源一定是各种各样的,不仅仅是CPU资源,还有诸如网卡,显卡,磁盘等其他设备,因此除了运行队列之外,系统中还存在其他进行管理资源等待与分配的队列。

 运行状态:并不仅仅是该进程正在运行就叫做运行态,而是task_struct在对应的运行队列中排队,这个task_struct对应的进程状态就叫做运行态。

阻塞状态:处于阻塞状态的进程,一定是处于某种资源未就绪的状态,因此等待非CPU资源就绪的进程所处的状态就称为阻塞状态。

挂起状态:内存快不足时,操作系统会执行一种策略,将长时间不执行的进程代码和数据换出到磁盘中,用来缓解内存使用紧张的问题。被换出的进程只有PCB在内存中。此时该进程的状态就称为挂起。

具体Linux下的进程状态

僵尸进程:进程已经退出,但是还不允许被操作系统释放,处于一个被检测的状态的进程称为僵尸进程。

具体例子:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        //创建子进程失败
        perror("fork error");
        return -1;
    }
    else if(id > 0)
    {
        //father process
        int cnt = 0;
        while(cnt < 10)
        {
            printf("我是父进程, pid:%d, ppid:%d\n", getpid(), getppid());
            ++cnt;
        }
    }
    else
    {
        //child process
        int cnt = 0;
        while(cnt < 5)
        {
            printf("我是子进程, pid:%d, ppid:%d\n", getpid(), getppid());
            ++cnt;
        }
    }
    return 0;
}

        上面的例子中,子进程先退出,父进程后退出,在子进程退出而父进程未退出的时间段内,该子进程为僵尸进程。

为什么会有僵尸进程的原因

        本质是为了维持该状态,以便于父进程结束后及时回收该系统资源,但维持该状态就意味着要维持该僵尸进程的PCB,虽然代码和数据可以被释放,但PCB不行,所以会存在系统资源层面的内存泄露问题。

        要解决僵尸进程的问题,要通过进程等待系列的系统接口wait和waitpid。

孤儿进程

概念:父进程提前退出,子进程并没有退出,此时的子进程称为孤儿进程。该孤儿进程会被1号进程领养,作为该进程的父进程来回收该子进程的资源,避免系统资源层面的内存泄露。1号进程就是bash。

具体例子:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        //子进程创建失败
    }
    else if(0 == id)
    {
        //child process
        int cnt = 0;
        while(cnt < 5)
        {
            printf("我是子进程  pid: %d, ppid:%d\n", getpid(), getppid());
            sleep(1);
            ++cnt;
        }
    }
    else
    {
        //father process
        int cnt = 0;
        while(cnt < 2)
        {
            printf("我是父进程  pid:%d, ppid:%d\n", getpid(), getppid());
            sleep(1);
            ++cnt;
        }
    }
    return 0;
}

上面例子中,父进程先退出,在父进程退出到子进程退出的时间段内,该子进程称为孤儿进程。

进程地址空间

概念:进程地址空间是操作系统为了管理实际内存而设计的一种数据结构,是一种虚拟地址。每个进程在被创建时,除了进程PCB之外,在合适的时候,操作系统也会为该进程申请对应的进程地址空间和页表,通过填写页表,维护虚拟地址和真实物理地址的关系。

设计进程地址空间的原因及好处

1、进程直接访问物理内存是不安全的,通过进程地址空间,凡是非法的访问或者映射,操作系统都会识别并终止该进程。有效的保护了物理内存和物理内存中的合法数据,因为地址空间和页表是操作系统创建和维护的,凡是向使用地址空间和页表进行映射,也一定会在操作系统的监管之下进行访问。

2、因为有地址空间的存在,因为有页表的映射存在,物理内存中可以对数据进行任意位置的加载,原本物理内存中的几乎所有数据和代码在内存中是乱序的,有了地址空间和页表后,在进程视角,所有的内存分布都可以是有序的,所以地址空间+页表的存在可以将内存分布有序化。

3、进程要访问的物理内存中的数据和代码,可能目前并没有在物理内存中,同样的,也可以让不同的进程映射到不同的物理内存,通过地址空间+页表的方式实现了进程的独立性。页表映射的时候,不仅仅可以映射物理内存,磁盘中的位置也是可以映射的。

4、完成了内存管理和进程管理模块的解耦合,本质上,因为地址空间的存在,上层申请空间其实是在地址空间上申请的,物理内存可以甚至一个字节都不给你,只有当真正进行对物理地址空间访问的时候,才执行内存的相关管理算法,帮你申请内存,构建页表映射关系,提高内存整体使用效率。

小问题

在fork后,返回值有两个,是因为两个进程分别return,子进程和父进程返回的值看似存到一个变量中,实际在页表映射时,映射的是不同的物理地址,也就是说,虚拟地址可以显示的一样,但实际上页表映射的物理空间是不一样的。

相关推荐

最近更新

  1. TCP协议是安全的吗?

    2024-04-20 14:22:01       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-20 14:22:01       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-20 14:22:01       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-20 14:22:01       18 阅读

热门阅读

  1. 解决方案:Pandas如何多条件筛选数据

    2024-04-20 14:22:01       12 阅读
  2. 通俗易懂降维理解------SIFT【大白话版】!!!

    2024-04-20 14:22:01       16 阅读
  3. SpringBoot项目中整合MinIO

    2024-04-20 14:22:01       13 阅读
  4. 负载均衡原理及算法

    2024-04-20 14:22:01       10 阅读
  5. CSP初赛知识精讲--图像存储

    2024-04-20 14:22:01       11 阅读
  6. STM32出现I2C通信问题如何解决

    2024-04-20 14:22:01       10 阅读
  7. 美团20240420春招第七场笔试【硬件开发方向】

    2024-04-20 14:22:01       16 阅读
  8. 甲辰年半日闲有得

    2024-04-20 14:22:01       13 阅读
  9. 前端的未来已然到来

    2024-04-20 14:22:01       13 阅读
  10. Spring Boot 学习(7)——条件注解

    2024-04-20 14:22:01       16 阅读
  11. Spring Boot 实现定时任务

    2024-04-20 14:22:01       15 阅读