写点什么

关于计算机面试重难点 之 操作系统,字节架构师有话说

  • 2021 年 11 月 11 日
  • 本文字数:10463 字

    阅读完需:约 34 分钟

僵尸进程在其父进程退出后转为孤儿进程。



如何解决僵尸进程:



通过 kill -9?杀掉其父进程,僵死进程就可以转为孤儿进程,进而被 init 进程回收;由于进程退出时会向父进程发送 SIGCHLD 信号,因此,可以在父进程捕获该信号并通过 wait 或 waitpid 释放子进程资源。


孤儿进程与僵尸进程


僵尸进程与孤儿进程


什么是死锁?死锁产生的必要条件?#


=================


死锁:在许多应用中进程需要以独占的方式访问资源,当多个进程并发执行时可能会出现相互等待对方所占用的资源而都无法继续向下执行的现象,此现象称为死锁


死锁产生的四个必要条件(发生死锁时,一定会有以下条件成立):


  • 互斥条件:一个资源只能被一个进程占有,进程应互斥且排他的使用这些资源。

  • 请求与保持条件:进程在请求资源得不到满足而等待时,不释放已占有的资源。

  • 不可剥夺条件:进程已经占有的资源,除非进程自己释放,其他进程不能强行剥夺 。

  • 循环等待条件:若干进程之间形成一种首位尾相接的环形等待资源关系。


处理死锁的基本策略和常用方法?#


================


预防死锁(破坏四个必要条件):


  • 并不是所有应用场景都可以破坏互斥条件。案例:SPOOLing 技术将一台独享打印机改造为可供多个用户共享的打印机。(破坏互斥条件 )

  • 当进程在运行前一次申请完他所需要的全部资源,在他的资源未满足前,不让他投入运行。(破坏请求和保持条件)

  • 给不同进程设置优先级,当某个高优先级进程需要的资源被其它进程占有的时候,可以由操作系统协助将想要的资源强行剥夺。(破坏不可剥夺条件)

  • 给资源编号,进程必须按照编号从小到大的顺序申请自己所需资源。(破坏循环等待条件)


避免死锁(银行家算法):


预防死锁的几种策略,会严重地损害系统性能。在避免死锁时,要施加较弱的限制,从而获得较为满意的系统性能。具有代表性的避免死锁算法是银行家算法


银行家算法的实质就是要设法保证系统动态分配资源后不会进入不安全状态,以避免可能产生的死锁。 即每当进程提出资源请求且系统的资源能够满足该请求时,系统将判断满足此次资源请求后系统状态是否安全,如果判断结果为安全,则给该进程分配资源,否则不分配资源,申请资源的进程将阻塞。


银行家算法所需数据结构


  1. Available[j] 向量:系统中可利用的各种资源数目

  2. Max[i, j] 矩阵:每个进程对每种资源的最大需求

  3. Allocation[i, j] 矩阵:每个进程已分配的各类资源的数目

  4. Need[i, j] 矩阵:每个进程还需要的各类资源数


其中三个矩阵间存在下述关系:


Need[i, j] = Max[i, j] - allocation[i, j]


银行家算法流程


设 Request 是第 i 个进程 P 的请求向量,如果 Request[j] = K,表示进程 P 需要 K 个 j 类型的资源。当 P 发出资源请求后,系统按下述步骤进行检查:


  1. 若 Request[j] <= Need[i, j],转向 2,否则认为出错(因为它所需的资源数目已超过它所宣布的最大值)。

  2. 若 Requesti[j] <= Available[j],转向 3,否则须等待(表现为进程 P 受阻)。

  3. 系统尝试把资源分配给进程 P,并修改下面数据结构中的数值:


Available[j] = Available[j] – Request[j]


Allocation[i, j] = Allocation[i, j] + Request[j]


Need[i, j] = Need[i, j] –Request[j]


  1. 试分配后,执行安全性算法,检查此次分配后系统是否处于安全状态。若安全,才正式分配;否则,此次试分配作废,进程 P 等待。


安全性算法步骤:



