讲了这么久的linux基础,终于要迎来我们第一个大章节了,当然在介绍进程的相关概念时候我们先来讲讲操作系统的基本概念以方便大家理解。
1.操作系统的基本概念
1.1经典老三样–冯诺依曼结构体系
在谈及操作系统的最基本概念前,我们不妨来回顾一下大名鼎鼎的冯诺依曼结构体系,相信大家应该也不陌生,下面就放上一张图带大家体会一下
当然直接放上一张图大家估计也看得云里雾里的,我们不妨结合现实里的一些设备来帮助大家加以理解。
-
输入设备:键盘,磁盘,网卡,显卡等。
-
输出设备:显示器,磁盘,网卡,显卡音响等。
-
存储器(内存)------这一体系的核心
-
运算器&&控制器(cpu)
截至目前为止我们对计算机的理解都停留在了硬件上(大佬可以装作没看见),而操作系统是这与硬件打交道所必不可少的东西,所以学习有关操作系统的基础概念还是相当重要的。
1.2操作系统的一些概念性总结
那么讲了这么多,到底什么是操作系统呢?
操作系统是一款软件,专门针对软硬件资源进行管理工作的软件 对上来说:给用户提供稳定的,高效的,安全的运行环境
对下来说: 管理好软硬件资源
那么究竟是如何管理的呢?
核心六个大字:先描述,在组织 (这里先记住就行,后面会娓娓道来,到时候在回过头看这六个字,保你难忘)
这里先给大家放上一张图:(图中有些内容不懂别着急后面都会一一讲解,这里是为了大家有个大局关的认识)
2.进程的基本认识
为了更好的认识到操作系统的组成原理我们在这里先学第一个重要概念------进程。
2.1进程的一些基本认识
大家可以想想当你在windows操作系统下运行一些软件,或者你在任务管理器看到那些正在运行的一些软件时,有没有疑惑这些运行着的东西是什么?没错就是我们的进程
而我们的进程有非常多的相关属性,所以先要描述。如何描述呢?
描述进程的结构体–PCB–进程控制块这一概念便呼之欲出
2.2操作系统的管理
首先在这里呢先给大家树立一个基本概念:OS(操作系统)是不信任任何用户,但是它又不得不为用户提供服务。如何做到的呢?
观察上面那幅图可知,操作系统和用户之间有一套系统调用接口(其实本质上就是函数),一些大佬会对这些系统调用接口进行封装,以第三方库或者语言的方式呈现。
2.3进程的本质
有了上面这些前置知识,我们就可以大概了解到进程的实质了。
依旧拿我们上面提到的六个大字来讲------先描述,在组织
描述本质上就是操作系统要创建一块描述进程的结构体—PCB,这结构体中包含了许多有关信息,然后操作系统会采取一些方式来对这些进程管理。而这个pcb在Linux下是名为task_struct的结构体,其内部属性大致如下:
-
标识符: 描述本进程的唯一标示符,用来区别其他进程。
-
状态: 任务状态,退出代码,退出信号等。
-
优先级: 相对于其他进程的优先级。
-
程序计数器: 程序中即将被执行的下一条指令的地址。
(具体运用:比如代码一行行的执行,计数器里存储的pc指针会控制代码下次执行哪一行)
-
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
-
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
-
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
-
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
(具体关联性:os有个调度模块,可以较为均衡的调度每个进程(本质就是在获取cpu资源),而这一模块本质就是利用了这个记账信息)
-
其他信息。
本质上来说我们启动程序的过程,就是系统上创建进程的过程。
2.4在linux下查看进程的一些有关信息
输入如下指令:ps axj | head -1 && ps ajx | grep "可执行文件名"
实例演示:
关于这条指令的一些解释:第一个子命令是 ps axj | head -1
,它的作用是列出系统中所有进程的信息,并且只显示第一行的信息。具体来说,ps
是一个用于显示进程信息的命令,axj
选项表示列出所有进程的信息,包括进程的状态、PID、父进程的PID等;head -1
命令表示只显示第一行的信息,也就是进程信息的表头。
第二个子命令是 ps ajx | grep "test6"
,它的作用是列出所有包含 “test6” 为文件名的进程信息。具体来说,ps ajx
命令也是列出所有进程的信息,但是它的输出格式和 ps axj
不同;grep "test6"
命令表示在 ps ajx
命令的输出中查找名为"test6"的进程。
整个指令的作用是先列出系统中所有进程信息的表头,然后再列出所有名为 "test6"的进程信息。
3.进程的进一步认识
3.1从程序的角度认识进程
大家可以看我上面这张图,其实程序运行的本质就是将原本与文件有关的内容加载到内存当中去,操作系统为了此程序的运行管理创造了一块叫PCB的进程控制块
至此关于进程的总结是:进程 = 程序的文件内容 + 相关的数据结构 (这里懂得多的大佬不要插嘴,我们循序渐进讲,后面会对最终结论进行补充)
那么这些创建的进程又是如何与这些程序的相关内容相关联的呢,请看如下的图片:
其实不难想象,其采取的都是这种一一对应的关系,而pcb因为要管理这些相关信息,其都存储了有关关联信息的一些内容方便来找到它们(具体如何找到,这里涉及到另一个知识点就不在这里细说了)
3.2进程标识符
首先从上面可以看出每一个进程从某种角度上因该都是独立的存在,那么是否有一种属性来描述进程呢?
pid就是这样的一种存在(存在pcb中)
而ppid为该进程父进程的pid
在命令行上运行的命令,基本上父进程都是bash。
如果要获得一个进程的pid我们可以使用一个接口函数getpid()
,杀死使用一条指令kill -9 pid
3.3进程的快速切换
进程的代码有可能不是短时间能运行完成的,操作系统为了不让单个进程一次跑太长时间,规定了时间片这样一个概念(每个进程单词可以跑的时间)。
在单cpu的情况下,理论上来说一次只能运行一个进程,你可能感觉每个程序都在跑,但是本质上这是通过进程间的快速切换完成的。
因此进程可能存在大量的临时数据,暂时存储在cpu的寄存器中,可一个cpu只有一个寄存器,所以其采取了一个叫做上下文切换的操作。
上下文切换是指操作系统在将CPU从一个进程切换到另一个进程时,保存当前进程的上下文(包括寄存器状态、进程状态等等),并加载另一个进程的上下文。这样,当操作系统再次切换回先前的进程时,它可以恢复先前的进程的状态,并继续执行该进程,就好像它从未中断过一样。
当然这里要彻底理解这个上下文切换技术的实际原理前置基础条件成本过高,这个会放到后面在去讲解
3.4插入一些额外的小知识
批量注释:在normal模式下,ctrl +v+j+大写I +// +ESC
第二种查看进程的方式:ls /proc (这个/proc是linux下默认带的一个目录,存储在内存中)
4.fork讲解
4.1基本概念
fork是一个系统调用函数,其作用是创建一个子进程,在默认情况下,会"继承"父进程的代码和数据,这个新的进程控制块会被标记为子进程,并分配一个新的进程ID,其内核数据结构task_struct也会以父进程为模板来创建。
4.2写时拷贝
在windows当中一个个软件(进程)不难观察到都具有独立性,可如果父子进程相互公用代码和数据如果父进程被修改那么其创建的子进程也会受到影响,为了解决这一问题,数据会通过一种技术”写时拷贝“来完成进程数据的独立性。
如图所示当我父进程将要受到更改的时候,系统会自动拷贝一份原来的数据和代码,让父进程修改那份拷贝过来的。
4.3两个返回值
fork函数创建子进程成功的时候会有两个返回值(如果失败会返回一个小于0的数):
给父进程返回子进程的pid
给子进程返回0
4.4重点例子理解
如下代码是我在mytest.c文件中写入的
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Before fork\n");
pid_t pid = fork();
if (pid == 0) {
printf("Child process\n");
} else {
printf("Parent process\n");
}
printf("After fork\n");
}
如上为运行结果
首先父进程打印了一个”Before fork“,接着创建了一个子进程,这里我们看到程序先运行完的是父进程的内容(这个运行顺序不是一定的,不同情况下会有变化),接着运行完了子进程的内容,但是这个子进程并没有运行”Before fork"这一行代码(子进程创建的本质就是在创建一个类似于mytest.c的文件并运行),这是为什么呢?
原先我们就提到过,pcb中有个叫程序计数器的来管理代码应该运行到哪一行,程序计数器当中的参数会随着代码运行的改变而改变,所以此时我们的子进程此时用的程序计数器中的参数恰巧就和父进程运行到fork这一行代码时一样,所以子进程并不会运行在调用fork()函数前的代码,但是实质上确实是继承了父进程完整的代码和数据。
5.进程的不同状态
5.1基本概念
在进程控制块pcb中,存储着进程的状态。为什么要搞这种状态设计呢?
其存在意义就是为了OS快速判断进程,以完成特点的功能。
实质上状态的运用就是在进程运行时,可能因为某种需要,会将不同状态的进程放入对应不同状态的队列中。
5.2不同状态的分类
-
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列(run-queue)里。
-
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
(interruptible sleep))。(可以被杀掉)(其实就是放入等待队列里等着)
-
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的
进程通常会等待IO的结束。(无法被杀掉)
-
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
-
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态(本质为了回收进程资源)
5.3一些小概念的解释
把运行状态的进程控制块放入放入等待队列里叫做挂起等待(阻塞)
从等待队列放到运行队列,被cpu调度被称为唤醒进程
5.4Z(zombie)-僵尸进程
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)
没有读取到子进程退出的返回代码时就会产生僵死(尸)进程僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
其存在的本质就是为了获取进程退出的原因
5.5孤儿进程
父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
父进程先退出,子进程就称之为“孤儿进程”
孤儿进程会被1号init进程(操作系统)领养
6.进程优先级(PRI)
6.1基本概念
进程的优先级(Process Priority)是指操作系统调度进程时,优先考虑调度哪个进程执行的程度。
6.2查看优先级的操作
在linux下输入以下指令ps -l
会出现以下几个参数:
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
6.3pri和ni的具体讲解
PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高。
那NI呢?
就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行所以,调整进程优先级,在Linux下,就是调整进程nice值。(nice其取值范围是-20至19,一共40个级别)
6.4修改进程优先级的命令
top 对应pid
进入top后按“r”–>输入进程PID–>输入nice值
7.环境变量
7.1PATH(环境变量)
首先在正式开讲之前我想请大家思考一个问题。在前面我们就提到过命令的本质也就是文件,操作系统提供的shell为帮助我们解析这些文件从而调用内核,那么正如我们前面的操作运行一个文件的指令一般是./test
,这个’./'实际就是确定文件地址的过程。可我们的指令从头到尾都没有写明该文件的地址,其又是怎么确定地址的呢?
原来,就是因为环境变量path的存在(里面包含了所有的路径)
7.2测试PATH
echo $NAME //NAME:你的环境变量名称(查看环境变量)
- 创建hello.c文件,并完成编译
- 将我们的程序所在路径加入环境变量PATH当中,
export PATH=$PATH:hello程序所在路径
- 这样./hello和hello都能执行
7.3常见的环境变量及其相关命令
PATH : 指定命令的搜索路径
HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
SHELL : 当前Shell,它的值通常是/bin/bash
相关命令:
-
echo: 显示某个环境变量值
-
export: 设置一个新的环境变量(基本使用语法:export NAME=value)
基本补充说明:
export 命令将环境变量创建在当前 shell 会话中,这意味着:
- 子进程可以继承这个环境变量
- 当当前 shell 会话结束后,这个环境变量就消失了
但是,如果你在 bash 的配置文件(如 ~/.bashrc)中设置环境变量,那么:
- 每次开启一个新 shell 时,这个环境变量都会被设置
- 这个环境变量会持续存在,直到你手动unset 它
-
env: 显示所有环境变量
-
unset: 清除环境变量
-
set: 显示本地定义的shell变量和环境变量
7.4环境变量的概念性总结
环境变量的本质是os在内存/磁盘文件中开辟空间,用来保存系统相关的数据。
7.5环境变量的组织及存在方式
- 键值对形式:环境变量以键值对的形式存储,如
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
。其中,PATH
是键,后面的字符串是值,表示可执行文件搜索路径。 - 继承:当一个进程启动时,它会继承其父进程的环境变量。这意味着一旦环境变量被设置,它们可以在当前会话及其子进程中使用。
- 全局和用户级别:环境变量分为全局变量和用户级别变量。全局变量对系统上所有用户都可见,而用户级别变量仅对特定用户可见。全局环境变量通常在
/etc/environment
和/etc/profile
文件或其包含的文件中定义,而用户级别的环境变量通常在用户的家目录下的.bashrc
,.bash_profile
或.profile
文件中定义。(其实前面有提到过由系统创建的这些经常其子进程都是bath,所以它们共用一套相同的环境变量)
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串
在程序启动时,操作系统将当前用户的环境变量从 shell 传递给新创建的进程,进程会将这些环境变量存储在自己的内存中,以便在运行过程中使用。
7.6以代码的方式获取环境变量
先来看如下代码:
#include <stdio.h>
int main(int argc, char *argv[], char *env[]) //操作系统会传递参数给这些数组
{
for(int i = 0; env[i]; i++)
{
printf("%s\n", env[i]);
}
return 0;
}
argc:数组的个数
argv:数组名 //里面放的是一些指令的选项
environ:数组名,里面存了系统的环境变量
tips:
主函数有三种形式:
1.没有参数的main函数:int main (void)
2.带有参数的main函数:int main(int argc ,char*argv[])
3.或者 int main(int argc ,char*argv[], char * env[])
而当你在你的程序中定义了没有参数的main函数时操作系统是不会给你的主函数传递参数的。
通过第三方变量environ来获取:
#include <stdio.h>
int main(int argc, char *argv[])
{
extern char **environ;
int i = 0;
for(; environ[i]; i++){
printf("%s\n", environ[i]);
}
return 0;
}
关于这段代码的一些小解释:
1.extern关键字的作用是用于声明environ变量。extern关键字用于告诉编译器该变量是在其他地方定义的,而不是在当前文件中定义。
2.environ是一个指向环境变量的指针(由c标准库定义)
因此程序在编译的时候会链接c标准库,找到environ
7.7通过系统调用获取和设置环境变量
putenv
和 getenv
是 Linux 系统中两个用于操作环境变量的 C 语言库函数。环境变量是在操作系统中存储的键值对,通常用于存储系统设置和配置信息。这些函数可以在 <stdlib.h>
头文件中找到。
getenv
getenv
函数用于从环境变量中读取一个值。它的原型如下:
char *getenv(const char *name);
参数:
name
:要检索的环境变量的名称。
返回值:
-
如果找到指定的环境变量,则返回一个指向其值的指针。
-
如果环境变量不存在,则返回
NULL
。示例:
#include <stdio.h> #include <stdlib.h> int main() { const char *var_name = "PATH"; char *path_value = getenv(var_name); if (path_value) { printf("The value of %s is: %s\n", var_name, path_value); } else { printf("The variable %s does not exist.\n", var_name); } return 0; }
putenv
putenv
函数用于设置或修改环境变量。它的原型如下:int putenv(char *string);
参数:
string
:一个包含name=value
形式的字符串,其中name
是环境变量的名称,value
是要设置的值。
返回值:
-
如果成功设置环境变量,则返回
0
。 -
如果发生错误,则返回非零值。
注意:传递给
putenv
的字符串不应该是在栈上分配的,因为环境变量会直接引用这个字符串。你应该使用在堆上分配的内存或者静态字符串。示例:
#include <stdio.h> #include <stdlib.h> #include <string.h> int main() { const char *var_name = "MY_VARIABLE"; const char *var_value = "12345"; char *env_entry = malloc(strlen(var_name) + strlen(var_value) + 2); if (!env_entry) { perror("malloc"); return 1; } sprintf(env_entry, "%s=%s", var_name, var_value); if (putenv(env_entry) != 0) { perror("putenv"); free(env_entry); return 1; } printf("Successfully set %s to %s\n", var_name, var_value); char *retrieved_value = getenv(var_name); if (retrieved_value) { printf("Retrieved value: %s\n", retrieved_value); } else { printf("Failed to retrieve value for %s\n", var_name); } // 注意:在此示例中,我们不会释放 env_entry,因为它已成为环境变量的一部分。 // 在程序终止时,操作系统会自动清理这些内存。 return 0; }
在这个示例中,我们首先使用
malloc
在堆上分配内存来存储环境变量字符串。然后,我们使用sprintf
将name=value
格式的字符串写入分配的内存。接下来,我们使用putenv
设置环境变量。最后,我们使用getenv
检查环境变量是否已成功设置。
8.程序地址空间
8.1一些c语言篇章中内存存储的回顾
静态区通常包含:
-
全局变量和静态变量 - 这些变量在程序运行时一直占用内存,直到程序结束。
-
const常量 - const定义的常量在程序运行时被分配内存,并保持不变。
-
静态函数 - 静态函数可以访问静态变量和const常量,并且可以在没有实例化的情况下调用。
-
初始化的全局和静态变量 - 这些变量的值在程序启动时被初始化,并且在程序运行时一直占用内存。
-
string字面量 - 如"Hello"这样的字符串在程序运行时被分配内存,并保持不变。
-
常量表达式的结果 - 比如enum,constexpr等在编译时被计算并分配内存。
一个小问题的补充:
const char* p = "Hello"; const char* q = "Hello"
打印这两个变量的地址发现其地址都一样这是为什么呢?
在静态区有个叫字符常量区的专门存储这些字符,为了维护的低成本基本所有只读数据只有一份这是咋们在c语言文章当中讲到的内存分配的总结。可是这些真的是物理角度讲的内存吗?
根本不是。
8.2一个现象
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){ //child
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}else{ //parent
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
结果:
//与环境相关,观察现象即可
parent[2995]: 0 : 0x80497d8
child[2996]: 0 : 0x80497d8
修改后的代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){ //child,因为父进程sleep了子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取(正常谁先跑完不一定的看,具体得看操作系统)
g_val=100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}else{ //parent
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
结果:
//与环境相关,观察现象即可
child[3046]: 100 : 0x80497e8
parent[3045]: 0 : 0x80497e8
以上的代码大概是这样一个过程:在linux下,fork()创建了子进程,让子与父一起跑,然后在子进程中改变某一全局变量,发生写实拷贝,之后再次打印地址的时候发现子进程和父进程打印出该全局变量的地址是一样的。
8.3虚拟地址
为了解决上述的问题,我们得引进一个叫做虚拟地址空间的概念,直接上一张经典的图片让大家感受一下:
如何理解上述这张图呢,不要着急且听我一一道来。
当一个进程被创建的时候它会创建一个叫做mm_struct的结构体,里面存储的是每个进程的地址空间的有关数据,这样每个进程都会认为自己在独占物理内存。
mm_struct的源码:
struct mm_struct {
struct vm_area_struct * mmap; /* list of VMAs,指向线性区对象的链表头部 */
struct rb_root mm_rb; /* 指向线性区对象的红黑树*/
struct vm_area_struct * mmap_cache; /* last find_vma result 指向最近找到的虚拟区间 */
#ifdef CONFIG_MMU
/*用来在进程地址空间中搜索有效的进程地址空间的函数*/
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
/*释放线性区的调用方法*/
void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
#endif
unsigned long mmap_base; /* base of mmap area ,内存映射区的基地址*/
unsigned long task_size; /* size of task vm space */
unsigned long cached_hole_size; /* if non-zero, the largest hole below free_area_cache */
unsigned long free_area_cache; /* first hole of size cached_hole_size or larger */
pgd_t * pgd; /* 页表目录指针*/
atomic_t mm_users; /* How many users with user space?,共享进程的个数 */
atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1),主使用计数器,采用引用计数,描述有多少指针指向当前的mm_struct */
int map_count; /* number of VMAs ,线性区个数*/
struct rw_semaphore mmap_sem;
spinlock_t page_table_lock; /* Protects page tables and some counters,保护页表和引用计数的锁 (使用的自旋锁)*/
struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung
* together off init_mm.mmlist, and are protected
* by mmlist_lock
*/
unsigned long hiwater_rss; /* High-watermark of RSS usage,进程拥有的最大页表数目 */
unsigned long hiwater_vm; /* High-water virtual memory usage ,进程线性区的最大页表数目*/
unsigned long total_vm, locked_vm, shared_vm, exec_vm;
unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
unsigned long start_code, end_code, start_data, end_data; /*维护代码区和数据区的字段*/
unsigned long start_brk, brk, start_stack; /*维护堆区和栈区的字段*/
unsigned long arg_start, arg_end, env_start, env_end; /*命令行参数的起始地址和尾地址,环境变量的起始地址和尾地址*/
unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */
/*
* Special counters, in some configurations protected by the
* page_table_lock, in other configurations by being atomic.
*/
struct mm_rss_stat rss_stat;
struct linux_binfmt *binfmt;
cpumask_t cpu_vm_mask;
/* Architecture-specific MM context */
mm_context_t context;
/* Swap token stuff */
/*
* Last value of global fault stamp as seen by this process.
* In other words, this value gives an indication of how long
* it has been since this task got the token.
* Look at mm/thrash.c
*/
unsigned int faultstamp;
unsigned int token_priority;
unsigned int last_interval;
unsigned long flags; /* Must use atomic bitops to access the bits */
struct core_state *core_state; /* coredumping support */
#ifdef CONFIG_AIO
spinlock_t ioctx_lock;
struct hlist_head ioctx_list;
#endif
#ifdef CONFIG_MM_OWNER
/*
* "owner" points to a task that is regarded as the canonical
* user/owner of this mm. All of the following must be true in
* order for it to be changed:
*
* current == mm->owner
* current->mm != mm
* new_owner->mm == mm
* new_owner->alloc_lock is held
*/
struct task_struct *owner;
#endif
#ifdef CONFIG_PROC_FS
/* store ref to file /proc/<pid>/exe symlink points to */
struct file *exe_file;
unsigned long num_exe_file_vmas;
#endif
#ifdef CONFIG_MMU_NOTIFIER
struct mmu_notifier_mm *mmu_notifier_mm;
#endif
};
其实给了一段段区域的start开始位置(本质里面存了一个地址开始的数值),和end(同理),可是这样的结构并不能帮助我们真正的来存储数值,在此基础上我们引进了又一概念------页表和mmu
页表:
- 页表用于建立虚拟内存地址和物理内存地址的映射关系。
- 页表由页目录和页面表条目组成。页目录中的每个条目都指向一个页面表。页面表中的每个条目都包含一个物理内存页框号和一些标志位。
- 当CPU访问一个虚拟内存地址时,会先在页目录中查找对应条目,得到页面表的地址。然后在页面表中继续查找,最终得到物理内存页框号,从而找到真实的物理内存地址。
- 页表由内核维护,当有进程申请内存或释放内存时,内核会动态调整页表。
MMU(内存管理单元):
- MMU是CPU中的一部分,用于管理内存访问和地址转换。
- MMU通过使用页表来实现虚拟地址到物理地址的转换,从而实现虚拟内存。
- MMU还可以通过标志位来控制内存访问权限,实现内存保护。例如通过读/写/执行标志控制对应的访问权限。
- 当CPU访问内存时,MMU会先检查访问权限,如果允许访问,则会使用页表进行地址转换,得到真实的物理地址。否则会触发页面错误。
所以简而言之,页表是OS利用的机制,MMU是CPU提供的机制。二者配合可以实现虚拟内存和内存保护等功能。
这一机制有什么用处呢?
1.通过添加一层软件层,完成有效的对进程操作内存进行风险管理(权限管理),本质目的是为了,保护物理内存以及各个进程的数据安全。
2.将内存申请和内存使用的概念在时间上划分清楚,通过虚拟地址空间,来屏蔽底层申请内存的过程,达到进程读写内存和os进行内存管理操作,进行软件层面的分离
(如果申请1000个字节,能立马全使用吗?在os角度,本来有一部分空间是立马给别人使用的,我却需要给他闲置,所以此时我们需要进行缺页中断来进行物理内存的申请)
3.站在cpu和应用层角度,进程统一可以看做统一使用一块特点的空间,而且每个空间区域的相对位置是比较确定的。