文章目录
- 17、简述一下虚拟内存和物理内存,为什么要用虚拟内存,好处是什么?
- 18、虚拟地址到物理地址怎么映射的?
- 19、说说堆栈溢出是什么,会怎么样?
- 20、简述操作系统中malloc的实现原理?
- 21、说说进程空间从高位到低位都有些什么?
- 22、32位系统能访问4GB以上的内存吗?
- 23、请你说说并发和并行?
- 24、说说进程、线程、协程是什么,区别是什么?
- 25、请你说说Linux的fork的作用?
- 26、请你说说什么是孤儿进程,什么是僵尸进程,如何解决僵尸进程?
- 27、请你说说什么是孤儿进程,什么是僵尸进程,如何解决僵尸进程?
- 28、说说进程通信的方式有哪些?
- 29、说说进程同步的方式?
- 30 、说说Linux进程调度算法及策略有哪些?
- 31 、说说Linux进程调度算法及策略有哪些?
- 32 、进程通信中的管道实现原理是什么?
17、简述一下虚拟内存和物理内存,为什么要用虚拟内存,好处是什么?
- 物理内存:物理内存有四个层次,分别是寄存器、高速缓存、主存、磁盘。
寄存器:速度最快、量少、价格贵。
高速缓存:次之。
主存:再次之。
磁盘:速度最慢、量多、价格便宜。
操作系统会对物理内存进行管理,有一个部分称为内存管理器(memory manager),它的主要工作是有效的管理内存,记录哪些内存是正在使用的,在进程需要时分配内存以及在进程完成时回收内存。 - 虚拟内存:操作系统为每一个进程分配一个独立的地址空间,但是虚拟内存。虚拟内存与物理内存存在映射关系,通过页表寻址完成虚拟地址和物理地址的转换。
- 为什么要用虚拟内存:因为早期的内存分配方法存在以下问题:
(1)进程地址空间不隔离。会导致数据被随意修改。
(2)内存使用效率低。
(3)程序运行的地址不确定。操作系统随机为进程分配内存空间,所以程序运行的地址是不确定的。 - 使用虚拟内存的好处:
(1)扩大地址空间。每个进程独占一个4G空间,虽然真实物理内存没那么多。
(2)内存保护:防止不同进程对物理内存的争夺和践踏,可以对特定内存地址提供写保护,防止
恶意篡改。
(3)可以实现内存共享,方便进程通信。
(4)可以避免内存碎片,虽然物理内存可能不连续,但映射到虚拟内存上可以连续。 - 使用虚拟内存的缺点:
(1)虚拟内存需要额外构建数据结构,占用空间。
(2)虚拟地址到物理地址的转换,增加了执行时间。
(3)页面换入换出耗时。
(4)一页如果只有一部分数据,浪费内存。
18、虚拟地址到物理地址怎么映射的?
操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫页表。页表中的每一项都记录了这个页的基地址。
三级页表转换方法:(两步)
- 逻辑地址转线性地址:段起始地址+段内偏移地址=线性地址
- 线性地址转物理地址:
(1)每一个32位的线性地址被划分为三部分:页目录索引(DIRECTORY,10位)、页表索引 (TABLE,10位)、页内偏移(OFFSET,12位)
(2)从cr3中取出进程的页目录地址(操作系统调用进程时,这个地址被装入寄存器中)
页目录地址 + 页目录索引 = 页表地址
页表地址 + 页表索引 = 页地址
页地址 + 页内偏移 = 物理地址
19、说说堆栈溢出是什么,会怎么样?
堆栈溢出就是不顾堆栈中分配的局部数据块大小,向该数据块写入了过多的数据,导致数据越界。常指调用堆栈溢出,本质上一种数据结构的满溢情况。堆栈溢出可以理解为两个方面:堆溢出和栈溢出。
- 堆溢出:比如不断的new 一个对象,一直创建新的对象,而不进行释放,最终导致内存不足。将会报错:OutOfMemory Error。
- 栈溢出:一次函数调用中,栈中将被依次压入:参数,返回地址等,而方法如果递归比较深或进去死循环,就会导致栈溢出。将会报错:StackOverflow Error
20、简述操作系统中malloc的实现原理?
malloc底层实现:当开辟的空间小于 128K 时,调用 brk()函数;当开辟的空间大于 128K 时,调用mmap()。malloc采用的是内存池的管理方式,以减少内存碎片。先申请大块内存作为堆区,然后将堆区分为多个内存块。当用户申请内存时,直接从堆区分配一块合适的空闲快。采用隐式链表将所有空闲块,每一个空闲块记录了一个未分配的、连续的内存地址。
21、说说进程空间从高位到低位都有些什么?
如上图,从高地址到低地址,一个程序由命令行参数和环境变量、栈、文件映射区、堆、BSS段、数据段、代码段组成。
- 命令行参数和环境变量
- 栈区:存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。
- 文件映射区,位于堆和栈之间。
- 堆区:动态申请内存用。堆从低地址向高地址增长。
- BSS 段:存放程序中未初始化的全局变量和静态变量的一块内存区域。
- 数据段:存放程序中已初始化的全局变量和静态变量的一块内存区域。
- 代码段:存放程序执行代码的一块内存区域。只读,代码段的头部还会包含一些只读的常数变量。
22、32位系统能访问4GB以上的内存吗?
正常情况下是不可以的。原因是计算机使用二进制,每位数只有0或1两个状态,32位正好是2的32次方,正好是4G,所以大于4G就没办法表示了,而在32位的系统中,因其它原因还需要占用一部分空
间,所以内存只能识别3G多。要使用4G以上就只能换64位的操作系统了。但是使用PAE技术就可以实现 32位系统能访问4GB以上的内存。
Physical Address Extension(PAE)技术最初是为了弥补32位地址在PC服务器应用上的不足而推出的。我们知道,传统的IA32架构只有32位地址总线,只能让系统容纳不超过4GB的内存,这么大的内存,对于普通的桌面应用应该说是足够用了。可是,对于服务器应用来说,还是显得不足,因为服务器上可能承载了很多同时运行的应用。PAE技术将地址扩展到了36位,这样,系统就能够容纳2^36=64GB的内存。
23、请你说说并发和并行?
- 并发:对于单个CPU,在一个时刻只有一个进程在运行,但是线程的切换时间则减少到纳秒数量
级,多个任务不停来回快速切换。 - 并行:对于多个CPU,多个进程同时运行。
- 区别。通俗来讲,它们虽然都说是"多个进程同时运行",但是它们的"同时"不是一个概念。并行的"同时"是同一时刻可以多个任务在运行(处于running),并发的"同时"是经过不同线程快速切换,使得看上去多个任务同时都在运行的现象。
24、说说进程、线程、协程是什么,区别是什么?
- 进程:程序是指令、数据及其组织形式的描述,而进程则是程序的运行实例,包括程序计数器、寄
存器和变量的当前值。 - 线程:微进程,一个进程里更小粒度的执行单元。一个进程里包含多个线程并发执行任务。
- 协程:协程是微线程,在子程序内部执行,可在子程序内部中断,转而执行别的子程序,在适当的时候再返回来接着执行。
线程与进程的区别:
(1)一个线程从属于一个进程;一个进程可以包含多个线程。
(2)一个线程挂掉,对应的进程挂掉;一个进程挂掉,不会影响其他进程。
(3)进程是系统资源分配、管理、调度的最小单位;线程CPU调度的最小单位。
(4)进程系统开销显著大于线程开销;线程需要的系统资源更少。
(5)进程在执行时拥有独立的内存单元,多个线程共享进程的内存,如代码段、数据段、扩展
段;但每个线程拥有自己的栈段和寄存器组。
(6)进程切换时需要刷新TLB并获取新的地址空间,然后切换硬件上下文和内核栈,线程切换时只
需要切换硬件上下文和内核栈。
(7)通信方式不一样。
(8)进程适应于多核、多机分布;线程适用于多核
线程与协程的区别:
(1)协程执行效率极高。协程直接操作栈基本没有内核切换的开销,所以上下文的切换非常快,
切换开销比线程更小。
(2)协程不需要多线程的锁机制,因为多个协程从属于一个线程,不存在同时写变量冲突,效率
比线程高。
(3)一个线程可以有多个协程
25、请你说说Linux的fork的作用?
fork函数用来创建一个子进程。对于父进程,fork()函数返回新创建的子进程的PID。对于子进程,fork()函数调用成功会返回0。如果创建出错,fork()函数返回-1。
#include <unistd.h>
pid_t fork(void);
fork()函数不需要参数,返回值是一个进程标识符PID。返回值有以下三种情况:
(1) 对于父进程,fork()函数返回新创建的子进程的PID。
(2) 对于子进程,fork()函数调用成功会返回0。
(3) 如果创建出错,fork()函数返回-1。
fork()函数创建一个新进程后,会为这个新进程分配进程空间,将父进程的进程空间中的内容复制到子进程的进程空间中,包括父进程的数据段和堆栈段,并且和父进程共享代码段。这时候,子进程和父进程一模一样,都接受系统的调度。因为两个进程都停留在fork()函数中,最后fork()函数会返回两次,一次在父进程中返回,一次在子进程中返回,两次返回的值不一样,如上面的三种情况。
26、请你说说什么是孤儿进程,什么是僵尸进程,如何解决僵尸进程?
- 孤儿进程:是指一个父进程退出后,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并且由init进程对它们完整状态收集工作。
- 僵尸进程:是指一个进程使用fork函数创建子进程,如果子进程退出,而父进程并没有调用wait()或者waitpid()系统调用取得子进程的终止状态,那么子进程的进程描述符仍然保存在系统中,占用系统资源,这种进程称为僵尸进程。
- 如何解决僵尸进程:
(1)一般,为了防止产生僵尸进程,在fork子进程之后我们都要及时使用wait系统调用;同时,当子进程退出的时候,内核都会给父进程一个SIGCHLD信号,所以我们可以建立一个捕获SIGCHLD信号的信号处理函数,在函数体中调用wait(或waitpid),就可以清理退出的子进程以达到防止僵尸进程的目的。
(2)使用kill命令。
打开终端并输入下面命令:
ps aux | grep Z
会列出进程表中所有僵尸进程的详细内容。
然后输入命令
kill -s SIGCHLD pid(父进程pid)
27、请你说说什么是孤儿进程,什么是僵尸进程,如何解决僵尸进程?
- 守护进程:守护进程是运行在后台的一种生存期长的特殊进程。它独立于控制终端,处理一些系统
级别任务。 - 如何实现:
(1)创建子进程,终止父进程。方法是调用fork() 产生一个子进程,然后使父进程退出。
(2)调用setsid() 创建一个新会话。
(3)将当前目录更改为根目录。使用fork() 创建的子进程也继承了父进程的当前工作目录。
(4)重设文件权限掩码。文件权限掩码是指屏蔽掉文件权限中的对应位。
(5)关闭不再需要的文件描述符。子进程从父进程继承打开的文件描述符。
28、说说进程通信的方式有哪些?
进程间通信主要包括管道、系统IPC(包括消息队列、信号量、信号、共享内存)、套接字socket。
- 管道:包括无名管道和命名管道,无名管道半双工,只能用于具有亲缘关系的进程直接的通信(父
子进程或者兄弟进程),可以看作一种特殊的文件;命名管道可以允许无亲缘关系进程间的通信。 - 系统IPC
- 消息队列:消息的链接表,放在内核中。消息队列独立于发送与接收进程,进程终止时,消息队列及其内容并不会被删除;消息队列可以实现消息的随机查询,可以按照消息的类型读取。
- 信号量semaphore:是一个计数器,可以用来控制多个进程对共享-资源的访问。信号量用于实现进程间的互斥与同步。
- 信号:用于通知接收进程某个事件的发生。
- 内存共享:使多个进程访问同一块内存空间。
- 套接字socket:用于不同主机直接的通信。
29、说说进程同步的方式?
- 信号量semaphore:是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步。P操作(递减操作)可以用于阻塞一个进程,V操作(增加操作)可以用于解除阻塞一个进程。
- 管道:一个进程通过调用管程的一个过程进入管程。在任何时候,只能有一个进程在管程中执行,调用管程的任何其他进程都被阻塞,以等待管程可用。
- 消息队列:消息的链接表,放在内核中。消息队列独立于发送与接收进程,进程终止时,消息队列及其内容并不会被删除;消息队列可以实现消息的随机查询,可以按照消息的类型读取。
30 、说说Linux进程调度算法及策略有哪些?
- 先来先服务调度算法
- 短作业(进程)优先调度算法
- 高优先级优先调度算法
- 时间片轮转法
- 多级反馈队列调度算法
31 、说说Linux进程调度算法及策略有哪些?
进程有五种状态:创建、就绪、执行、阻塞、终止。一个进程创建后,被放入队列处于就绪状态,等待操作系统调度执行,执行过程中可能切换到阻塞状态(并发),任务完成后,进程销毁终止。
- 创建状态
一个应用程序从系统上启动,首先就是进入创建状态,需要获取系统资源创建进程管理块(PCB:Process Control Block)完成资源分配。 - 就绪状态
在创建状态完成之后,进程已经准备好,处于就绪状态,但是还未获得处理器资源,无法运行。 - 运行状态
获取处理器资源,被系统调度,当具有时间片开始进入运行状态。如果进程的时间片用完了就进入就绪状态。 - 阻塞状态
在运行状态期间,如果进行了阻塞的操作,如耗时的I/O操作,此时进程暂时无法操作就进入到了阻塞状态,在这些操作完成后就进入就绪状态。等待再次获取处理器资源,被系统调度,当具有时间片就进入运行状态。 - 终止状态
进程结束或者被系统终止,进入终止状态
32 、进程通信中的管道实现原理是什么?
操作系统在内核中开辟一块缓冲区(称为管道)用于通信。管道是一种两个进程间进行单向通信的机制。因为这种单向性,管道又称为半双工管道,所以其使用是有一定的局限性的。半双工是指数据只能
由一个进程流向另一个进程(一个管道负责读,一个管道负责写);如果是全双工通信,需要建立两个管道。管道分为无名管道和命名管道,无名管道只能用于具有亲缘关系的进程直接的通信(父子进程或者兄弟进程),可以看作一种特殊的文件,管道本质是一种文件;命名管道可以允许无亲缘关系进程间的通信。
#include <unistd.h>
int pipe(int fd[2]);
pipe()函数创建的管道处于一个进程中间,因此一个进程在由 pipe()创建管道后,一般再使用fork() 建立一个子进程,然后通过管道实现父子进程间的通信。管道两端可分别用描述字fd[0]以及fd[1]来描述。注意管道的两端的任务是固定的,即一端只能用于读,由描述字fd[0]表示,称其为管道读端;另一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。如果试图从管道写端读取数据,或者向管道读端写入数据都将发生错误。一般文件的 I/O 函数都可以用于管道,如close()、read()、write()等。
具体步骤如下:
- 父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。
- 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
- 父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是
用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define INPUT 0
#define OUTPUT 1
int main(){
//创建管道
int fd[2];
pipe(fd);
//创建子进程
pid_t pid = fork();
if (pid < 0){
printf("fork error!\n");
exit(-1);
}
else if (pid == 0){//执行子进程
printf("Child process is starting...\n");
//子进程向父进程写数据,关闭管道的读端
close(fd[INPUT]);
write(fd[OUTPUT], "hello douya!", strlen("hello douya!"));
exit(0);
}
else{//执行父进程
printf ("Parent process is starting......\n");
//父进程从管道读取子进程写的数据 ,关闭管道的写端
close(fd[OUTPUT]);
char buf[255];
int output = read(fd[INPUT], buf, sizeof(buf));
printf("%d bytes of data from child process: %s\n", output, buf);
}
return 0;
}