检査当前的剩余可用资源是否能满足某个进程的最大需求,如果可以,就把该进程加入安全序列,并把该进程持有的资源全部回收不断重复上述过程,看最终是否能让所有进程都加入安全序列。



注:只要能找出一个安全序列,系统处于安全状态。当然,安全序列可能有多个。如果分配了资源之后,系统中找不出任何安全序列,系统就进入了不安全状态。


详解操作系统之银行家算法(附流程图)


死锁检测和死锁解除:


如果系统中既不采用预防死锁也不采用避免死锁的措施,系统就很有可能发生死锁。这种情况下,系统应当提供两种算法。


  1. 死锁检测算法:用于检测系统状态,确定系统中是否已经发生了死锁。所需数据结构:资源分配图,又叫资源有向图,如下:圆圈代表一个进程,方框代表一类资源,方框内的圆圈代表该类资源的一个单位的资源。从进程到资源的有向边为请求边,表示该进程申请一个单位的该类资源;从资源到进程的边为分配边,表示该类资源已有一个资源分配给了该进程。图中,进程 P1 已经分得了两个 R1 资源,请求了一个 R2 资源;进程 P2 分得了一个 R1 资源和一个 R2 资源,并又请求了一个 R1 资源。算法流程:尝试将满足运行条件的进程有向边消去以简化资源分配图,如果能简化说明系统占时没有出现死锁。如果此时系统的资源分配图是不可简化的,那么此时发生了系统死锁(死锁定理)。资源分配图简化实例:按照死锁定理中,找出的进程为 P1,因为它申请的资源可以被满足,说明(a)时刻没有发生死锁。

  2. 死锁解除算法:该算法可将系统从死锁中解脱出来。资源剥夺法:将一些死锁进程暂时挂起来,并且抢占它的资源,并将这些资源分配给其他的死锁进程 ,要注意的是应该防止被挂起的进程长时间得不到资源而处于饥饿状态。撤销进程法:强制撤销部分甚至全部死锁并剥夺这些进程的资源。撤销的原则可以按照进程优先级和撤销进程的代价高低进行。进程回退法:让一或多个进程回退到足以回避死锁的地步,进程回退时自愿释放资源而非被剥夺。这个方法要求系统保持进程的历史信息,并设置还原点。


死锁的检测及解除


进程间通信(IPC)的方式有哪些?#


