• Linux进程概念


    冯诺依曼体系结构

    在这里

    操作系统(Operator System)

    在这里
    设计OS目的:
    对下:1、与硬件交互,管理好软硬件资源。
    校长:操作系统
    辅导员:驱动程序
    硬件:学生
    校长提出决策,辅导员执行决策。
    对上:2、为用户程序(应用程序)提供一个良好的执行环境。
    操作系统管理好软硬资源后,操作系统就能为用户提供良好的运行环境,例如提供了系统接口。
    例如:用户需要执行printf(“hello”);语句,向显示器写入hello字符串,那么printf()函数的操作必须贯穿操作系统,通过调用系统接口,并且得到操作系统的允许,才能访问软硬件资源。
    系统接口与用户层接口
    系统接口:Linux操作系统由c语言写,所以叫c函数;
    用户层操作接口:在系统接口之上,对系统接口封装的函数库(c库,java库),shell的外壳,部分指令。
    注意:用户能通过用户操作接口进行指令操作,开发操作,管理操作。库函数里如果软件需要对硬件方面的操作,那么在语言层面上会调用系统接口。

    进程

    基本概念

    课本概念:程序的一个执行实例,正在执行的程序等
    内核观点:担当分配系统资源(CPU时间,内存)的实体。

    什么是进程?

    结论:进程= 程序 + 操作系统维护进程的相关的数据结构。
    曾经我们启动的程序,先从硬盘上把程序的代码+数据加载到内存里,被刚刚加载到内存的叫程序文件内容,这个程序文件内容就好比学生,校长要管理好学生,先对学生描述,然后通过数据结构把每一位学生组织起来。所以操作系统给每个加载到内存的程序都创建了进程控制块(PCB),PCB是操作系统里的统称名词,在Linux操作系统下叫task_struct的结构体名。PCB对程序进行描述,还有相关的数据结构,操作系统只需要对task_struct操作就能达到对程序的管理。
    在这里插入图片描述tast_struct包含了进程内部的所有属性信息!进程就好比链表节点,为了管理好,就要有链表的数据结构。
    占在os的角度, 由双链表数据结构组织进程,CPU有一个运行队列,CPU要处理程序时,只需要让进程的链表头链接到 运行队列即可。
    在这里插入图片描述结论:有了进程控制块,所有的进程管理任务与进程对应的程序毫无关系!!与进程对应的内核创建的该进程的PCB强相关。

    描述进程-PCB

    • 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
    • 课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct

    task_struct-PCB的一种

    • 在Linux中描述进程的结构体叫做task_struct。
    • task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。

    task_ struct内容分类

    task_struct数据结构庞大而复杂,但它可以分成一些功能组成部分:

    1.标示符: 描述本进程的唯一标示符,用来区别其他进程。
    例如进程pid,获取进程pid,我们可以调用getpid(),该函数包含在unistd.h文件里,该文件不属于c标准库。

    #include
    #include
    #include
    
    int main()
    {
      printf("%d\n",getpid());//获取当前进程pid
      printf("%d\n",getppid());//获取父进程pid
      return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    注意:我们执行的程序都是bash的子进程。

    2. 状态: 任务状态,退出代码,退出信号等。
    ps:进程状态在下面会将。
    当程序执行最后一条代码时,return n;n为退出码,该退出码会写到进程信息里。在这里插入图片描述echo $? 为获取环境变量,该变量保存输出最近执行的程序的退出码。

    也就task_struct 当前具有:

    task_struct{
    	pid_t pid;// 进程pid
    	pid_t ppid;// 父进程pid
    	int code,exit_code; 退出码 
    	int status;// 进程转态
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3. 优先级: 相对于其他进程的优先级。
    进程都能被CPU调用,但是优先级能决定进程谁先被执行。

    4. 程序计数器: 程序中即将被执行的下一条指令的地址。
    有一个pc指针,指向下一条指令,程序在运行的时候会不断的修改pc指针。
    5.内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
    可通过该指针找到对应实体;
    6. I/ O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
    文件描述符。关于i/o
    7.记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
    记账信息给操作系统的调度模块(算法)使用,能较为均衡的调度每个进程,获取CPU资源。
    8.上下文数据进程执行时处理器的寄存器中的数据。
    CPU里有一套寄存器,用来保存每个进程的临时数据,CPU规定在一个时间片后会切换下一个进程,为了保护上下文数据,CPU的寄存器的数据会保存到PCB里,当切换回来时,再把上下文数据恢复。通过CPU的来回切换,用户能感受到多个进程同时在运行。

    在这里插入图片描述
    9.其他task_struct结构体信息

    查看进程

    进程的信息可以通过 /proc 系统文件夹查看 ,进程创建时,操作系统会以当前pid为目录名,把进程的相关信息保存到该目录下,进程销毁该目录也销毁。
    在这里插入图片描述
    cwd:当前进程工作目录路径。
    exe:启动执行程序的文件路径。
    注意:这些文件不属于磁盘文件,而是内存文件。
    大多数进程信息同样可以使用top和ps这些用户级工具来获取。

    fork初识

    fork是用来创建子进程。创建子进程那么系统上就多了一个进程,
    1、默认情况下,子进程会“继承”父进程代码和数据,原因很简单,父进程的代码和数据是向磁盘上获取的,而子进程没有。
    2、子进程内核数据结构task_struct也会以父进程为模版,初始化子进程task_struct.

    如何理解父子继承?

    继承代码:代码只有一份,并且代码是不可修改的。
    继承数据:进程之间是具有独立性的,默认情况下数据是共享的,但是如果要进程对数据的修改,那么需要通过“写时拷贝”来完成进程数据的独立性。

    通过fork()创建进程

    头文件:
    参数:无参
    返回值:失败返回<0;成功时,给子进程返回0,给父进程返回子进程pid

    #include 
    #include 
    #include 
    int main()
    {
    	int ret = fork();
    	if(ret < 0)
    	{
    		perror("fork");
    		return 1;
    	}
    	else if(ret == 0)
    	{ 	//child
    		printf("I am child : %d!, ret: %d\n", 	getpid(), ret);
    	}	
    	else
    	{ 	//father
    		printf("I am father : %d!, ret: %d\n", getpid(), ret);
    	}
    	sleep(1);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    如何理解fork()函数有两个返回值?

    pid_t fork()
    {
    	// 创建进程代码
    	// ……
    	// ……
    	// ……
    	return xxx;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    父进程创建了新进程,新进程继承了父进程代码,那么父子进程都会往下执行,自然就有了两个返回值,至于为什么给子进程返回0,给父进程返回子进程pid,一开始我还以为是return getpid(),结果不是我想的那样;父进程创建子进程目的就是为了让子进程干活,父进程与子进程是1:n的关系,父进程要获取子进程pid达到能控制子进程,而子进程只需要通过getppid即可获得。

    如何理解两个返回值的设置?

    返回值由同一块空间接收,数据是共享的,但是进程具有独立性,那么谁先写入,那么谁就要“写时拷贝”。

    进程状态

    进程状态的意义:方便操作系统快速判断进程,完成特定的功能,比如调度,本质是一种分类。

    为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。下面的状态在kernel源代码里定义:

    static const char * const task_state_array[] = {
    "R (running)", /* 0 */
    "S (sleeping)", /* 1 */
    "D (disk sleep)", /* 2 */
    "T (stopped)", /* 4 */
    "t (tracing stop)", /* 8 */
    "X (dead)", /* 16 */
    "Z (zombie)", /* 32 */
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    运行状态-R

    一个进程处于运行状态时不一定正在占用CPU资源,一个进程在满足被CPU调度的条件下会被放在run_queue里,在队列里准备给CPU调度,这就叫进程处于运行状态。

    状态演示:

    int main()
    {
    	while(true)
    	{};//死循环
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    浅度睡眠状态-S(可中断睡眠)

    当我们完成某种任务时,任务条件不具备,需要进程进行某种等待,可能等待网卡,磁盘,显示器,等外设资源时还有sleep命令,这时候进程处于S状态。
    ps:千万不要以为进程只会等待CPU资源。
    我们把运行状态的进程从run_queue放到等待队列中,就叫做挂起(阻塞)。
    从等待队列,放到运行队列,然后被CPU调度就叫做唤醒进程。

    等待队列与运行队列有什么区别?

    运行队列:等待着CPU的资源。
    等待队列:等待着外设资源,或者是被某种方式被限制到等待队列里(例如:sleep() )。当进程等到某种资源时,就会被放到运行队列里,然后等待CPU调度,访问某种资源。
    所以:进程在某种队列里就处于某种状态。
    在这里插入图片描述状态演示:

    int main()
    {
    	while(true)
    	{
    		printf("hello world\n");
    	};//死循环
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述大部分情况处于S状态,是因为大部分时间在等待IO设备准备就绪,当IO准备就绪后,进程放到运行队列被CPU调度然后往外设写入。

    深度睡眠状态-D(不可中断睡眠)

    一个进程处于深度睡眠状态(disk sleep),表示该进程不会被杀掉,即便是操作系统也不行,只有该进程自动唤醒才可以恢复。该状态有时候也叫不可中断睡眠状态(uninterruptible sleep),处于这个状态的进程通常会等待IO的结束。
    有一种场景: 进程要求从内存中写入数据到磁盘,此时进程处于休眠状态,操作系统发现进程什么事都不干浪费资源,操作把进程杀掉,那么磁盘完成工作以后会向进程汇报,但是进程已死,万一磁盘写入失败,那么失败的结果没人知道及处理,所以就有了D状态,一个D状态一个爷,如果系统中存在大量的爷进程那么系统会宕机。

    暂停状态-T

    在Linux当中,我们可以通过发送SIGSTOP信号使进程进入暂停状态(stopped),发送SIGCONT信号可以让处于暂停状态的进程继续运行。

    暂停状态与等待状态的区别?

    等待状态期间还会有部分数据可以被修改。
    暂停状态不会有数据被修改。

    状态演示:

    一个程序正在运行, 我们给该进程发送 19信号。
    kill -19 进程pid
    程序继续运行
    kill -18 进程pid
    
    • 1
    • 2
    • 3
    • 4

    注意:完成暂停及继续操作后进程会被切换到后台,我们需要kill -9 pid 才能杀死进程。
    关于信号部分,进程信号章节会讲解。

    僵尸状态&死亡状态

    死亡状态:只是一个回收状态,当一个进程的退出信息被读取后,该进程所申请的资源进程相关的内核数据结构+你的代码和数据就会立即被释放,该进程也就不存在了,所以你不会在任务列表当中看到死亡状态(dead)。
    僵尸状态
    当进程被某种因素终止后,会先进入僵尸状态,该进程不会立马被释放,而是供操作系统或是其父进程进行辨别退出原因。

    所以说进程的死亡顺序是先进入僵尸状态然后死亡状态。

    状态演示:

    #include 
    #include 
    #include 
    int main()
    {
    	int ret = fork();
    	if(ret < 0)
    	{
    		perror("fork");
    		return 1;
    	}
    	else if(ret == 0)
    	{ 	//child
    		while(true)
    		{
    		 	printf("I am child : %d!, ret: %d\n", 	getpid(), ret);
    		    sleep(1);
    		}
    	}	
    	else
    	{ 	//father
    		while(true)
    		{
    			printf("I am father : %d!, ret: %d\n", getpid(), ret);
    			sleep(1);
    		}
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    父子进程不断的打印,父子当前处于S或者R状态,我们给子进程发送9号信号杀死子进程,

    在这里插入图片描述
    子进程进入僵尸状态,父进程没有对子进程由任何的处理操作例如等待操作,这个后面会学,所以子进程一直处于僵尸状态。

    僵尸进程的危害:

    • 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
    • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说, Z状态一直不退出, PCB一直都要维护?是的!
    • 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构
    • 对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
    • 内存泄漏?是的!

    孤儿进程

    父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
    父进程先退出,子进程就称之为“孤儿进程”,孤儿进程被1号init进程领养,当然要有init进程回收喽。

    进程优先级

    基本概念

    cpu资源分配的先后顺序,就是指进程的优先权(priority)。
    优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
    还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。

    为什么要有优先级?

    资源少,让优先级高的享受更多的资源。

    查看系统进程

    在这里插入图片描述
    我们很容易注意到其中的几个重要信息,有下:
    UID : 代表执行者的身份
    PID : 代表这个进程的代号
    PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
    PRI :代表这个进程可被执行的优先级,其值越小越早被执行
    NI :代表这个进程的nice值

    PRI & NI

    PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高。
    那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=80+nice
    这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
    所以,调整进程优先级,在Linux下,就是调整进程nice值,nice其取值范围是-20至19,一共40个级别。

    nice值为什么是一个相对较小的一个范围?

    优先级再怎么设置,也只能是一种相对的优先级,不能出现绝对的优先级,否则会出现很严重的进程“饥饿问题”。也就是部分进程优先级低,没有或者极少数能获取CPU资源。
    调度器:较为均衡的让每个进程享受到CPU资源。例如:A、B、C进程,在一个时间内,分别获取CPU资源的次数,50,40,30。

    用top命令更改已存在进程的nice:

    1、top
    2、进入top后按“r”–>输入进程PID–>输入nice值
    
    • 1
    • 2

    其他概念

    竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。

    独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰

    并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行

    并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。

    环境变量

    基本概念

    环境变量本质就是 操作系统在内存/磁盘文件中开辟的空间,用来保存系统相关的数据。

    环境变量的用途

    为什么 ls 命令不用带路径?

    因为有环境变量PATH,系统会通过该变量去查找 ls指令程序。
    打印PATH环境变量:

    echo $PATH
    
    • 1

    打印结果:

    /usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/BBQ
    
    • 1

    用冒号隔开的一条一条搜索路径。

    常见环境变量

    PATH : 指定命令的搜索路径

    HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)

    SHELL : 当前Shell,它的值通常是/bin/bash。

    和环境变量相关的命令

    1.echo: 显示某个环境变量值
    2. export: 设置一个新的环境变量
    3. env: 显示所有环境变量
    4. unset: 清除环境变量
    5. set: 显示本地定义的shell变量和环境变量

    本地变量 & 环境变量

    系统上还存在一种变量,是与本次登录(session)有关的变量,只在本次登录有效。
    本地变量的定义:

    myval = 12345
    set | grep myval
    
    env | grep myval
    export myval
    env | grep myval
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    export myval :导入变量myval到环境变量列表里。

    环境变量的组织方式

    在这里插入图片描述每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串。每个进程都有它所运行的的一个环境变量,环境变量一般是存放在内存的用户空间的一个环境变量表中,这个环境变量表是在进程生成时,从父进程的环境变量表中拷贝一份。

    环境变量的获取方式

    方式一、通过系统调用获取或设置环境变量(推荐写法)

     char *getenv(const char *name)
     // name:环境名
     // 返回值:环境名对应值
    
    • 1
    • 2
    • 3
    #include 
    #include 
    int main()
    {
    	printf("%s\n", getenv("PATH"));
    return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    相当于:echo $PATH

    方式二、main函数的第三个参数
    main函数的第三个参数接收的实际上就是环境变量表,我们可以通过main函数的第三个参数来获取系统的环境变量。

    int main(int argc,char* argv[],char * envp[])
    
    • 1

    argc: 传入的选项个数
    argv:传入的选项列表,末尾null
    envp:传入环境变量列表,末尾null
    例如:ls -a -l
    argc=2,argv[]= “-a”, “-l”

    #include
    int main(int argc,char* argv[],char * envp[])
    {
    	int i=0;
    	while(envp[i])
    	{
    		printf("envp[%d]:%s\n",i,envp[i]);
    		i++;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    方式三、通过第三方变量environ获取

    #include 
    int main(int argc, char *argv[])
    {
    	extern char **environ;
    	int i = 0;
    	for(; environ[i]; i++)
    	{
    		printf("%s\n", environ[i]);
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在 使用时 要用extern声明。

    环境变量通常是具有全局属性的

    环境变量通常是具有全局属性,是因为环境变量可以被子进程继承下去。
    我们在命令行上启动的进程,父进程都是bash,bash的父进程是操作系统。在bash被启动时,就把环境配置好,包含环境变量等等,例如通过配置文件/etc/bashrc。

    验证父子进程会继承环境变量:

    // code.c
    #include
    #include
    int main()
    {
      printf("%s\n",getenv("my_val")) ;
      return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    进程地址空间

    看如下代码:

    #include 
    #include 
    #include 
    #include 
    int g_val = 100;
    int main()
    {
        //数据是各自私有一份(写时拷贝)
        if(fork() == 0){
            //child
            int cnt = 5;
            while(cnt){
                printf("I am child, times: %d, g_val = %d, &g_val = %p\n", cnt, g_val, &g_val);
                cnt--;
                sleep(1);
                if(cnt == 3){
                    printf("##################child更改数据#########################\n");
                    g_val = 200;
                    printf("##################child更改数据done#########################\n");
                }
            }
        }
        else{
            //parent
            while(1){
                printf("I am father, g_val = %d, &g_val = %p\n", g_val, &g_val);
                sleep(1);
            }
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    运行结果:
    在这里插入图片描述
    代码分析:全局变量g_val被写时拷贝,在物理内存中应该有两份g_val,可是child更改数据后,父子进程的g_val的值不一样,但地址却是一样的。
    说明我们获取的地址并不是物理内存上的地址。
    实际上,我们在语言层面上打印出来的地址都不是物理地址,而是虚拟地址。物理地址用户一概是看不到的,是由操作系统统一进行管理的。

    进程虚拟地址空间初识

    进程地址空间分布图:
    在这里插入图片描述

    每个进程都有一张进程地址空间,它是操作系统给进程画的一张大饼,让进程以为自己独占物理内存。
    进程地址空间在内核中是一个数据结构类型 ,具体进程的地址空间变量 struct mm_struct{}。

    mm_struct结构体区域的划分

    大饼是可以通过数据的方式进行画大饼!生活中处处是大饼,银行,亿万富翁私生子,父母的代保管的压岁钱等等。

    划分区域?

    划分区域就是区域的开始到区域的结束。从高地址到低地址就像一把尺子,我们可以通过尺子的刻度来划分区域。
    在这里插入图片描述虽然这里只有start 和 end 但是每个进程都认为 mm_struct 代表了整个内存 且所有的地址为0x0000……00 ~ 0xFFF……FF,且每个进程地址空间的划分是按照4GB空间划分的,也就是每个进程都认为自己拥有4GB。

    什么是虚拟地址?

    地址空间上进行区域划分时,对应的线性位置虚拟地址。
    在这里插入图片描述

    mm_struct结构体与物理内存进行关联

    页表和MMU硬件的作用是将虚拟地址转换为物理地址。
    在这里插入图片描述页表详细讲解

    进程虚拟地址空间与页表的作用

    注意:对软硬件的访问都必须贯穿操作系统。

    1、通过添加一层软件层,完成有效的对进程操作内存进行风险管理,本质目的是为了,保护物理内存以及各个进程的数据安全。

    添加软件层就是os的助理,帮助OS。
    例如:如果没有进程虚拟空间与页表,那么程序之间就会对内存不发觉的乱用,还有就是页表其实是一种权限,例如我们的语言层定义const变量,该变量只能读,一但我们修改,就会被页表检测出来。

    2、将内存申请和内存使用的概念,在时间上划分清楚,通过虚拟地址空间,来屏蔽底层申请内存的过程,达到进程读写内存和os进行内存管理操作,进行软件上面的分离。

    例如:我们申请的1000字节有可能不会立马在物理内存中申请,而是在读写的时候才申请。在os的角度,如果空间立马给你,就意味着,整个系统会有一部分空间,本来可以被别使用的,现在却被闲置这。
    注意:这种操作是基于缺页中断进行物理内存申请。
    我们也不用担心os系统申请不到空间,os会通过内存管理算法来给进程想办法开辟空间。例如:把一些进程闲置的内存数据放到磁盘上,然后把空间让出来。

    3、站在CPU和应用层的角度,进程统一可以看做统一使用4GB空间,而且每个空间区域的相对位置,是比较确定的。(统一的视角看待进程)

    例如:CPU是如何知道我们进程的起始代码在哪里?只要找到入口就可以按顺序执行了。每个进程的代码都在内存里,CPU每次都要维护这起始入口去找,效率低。
    那么有了进程地址空间后,CPU只需要有一个特定的地址,每次的都使用该地址去查找,从磁盘加载到内存,页表只需要获取到加载到内存的物理地址,那么CPU就能轻松找到。其他区域也类似。
    在这里插入图片描述
    总结:OS最终这样设计的目的,达到一个目标:每个进程都认为自己是独占系统资源的!
    程序地址空间补充

    最后解析:
    在这里插入图片描述

    为什么父子g_val是两个相同的地址,原因就是进程虚拟地址空间,在语言上我们用的都是虚拟地址,当创建子进程时,子进程以父进程为模版创建,所以此时,子进程的页表地址空间与父进程基本一样,当子进程对g_val 进行修改时,os检测到要进行写时拷贝,开辟新空间复制g_val值,改变子进程页表对g_val的物理映射。父子进程对g_val的虚拟地址从未改动过,所以打印的g_val的地址是一样的。
    在这里插入图片描述
    补充:
    所有的只读数据,一般在物理内存只保留一份。原因:操作系统维护一份是成本最低的。

    int main()
    {
    	const char* p="hello";
    	const char* str="hello";
    	printf("%p\n",p);
    	printf("%p\n",str);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    运行结果:打印同样的地址。

  • 相关阅读:
    flask入门(四)前后端数据传输
    JAVA - 网络编程
    C++学习笔记(2)--程序流程结构、数组、函数
    jclasslib :java class字节码jvm指令分析
    search——Bloom Filter
    P1104 生日
    JS中数组随机排序实现(原地算法sort/shuffle算法)
    图片编辑用什么软件?快把这些软件收好
    【银河麒麟系统】备份还原工具显示“备份分区空间不足,请删除过期或者不需要的备份”解决方法
    HarmonyOS 如何使用异步并发能力进行开发
  • 原文地址:https://blog.csdn.net/weixin_58004346/article/details/125984256