==================


  1. 管道(pipe)管道可用于具有亲缘关系的进程间的通信,通常指父子进程之间;同时,管道是一种半双工的通信方式,数据只能单向流动。所谓管道,实际是内核管理的一串缓存,生命周期随进程的创建而创建,随进程的结束而销毁。在?Linux 系统(一种?UNIX 系统)中,可以在 C 代码中调用 pipe 系统调用创建管道并通信:#include?<unistd.h>?int?pipe(int?pipedes[2]);?// 创建管道?成功返回 0,失败返回-1,pipedes[0]指向管道的读端,pipedes[1]指向管道的写端。使用时,先创建管道并得到两个分别指向管道两端的文件描述符,父进程通过 fork 函数创建子进程,然后子进程也有两个文件描述符分别指向同一管道的两端;父进程通过?close(pipedes[0])?关闭管道读端,子进程通过 close(pipedes[1])关闭管道写端;父进程通过 write(pipedes[1], ... )系统调用往管道里写,子进程通过 read(pipedes[0], ...)系统调用从管道里读。(这里是父写子读,也可以反过来)备注:头文件?unistd.h?意为:unix std,其提供了访问?POSIX?操作系统?API 的功能。类似于?Windows 系统提供的?windows.h。POSIX,Portable Operating System Interface of UNIX,可移植操作系统接口,是?IEEE 为了方便在各种?UNIX 系统之间移植软件而定制的标准。示例:#include?<stdio.h>?#include?<unistd.h>?#include?<string.h>?int?main() {?int?pipedes[2];?if?(pipe(pipedes)) { perror("pipe() fail");?return?-1; }?pid_t?pid = fork();?if?(pid <?0) { perror("fork() fail");?return?-2; }?// child process?if?(pid ==?0) { close(pipedes[0]);?char?str[] =?"Hello, parent!";?//这里用没有发送结束符'\0'?int?len = write(pipedes[1], str,?strlen(str));?printf("write len : %d \n", len);?return?0;?// parent process?}?else?{ close(pipedes[1]);?char?buf[1024];?int?len = read(pipedes[0], buf,?sizeof(buf));?//手动添加上结束符?buf[len] =?'\0';?printf("read message from child process : %s , length %d \n", buf, len);?return?0; } }

  2. 命名管道(named pipe)命名管道克服了管道没有名字的限制,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。命名管道提供一个路径名与之关联,以文件形式存储文件系统中,只要进程可以访问该路径,就可以通过该命名管道相互通信。在 Linux 系统中,可以在 shell 中通过 mkfifo 命令创建命名管道;也可以在 C 源代码中通过调用 int mkfifo(const char * pathname, mode_t mode)系统调用创建,第一个参表示命名管道路径,第二个表示文件权限,通常为 0666(可读可写)。要借助该命名管道通信的进程在源代码通过 open、write、read、close 系统调用配合进行通信。(命名管道也是半双工的)示例:shell 中运行命令?mkfifo mypipe,会创建一个名为 mypipe 的命名管道。创建?namepipe_test_write.c?作为写端:#include?<stdio.h>#include?<string.h>?#include?<fcntl.h>?#include?<unistd.h>?// 一次传输的字符个数不超过 127 个字符,最后一位存'\0'?#define LEN 128?int?main() {?int?filedes = open("./mypipe", O_WRONLY);?char?buf[LEN] = {'\0'};?while(1) {?printf("please input message: \n");?int?i =?0;?while?(i < LEN -?1) {?char?ch = getchar();?if?(ch ==?'\n') {?break; } buf[i++] = ch; }?//手动添加字符串结束符?buf[i] =?'\0';?if?(strcmp(buf,?"quit") ==?0) {?break; }?int?len = write(filedes, buf,?strlen(buf));?printf("write len : %d \n", len);?printf("-----------------------------------\n"); } close(filedes);?return?0; } 创建 namepipe_test_read.c 作为读端:#include?<stdio.h>?#include?<string.h>?#include?<fcntl.h>?#include?<unistd.h>?int?main() {?intfiledes = open("./mypipe", O_RDONLY);?char?buf[128] = {'\0'};?while(read(filedes, buf,?sizeof(buf)) >?0) {?printf("read from pipe : \n%s\n", buf);?memset(buf,?'\0',?sizeof(buf));?printf("-----------------------------------\n"); } close(filedes);?return?0; } 编译后分别在两个终端中运行,无论谁先启动都行。当两个进程都启动后,写端才会打印出提示符,表示可以开始通信。在写端输入字符串并回车后,读端会读取并打印。当写端输入 quit 字符串时,通信结束。详情如下图:(编译运行环境:w10 下?wsl2(ubuntu 20.04))

  3. 信号(signal)信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式。信号用于通知接收进程有某种事件发生,接收到该信号的进程会相应的采取一些行动。在 Linux 系统中,信号在 signal.h 中定义,信号的名称都以 SIG 开头,如:SIGINT:终止信号 (ctrl + c)SIGQUIT:退出信号 (ctrl + \)SIGSTOP:暂停信号 (ctrl + z?)SIGSCONT:继续信号 (ctrl + z)SIGALRM:闹钟信号,常用作定时器 SIGCHLD:子进程状态改变,父进程收到信号 SIGKILL:杀死信号(?kill -9 pid)信号发送:通常,用户可以通过按键、调用 kill 命令、或在 C 源代码中调用 int kill(pid_t pid, int sig)系统调用来向另一个进程发送信号。信号处理:接收进程可以通过 signal、sigaction 函数注册对信号的处理方式。如:忽略(SIGKILL,SIGSTOP 不能被忽略)、默认处理、自定义处理。

  4. 消息队列(message queue)消息队列本质上是位于内核空间的链表,链表的每个节点都是一条消息,每一条消息都有自己的消息类型,这个消息类型是由发送方和接收方约定。不同于管道,从消息队列读取数据时不一定要以先进先出的次序读取,可以按消息的类型读取,借此可以实现提前查看紧急消息的功能。消息队列的生命周期是伴随着内核的,如果没有手动释放消息队列,他会直到操作系统关闭才释放。在 Linux 系统中,利用 C 语言,通过调用 int msgget(key_t key, int msgflg)系统调用创建(不存在时)消息队列并得到消息队列的唯一标识符。两个需要通信的进程中,只要传入 msgget 中的 key 相同,他们得到的消息队列标识符就相同。key 通常使用 key_t ftok(const char *pathname, int proj_id)系统调用来获得。进程获得消息队列唯一标识符后可以使用 int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)、ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg)系统调用分别向消息队列发送消息和从消息队列接收消息。示例:msg.h:定义消息格式 #ifndef _MSG_?#define _MSG_?typedefstruct?msg?{?long?msgType;?//必须是 long 型且是结构体第一个变量?charmessage[128];?//类型任意?//...可以有更多数据?} Msg;?#endifmsg_queue_test_write.c:发送端 #include?<stdio.h>?#include?<string.h>#include?<sys/ipc.h>?#include?<sys/msg.h>?#include?<errno.h>#include?"msg.h"?int?main() {?key_t?key = ftok("./",?2021);?if?(key ==?-1) { perror("ftok() fail");?return?-1; }?//创建(不存在时)消息队列并得到消息队列的唯一标识符, 0666 表示权限?int?msgqid = msgget(key, IPC_CREAT |?0666);?if?(msgqid ==?-1) { perror("msgget() fail");?return?-2; }?//发送类型 1 ~ 5 共 5 种类型的消息?for?(int?i =?1; i <?6; i++) { Msg msg; msg.msgType = i;?strcpy(msg.message,?"Hello : ");?//四字节 int 型转化为字符串长度最多为 12:一个符号 + 10 个数字 + 一个'\0'结束符?char?tmp[12] = {'\0'};?sprintf(tmp,?"%d", i);?//将 i 转化为字符串并拼接至 message_strcat(msg.message, tmp);?//最后一个参数:阻塞方式发送消息,如果消息队列没有空间接收新发送的消息则阻塞_?int?flag = msgsnd(msgqid, &msg,?sizeof(msg) -?sizeof(long),?0);?if?(flag ==?-1) {?printf("msgsnd(): send type: %d failed: %s", i, strerror(errno)); } }?return?0; }?msg_queue_test_read.c:接收端 #include?<stdio.h>?#include?<string.h>#include?<sys/ipc.h>?#include?<sys/msg.h>?#include?<errno.h>#include?"msg.h"?int?main() {?key_t?key = ftok("./",?2021);?if?(key ==?-1) { perror("ftok() fail");?return?-1; }?//创建(不存在时)消息队列并得到消息队列的唯一标识符, 0666 表示权限?int?msgqid = msgget(key, IPC_CREAT |?0666);?if?(msgqid ==?-1) { perror("msgget() fail");?return?-2; } Msg msg;?// 第二个参数:指明消息长度(不包含消息类型)?// 倒数第二个参数: 大于 0 时表示获取制定类型的消息,如果等于 0 表示获取最前面的消息?// 最后一个参数:如果消息队列为空就阻塞,直到读取到消息?int?len = msgrcv(msgqid, &msg,?sizeof(Msg) -?sizeof(long),?0,?0);?if?(len <?0) { perror("msgrcv() fail"); }?printf("receive %d length: %s \n", len, msg.message);?return?0; } 编译后在两个终端中分别开启读端和写端(无论先后),消息队列开始为空,如果先启动读端,读端会被阻塞,直到写端写了数据后,读端读取到数据了才会退出。该例中,写端一次写入五个消息,读端可以成功读取五次,第六次读取会被阻塞,直到消息队列又有新的消息到达。如下:(编译运行环境:ubuntu 20.14。在 wsl2(ubuntu 20.14)中编译可以通过,但运行时提示没有相应实现)以上代码并没有释放消息队列(msgctl(msgqid, IPC_RMID, 0)函数可以释放),因此,程序运行结束后,通过命令 ipcs -q 仍然可以看到我们创建的消息队列(系统重启后消失):最后,我们可以通过命令 ipcrm -q msqid 释放消息队列。消息队列

  5. 共享内存(shared memory)不同程序拥有各自独立的逻辑地址空间,不能相互访问。共享内存通过将一块物理内存映射到不同进程的逻辑地址空间,使得他们可以访问同一块物理内存,从而实现共享内存。访问共享内存区域和访问进程独有的内存区域一样快,读取和写入的过程中不用像管道和消息队列那在用户态与内核态之间拷贝信息,因此,共享内存是最快的进程间通信形式。由于多个进程共享同一块内存区域,为保证正确通信,需要借助同步机制,如:信号量,来进行同步。在 Linux 系统中,利用 C 语言,通过调用 int shmget(key_t key, size_t size, int shmflg)系统调用创建(不存在时)共享内存并得到共享内存的唯一标识符,其中 key 和消息队列中提到的 key 相同。进程获得共享内存唯一标识符后通过调用 void *shmat(int shm_id, const void *shm_addr, int shmflg)系统调用建立(attach)用户进程空间到共享内存的映射,得到指向共享内存的指针。根据该指针就可以利用系统读写函数向共享内存中写数据或者从共享内存读取数据。通信完成后,进程通过调用 int shmdt(const void *shmaddr)函数解除(detach)映射关系,shmaddr 参数是之前调用 shmat 时的返回值(指向共享内存的指针)。示例:shared_memory_test_write.c:写端 #include?<stdio.h>?#include?<sys/ipc.h>?#include?<sys/shm.h>?intmain() {?//根据路径和指定的 id 生成唯一的 key?key_t?key = ftok("/",?2021);?if?(key ==?-1) { perror("ftok() fail"); }?//创建(不存在时)大小为 1024KB 的共享内存,0666 代表权限?int?shmid = shmget(key,?1024, IPC_CREAT |?0666);?if?(shmid ==?-1) { perror("shmget() fail"); }?//attach,将共享内存映射到当前进程?//第二个参数:共享内存连接到当前进程中的地址,通常为 0,表示让系统来选择?//第三个参数:shm_flg 是一组标志位,通常为 0?//返回共享内存地址?void?*shm = shmat(shmid,?0,?0);?if?(shm == (void?*)-1) { perror("shmat() fail"); }?//将键盘输入的数据写入共享内存?fgets(shm,?1024,?stdin);?//detach,把共享内存从当前进程中分离出去?int?flag = shmdt(shm);?if?(flag ==?-1) { perror("shmdt() fail"); }?return?0; }?shared_memory_test_read.c:读端 #include?<stdio.h>?#include?<sys/ipc.h>?#include?<sys/shm.h>?int?main() {?//根据路径和指定的 id 生成唯一的 key?key_t?key = ftok("/",?2021);?if?(key ==?-1) { perror("ftok() fail"); }?//创建(不存在时)大小为 1024KB 的共享内存,0666 代表权限?intshmid = shmget(key,?1024, IPC_CREAT |?0666);?if?(shmid ==?-1) { perror("shmget() fail"); }?//attach,将共享内存映射到当前进程?//第二个参数:共享内存连接到当前进程中的地址,通常为 0,表示让系统来选择?//第三个参数:shm_flg 是一组标志位,通常为 0?//返回共享内存地址?void?*shm = shmat(shmid,?0,?0);?if?(shm == (void?*)-1) { perror("shmat() fail"); }?//将键盘输入的数据写入共享内存?fputs(shm,?stdout);?//detach,把共享内存从当前进程中分离出去?int?flag = shmdt(shm);?if?(flag ==?-1) { perror("shmdt() fail"); }?return?0; } 编译后先运行写端,输入 hello world 回车后程序退出;然后运行读端,程序读取并打印出 hello world。如下:(编译运行环境:ubuntu 20.14)以上代码并没有释放共享内存(shmctl(shmid, IPC_RMID, 0)可以释放),因此,程序运行结束后,通过命令 ipcs -m 仍然可以看到我们创建的共享内存(系统重启后消失)。最后,我们可以通过命令 ipcrm -m shmid 释放共享内存。进程间的 7 种通信方式(含例程代码)Linux 下进程间通信方式——共享内存

  6. 信号量(semaphore)信号量的原理是一种数据操作锁的概念,可用于多个进程间的同步。它本身并不具备数据交换的功能,而是通过控制其他的通信资源来实现进程间通信。我们可以将其理解成一个具有原子性的计数器,每当有进程申请使用信号量,通过一个 P 操作来对信号量进行-1 操作,当计数器减到 0 的时候就说明没有资源了,其他进程继续访问就会被阻塞,当该进程执行完这段工作释放临界资源之后,就会执行 V 操作来对信号量进行+1 操作,被阻塞的进程就会被唤醒。在 Linux 系统中,利用 C 语言,通过调用 int semget(key_t key, int nsems, int semflg)系统调用创建(不存在时)信号量并得到信号量的唯一标识符,其中 key 和前述 key 相同。可以通过命令 ipcs -s 查看系统当前存在的信号量,通过命令 ipcrm -s semid 可以释放信号量。进程间的通信之信号量用信号量为共享内存添加同步机制

  7. 套接字(socket)更为一般的进程间通信机制,可用于运行在不同主机上的进程之间通信。他通过 IP 地址和端口号确定一台主机上一个进程,常用于网络编程。


备注:以上这些通信方式并不是所有的操作系统都提供了实现,即使操作系统提供了实现,编程语言也不一定提供了访问的接口。以上消息队列、共享内存和信号量都是基于 System V 规范的(另一种实现是基于 POSIX 规范的)。


常见内存管理方式有哪些?#


=============


内存管理机制分为连续分配管理方式和非连续分配管理方式。前者为进程分配一个连续的内存空间,是古老的内存管理方式,常见的有单一连续分配固定分区分配等。后者是充分利用离散的内存,将进程分散的装入内存分区中;根据分区大小是否固定可以分为页式管理(固定分区大小)和段式管理(不固定分区大小);还可以二者混用成段页式管理


什么是分页存储管理?什么是分段存储管理?区别是什么?#


===========================


分页存储管理:


分页存储管理极大的提高了内存利用率,他将实际的物理内存分为大小相等的块,通常被称为页帧。页式管理将用户程序所需内存以离散的页帧形式分配给他们。每个用户程序都有自己的逻辑地址空间,逻辑空间也被划分为与页帧大小相同的页面,逻辑页面和物理页帧是一一对应的关系。


那么 CPU 寻址时,是如何完成逻辑地址到实际物内存地址的转换呢?首先要知道,逻辑地址被划分为高位和低位,高位代表当前逻辑地址所在页面对应的页号,低位代表的是页内偏移,以 32 位操作系统来说,他的逻辑地址共有 32 位,如果页面(由页帧大小决定)大小为 4KB(4 * 1024 = 212,注:地址单位为 B,字节。)则需要占用低 12 位来表示页内偏移。显然,CPU 仅仅借助逻辑地址是无法完成寻址的,还需要借助进程页表才能完成逻辑地址到物理地址的转换,页表中记录的是页面和页帧的对应关系。开始寻址时,CPU 根据逻辑地址得到页号和页内偏移,查询页表可得到页号对应页帧在物理内存中的起始地址,页帧起始地址加上页内偏移即可得到实际的物理地址。如下图:



分段存储管理


分页存储是从计算机的角度进行设计的,目的是为了提高内存的利用率,每一页面并没有实际意义。段式存储管理从程序员和用户角度出发,把程序分割成具有逻辑意义的段,例如:主程序段、子程序段、数据段等等,每一段的大小不定。段式管理将用户程序所需内存以离散的内存段的形式分配给她们。借助段式管理容易实现数据的共享与保护。


那么分段存储管理中,CPU 又是如何完成寻址的呢?分段存储管理中,逻辑地址同样被划分为高位和低位,高位表示段号,低位表示段内偏移。仅仅根据段号和段内偏移尚无法完成寻址,还需要借助进程段表,段表记录了逻辑段的大小(段长)以及逻辑段在内存中的起始地址(基址)。开始寻址时,CPU 先拿到指明的段号和段内偏移(由于段长不定,段号和段内偏移无法像分页管理那样根据逻辑地址和页面大小直接计算出来页号和页内偏移,需要指明逻辑地址中哪部分表示段号,哪部分表示段内偏移,这也是段式管理中逻辑地址是二维的原因),继续查询段表可以得到逻辑段的基址(段表中的段长是用来检查当前段的段内偏移是否超过段长而发生越界),基址加上段内偏移即可得到实际的物理地址。如下图:


![关于计算机面试重难点 之 操作系统,字节架构师有话


【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


说](https://static001.geekbang.org/infoq/9f/9f6bc38948ec37296e9e6b63ae29fe75.png)


分页管理和分段管理区别:


  • 分页管理是站在计算机角度进行设计,每一页并无逻辑意义,目的是减少外部碎片,提高内存的利用率,对用户不可见;段式管理站在程序员和用户角度,是一种逻辑上的划分,是为了满足用户的需要,对用户是可见的,编程时需要指明段名和段内地址(汇编语言中指明了段名和段内地址就指明了逻辑地址的段号和段内偏移)。

  • 分页管理中,页面大小是固定的;而分段管理中段的大小取决于具体程序代码段,是变化的。

  • 分页管理中,逻辑地址是一维的;而分段管理中逻辑地址是二维的。

  • 在实现对程序和数据的共享与保护时,分段管理中,程序和数据本就按逻辑段存储在内存段中,容易实现对程序段、数据段的共享控制以及保护控制;而分页管理中,逻辑上的代码段或数据段是被分散的存储在各个离散的内存页帧当中,很难实现对逻辑程序段或逻辑数据段的共享与保护。


注:进程页表和进程段表都存放于物理内存中,且一个页表或段表是占用的连续空间。以上图中为了方便表达没有将页表或段表画在物理内存中。


操作系统之页式管理


段页式存储管理了解吗?#


============


在分页存储管理中,内存利用率高,不会产生外部碎片,但不方便实现数据的共享与保护;而分段存储管理则刚好相反。段页式存储管理就是为了结合两者的优点。简单来说,段页式存储管理将逻辑空间划分为逻辑段,逻辑段再划分为逻辑页面;而物理内存划分为大小相同的页帧,逻辑页面和物理页帧一一对应,并装入物理页帧当中。


在段页式存储管理中,逻辑地址被划分为三段,由高到低依次代表段号页号页内偏移。CPU 寻址时需要借助段表和页表,段表记录了各个逻辑段对应页表的物理内存起始地址,以及页表长度;页表则记录了各个逻辑页面对应物理页帧的起始地址。 寻址开始时,CPU 首先拿到指明的段号并根据页面(页帧)大小计算出页号和页内偏移,CPU 根据段号和段表可以找到该逻辑段对应页表的起始物理内存地址,再根据页号和页表找到对应页帧首地址,该首地址加上页内偏移即可得到实际的物理地址。(段表中的页表长度用来检查页号是否越界)。如下图:



操作系统之段式管理及段页式管理


虚拟存储器(虚拟内存)了解吗?#


================


基于局部性原理,在程序装入时,可以先装入当前运行需要的部分,然后就可以开始启动程序,而将其他的部分暂时留在外存。在程序执行时,如果访问的信息不在内存中,由操作系统将所需要的部分调入内存;如果此时内存已经没有空间给新调入的部分,那么操作系统按照某种淘汰策略将一部分旧的内容暂时换到外存上存放,然后再将新需要的部分调入内存,接着继续执行程序。这样,操作系统就可以执行比实际内存大的多的程序,就好像为用户提供了一个比实际内存大的多的存储器。


虚拟存储器种类:


  • 虚拟页式存储管理

  • 虚拟段式存储管理

  • 虚拟段页式存储管理


局部性原理:



**时间局部性原理:**如果程序中的某条指令?旦执?,不久以后该指令可能再次执?;如果某数据被访问过,不久以后该数据可能再次被访问。



空间局部性原理:?旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问, 即程序在?段时间内所访问的地址,可能集中在?定的范围之内。

评论

发布
暂无评论
关于计算机面试重难点 之 操作系统,字节架构师有话说