您的位置:首页 > 职场人生

面试5——操作系统基础知识储备

2020-07-22 21:41 176 查看

1.线程的基本概念,线程的基本状态

线程称为轻量级进程,是cpu使用的基本单元;它由线程ID、程序计数器、寄存器集合和堆栈组成。它与属于同一进程的其他线程共享其代码段,数据段和其他操作系统资源。
线程有四种状态:1.新生状态 2.可运行状态 3. 被阻塞状态 4.死亡状态

2. 多进程与多线程区别,应用场景

从资源使用的角度出发
根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
开销方面:每个进程都有独立的代码和数据空间(程序上下文),进程之间切换开销大;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
内存分配:系统为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源
包含关系:线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程

3.多线程有几种实现方法,都是什么?

java中有四种创建方法:1.继承 threads 类。 2 实现 runnable 接口
c/c++标准库 thread,mutex、使用第三方库 pthread,或者Linux 封装的 pthread 库。

Linux下的多线程遵从POSIX线程接口,简称pthread,在pthread库中提供。

pthread_create():创建一个线程

pthread_exit():退出一个线程

pthread_jion():阻塞当前线程,直到另一个线程执行结束

pthread_attr_init():设置线程是否脱离属性

pthread_kill():给线程发送kill信号

同步函数:

pthread_mutex_lock():互斥加锁

pthread_mutex_unlock():互斥锁解锁

pthread_cond_init():初始化条件变量

pthread_cond_signal():发送信号唤醒进程

pthread_cond_wait():等待条件变量的特殊事件发生

4.线程同步实现方法?

互斥锁,条件变量,读写锁,信号量

线程同步是指同一进程中的多个线程互相协调工作从而达到一致性。为了避免对同一数据对象进行修改操作的时候对数据造成破坏,所以产生了互斥锁,又为了保证当一个线程完成工作后另一个线程能够及时的得到通知来进行它的工作,从而产生了条件变量,信号量等等通信类的保障机制。

5.多线程同步和互斥有何异同,在什么情况下分别使用他们?举例说明

线程同步 :是线程之间的一种制约关系,一个线程的执行需要依赖另一个线程的消息,当它没有得到另一个线程的消息时等待,直到有消息时被唤醒,需要条件变量来进行线程通信。
线程互斥:是指多线程共享一个进程资源时,需要利用互斥锁将共享资源每次的访问只允许一个线程去访问。即互斥访问,为了保证共享资源区不会同时被多个线程访问从而造成混乱。最有代表性的模式就是生产消费模式,共享同一个资源区,为了别面资源区的混乱从而需要对其进行互斥锁管理。

6.volatile和原子变量的区别

在 java 语言中,volatile 变量提供了一种轻量级的同步机制,volatile 变量用来确保将变量的更新操作通知到其他线程。
原子变量 是“更加强大的 volatile 变量”,原子变量提供读,改,写的原子操作,更强大,更符合一般并发场景的需求。

7.自旋锁与普通锁的区别

1.读写锁: 有三个状态:读加锁状态、写加锁状态、不加锁状态。
读写锁可以支持高并发,其中一个原因就是 只能允许一个线程占用写状态锁,但是多个线程可以共享读状态锁。当写状态锁被占用的时候,后续想要写状态的线程就会被阻塞。所以读写锁非常适合那些 读状态 远远多于 写状态的应用场景。
特点
1.多个读线程可以同时进行读
2.写线程必须互斥,不能同时读同时写。
3.写线程优先于读线程,先写加锁线程之后才能读加锁线程。
2.互斥锁:访问共享资源时,允许一个线程对资源区进行操作,其他线程即为堵塞睡眠状态,直到被唤醒。
特点:一次只能一个线程拥有互斥锁,其他线程只有等待。
3.自旋锁
自旋锁是一种特殊的互斥锁,当资源被加锁后,其他想要访问的线程不会被阻塞而是陷入循环等待状态,循环检查资源持有者是否已经释放了资源,这样的好处是节省了线程从休眠到被唤醒的开销,但同时也会一直占据CPU的资源。所以自旋锁适合多核CPU、共享资源区被加锁的时间短的情况。但是还有一个问题是当自旋锁递归调用的时候会造成死锁现象。所以慎重使用自旋锁。
4.乐观锁、悲观锁:一种思想:当线程去访问数据的时候,如果认为其他线程 不会修改数据,那么就不会对数据资源上锁,但是在更新数据的时候会去判断以下其他线程是否修改了数据。通过版本来判断,如果数据被修改了就拒绝更新,这就是乐观锁,即不上锁。悲观锁反之,它会对数据加锁,保证数据的正确性。
特点:以上两种锁多用于数据库,当读操作远远大于写操作的时候,乐观锁的效率会很高,会增加数据库的吞吐量。

8.进程的内存分布

Linux操作系统采用的是虚拟内存管理技术,这使得进程都拥有了独立的虚拟内存空间。该内存空间的大小为4G的线性虚拟空间,进程只需关注自己可以访问的虚拟地址,无需直到物理地址的映射情况。4GB的进程地址空间会被分成两个部分——用户空间与内核空间
用户地址空间是 0~3GB(0xC0000000),内核地址空间占据 3~4GB。用户进程在通常情况下只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。只有用户进程使用系统调用(代表用户进程在内核态执行)时才可以访问到内核空间。每当进程切换,用户空间就会跟着产生变化;而内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。内核空间地址有自己对应的页表,用户进程各自有不同的页表。每个进程的用户空间都是完全独立、互不相干的。

(1) 程序代码段:具有只读属性,包含程序代码(.init和.text)和只读数据(.rodata)。
(2)数据段:存放的是全局变量和静态变量。 其中初始化数据段(.data)存放显示初始化的全局变量和静态变量,未初始化数据段,此段通常也被称为BSS段(.bss),存放未进行显示初始化的全局变量和静态变量。
(3)栈:由系统自动分配释放,存放函数的参数值、局部变量的值、返回地址等。
(4)堆:存放动态分配的数据,一般由程序动态分配和释放,若程序不释放,程序结束时可能由操作系统回收。例如,使用malloc()申请空间。
(5)共享库的内存映射区域:这是Linux动态链接器和其他共享库代码的映射区域。

8.1 Linux 虚拟内存映射机制:

Linux 采用的是虚拟内存映射带物理内存的管理方式,每一个进程都将拥有自己独立的4G虚拟内存,它们核心点就是mmap 文件映射机制。
**mmap 将一个文件映射到进程的虚拟地址空间,实现了文件磁盘地址与进程虚拟地址的一一对应关系,实现这样的关系之后,进程就可以通过指针的方式去读写这一段内存,避免了通过write/read 函数,(write/read函数会形成多次拷贝,内核压力过大)。**相反,内也可以通过映射直接修改数据,从而实现不同进程之间对该内存空间的通信,这也就是进程之间共享内存的通信方式。
mmap 优势:

  1. 对文件的读取跨过了页缓存,减少了数据的拷贝次数,提高了文件的读取效率
  2. 共享内存通信方式提供了一种不管什么进程都可以通信的方式,父子进程等都可以。

9.栈内存为什么由系统自动分配和释放

栈内存空间一般较小,在程序中的局部变量和全局变量声明是直接由系统赋予内存空间,但是当某些变量所需内存空间较大,则有程序员手动申请和释放空间,而这些空间一般为堆内存。

10.创建守护进程的步骤

Linux 守护进程:守护进程是一个孤儿进程,孤儿进程是在父进程先退出,从而导致子进程被系统1号init进程领养,而一旦被领养之后,这个子进程就会具备很多属性,从而有了具备守护进程的条件
守护进程 独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。

1).自成进程组,自成会话,与控制终端脱关联;
2).守护进程的父进程是1号进程;
3).守护进程的命令一般以字符d结尾;
4).守护进程的生命周期是7*24小时不掉线;
5).一般的网络服务器都以守护进程的形式在后台运行,比如常见的http,ftp等等服务器都是以守护进程的形式在后台运行;

1. 调用 fork ,父进程退出(exit)

pid_t id = fork();
if(id > 0)
{
exit(1);
}

孤儿进程是在父进程先退出产生的,从而导致子进程被系统1号init进程领养,而一旦被领养之后,这个子进程就会具备很多属性,从而有了具备守护进程的条件。
2. 调用 setsid 函数创建一个新的Session进程集合(进程组),并使得当前进程为session leader setsid()函数会让子进程完全从父进程当中独立出来,摆脱其他线程的控制。

#include<unistd.h>
pid_t setsid(void);

返回值:该函数调用成功时返回新创建的Session的id(其实也就是当前进程的id),出错返回-1。
3、再次 fork() 一个子进程,父进程 exit 退出
现在随着setsid函数的调用,子进程已经成为无终端的会话组长,它就可以重新申请打开一个控制终端
为了避免这种情况,可以通过 fork() 一个子进程,该子进程不是会话首进程,该进程将不能重新打开控制终端。退出父进程。也就是说通过再次创建子进程结束当前进程,使进程不再是会话首进程来禁止进程重新打开控制终端。
4. 调用 umask 将文件模式创建屏蔽子设置为0

umask(0); //umask必须清0,否则创建文件受系统默认权限的影响

文件权限掩码是屏蔽掉文件权限中的对应位。由于使用 fork()函数新创建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带了很多的麻烦(比如父进程中的文件没有执行文件的权限,然而在子进程中希望执行相应的文件这个时候就会出问题)。因此在子进程中要把文件的权限掩码设置成为0,即在此时有最大的权限,这样可以大大增强该守护进程的灵活性。
5.将当前的工作目录更改为根目录。
避免子进程因为继承父进程的目录从而带来些许问题。
6.关闭不再需要的文件描述符
关闭失去价值的输入、输出、报错等对应文件的描述符
重点:两次 fork 子进程,父进程退出

11.进程间的通信方式及其区别,应用场景

1.共享内存通信:通过多各进程共享同一块内存中的数据从而达到通信的目的,是速度最快的通信方式,需要配合信号量机制实现进程同步。
共享内存的缺点:基于内存,只能在同一主机内使用,不能应用与分布式系统当中。
2.管道通信 pipe :
2.1 管道是一种半双工的通信方式,数据只能单向流动,单向传输。一般只能在亲缘关系的进程间,即是父子进程间流动。
2.2 流管道 s_pipe 半双工,可双向传输。
2.3 命名管道 name_pipe 😗*(FIFO)**去除了两个亲属进程之间通信的规定,可以在许多不相关的进程之间进行通信。
3.消息队列:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
4.信号(sinal): 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
信号量主要用于进程间同步,若要在进程间传递数据需要结合共享内存。
5.套接口:套解口也是一种进程间通信机制,它也可用于不同及其间的进程通信。

12.死锁条件和解除

死锁:多个并发进/线程因为争夺系统资源陷入相互等待的死循环的现象。
原理:当一组进程或者线程都在等待某个事件的产生,而且只有该组进程/线程才能出发该事件,这就称这组进程、线程发生了死锁。

死锁产生的四个必要条件:
1.互斥,某种资源只允许一个进程访问,另一个进程想要访问该资源必须等待阻塞。
2.占有并等待,当某个进程本身占有资源,同时它又在等待其他线程释放的某种其他的资源,这就造成了这两个线程出现相互等待的现象。
3.不可抢占,产生第2条的条件之一就是两个进程之间不可抢占被别人占据的资源,也即是互斥条件
4.循环等待,存在一个进程链,使得每一个进程都占有下一个进程所需要的至少一种资源

避免死锁的方法:
以上4个必要条件对于死锁产生来说都是必要的,当我们破坏他的这几个条件那么就不会产生死锁了。
1、第一个互斥条件是共享资源最核心的保证,不可能去改变
2.破坏占有等待条件,当进程运行时一次性申请整个运行过程所需要的所有资源,但是这样在某些场景中比较浪费资源。
改进:进程边运行边释放掉已经使用过的资源,然后再去请求新的资源,这样提高资源的利用率。
3.破坏不可抢占条件,当一个进程申请的资源被其他线程抢占且没有即是释放的话,那么这个线程就必须释放掉它本身占用的资源,后续在用的时候重新申请。(操作复杂,代价比较大,影响效率)
4.破坏循环等待条件,为每个资源编号,每个进程申请的资源编号必须比前一号资源编号大的资源,防止对统一资源的申请等待。这样效率因为是比较低的。

死锁的检测与解除: 检测到运行系统进入死锁,并进行恢复
1、判断“系统安全状态”法
在进行系统资源分配之前,先计算此次资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程; 否则,让进程等待。
2、银行家算法

  1. 申请的贷款额度不能超过银行现有的资金总额
  2. 分批次向银行提款,但是贷款额度不能超过一开始最大需求量的总额
  3. 暂时不能满足客户申请的资金额度时,在有限时间内给予贷款
  4. 客户要在规定的时间内还款

死锁的解除:
1、抢占资源:从一个或多个进程中抢占足够数量的资源分配给死锁进程,以解除死锁状态。
2、终止(或撤销)进程:终止或撤销系统中的一个或多个死锁进程,直至打破死锁状态。
a、终止所有的死锁进程。这种方式简单粗暴,但是代价很大,很有可能会导致一些已经运行了很久的进程前功尽弃。
b、逐个终止进程,直至死锁状态解除。该方法的代价也很大,因为每终止一个进程就需要使用死锁检测来检测系统当前是否处于死锁状态。另外,每次终止进程的时候终止那个进程呢?每次都应该采用最优策略来选择一个“代价最小”的进程来解除死锁状态。
一般根据如下几个方面来决定终止哪个进程:
进程的优先级
进程已运行时间以及运行完成还需要的时间
进程已占用系统资源
进程运行完成还需要的资源
终止进程数目
进程是交互还是批处理

13.进程调度方式

  1. 先来先服务算法:FCFS ,按照顺序执行
  2. 短作业优先算法:SJF 按照任务时间,先执行时间短的
  3. 最高响应比优先法:以上两种的综合算法,提高效率
  4. 时间片轮转法:采用剥夺方式,每个进程分配一个固定的时间段,按照队列中的顺序执行
  5. 多级反馈队列:UNIX采用这种算法。算法详情百度上很多。

14.对编译连接的理解

使用 gcc 命令不跟任何的选项的话,会默认执行预处理、编译、汇编、链接这整个过程,如果程序没有错,就会得到一个可执行文件,默认为a.out

-E选项:提示编译器执行完预处理就停下来,后边的编译、汇编、链接就先不执行了。
-S选项:提示编译器执行完编译就停下来,不去执行汇编和链接了。
-c选项:提示编译器执行完汇编就停下来。

15.共享内存实现原理

共享内存可以说是最有用的进程之间的通信方式,两个进程之间的共享内存的意思是,同一块的物理内存被映射到进程A/B各自的进程地址空间,A/B都可以随时看到共享内存中数据的更新。
Linux中,通过吧一块内存分别映射到不同的进程空间中实现进程间通信,实际中需要联合同步、互斥机制来一起完成。
共享内存实现机制
一、mmap机制:在磁盘上建立一个文件,每个进程存储器中,单独开辟一个空间来映射
保存到实际硬盘,实际并没有反映到主存上
优点:存储量大
缺点:读取和写入速度比较慢
mmap() 是一个系统调用函数,本质是一种进程虚拟内存的映射方法,可以将一个文件、一段物理内存或者其它对象映射到进程的虚拟内存地址空间。实现这样的映射关系后,进程就可以采用指针的方式来读写操作这一段内存,进而完成对文件的操作,而不必再调用 read/write 等系统调用函数了。

为什么对于大文件而言 mmap 的效率要高于 read/write?

先回顾一下调用 read/write 进行的常规的文件系统操作中,函数的调用过程:
进程发起读文件请求。 内核通过查找进程文件符表,定位到内核已打开文件集上的文件信息,从而找到此文件的 inode。
inode 在 address_space 上查找要请求的文件页是否已经缓存在页缓存中。如果存在,则直接返回这片文件页的内容。 如果不存在,则通过 inode 定位到文件磁盘地址,将数据从磁盘复制到页缓存。之后再次发起读页面过程,进而将页缓存中的数据发给用户进程。

总结来说,常规的文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一样,待写入的 Buffer 在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。
共享内存通过零拷贝技术,直接将磁盘文件拷贝到内核缓存中,然后进程通过共享内存直接读取,整个过程只有一次拷贝,所以从效率上来说是比传统读写高的。

二、shm机制:每个进程的共享内存都直接映射到实际物理存储器上
shm保存到物理存储器(主存),实际的存储量直接反映到主存上
优点:进程间访问速度比磁盘快
缺点:存储量不能非常大

16.僵尸进程是什么,如何处理

什么是僵尸进程?
当父进程先与子进程退出,此时子进程会被1号进程INIT领养。而当子进程先于父进程退出,此时父进程应当调用wait()函数,以便及时收到子进程状态,资源等信息,以便回收资源。但是假如子进程退出,而父进程没有调用 wait() 函数,那么 子进程 的善后事宜 父进程得不到通知,此时子进程还占有些许资源,它就变成了一个僵尸进程。

查看僵尸进程
ps 就可以查看到。
SIGCHLD信号和处理僵尸进程
当子进程终止时,内核就会向它的父进程发送一个SIGCHLD信号,父进程可以选择忽略该信号,也可以提供一个接收到信号以后的处理函数,对于这种信号的系统默认动作是忽略它。
我们不希望有过多的僵尸进程产生,所以当父进程接收到SIGCHLD信号后就应该调用 wait 或 waitpid 函数对子进程进行善后处理,释放子进程占用的资源。
wait() /waitpid() 函数区别:

pid_t wait(int *statloc);//能够回收一定数量的子进程,处理能力有限,不一定会全部处理
pid_t waitpid(pid_t pid, int *statloc, int options);//全部处理,排好队列,都会解决。

wait和waitpid都是用来处理终止进程的,这两个函数都返回两个值:已终止的进程ID号,以及通过

statloc
指针返回的子进程的状态(一个整数)。如果调用wait()的进程没有终止的进程而还有一个或多个子进程还在运行,函数将会一直阻塞到现有子进程第一个终止。waitpid函数则有选项可供控制,waitpid在一个循环里可以获取所有的终止进程。pid指需要等待的进程ID,如果值为-1表示等待第一个终止的进程,options是附加选项,常用的选项是
WNOHANG
,即没有子进程终止时不阻塞,至于其他选项自己百度吧。
核心区别:
wait不管来多少都会接受,但是能力有限,只处理一次,至于能处理多少就看运气了,运气好,多出里一些,运气不好就呵呵了,不过一个一个来还是能全部处理的,只要不蜂拥而至就行,不然就真的呵呵了。而 waitpid 却不一样,即使你蜂拥而至,所有来的信号都排好队,一个一个来,保证每个都会处理。

17.自旋锁在单cpu与多cpu下的使用

Linux上的自旋锁有三种实现:

  1. 在单cpu,不可抢占内核中,自旋锁为空操作。
  2. 在单cpu,可抢占内核中,自旋锁实现为“禁止内核抢占”,并不实现“自旋”。
  3. 在多cpu,可抢占内核中,自旋锁实现为“禁止内核抢占” + “自旋”。

自旋锁在单核处理器中容易在成CPU的瘫痪,试想一下这么一个场景:

cpu1持有锁,访问临界区……
cpu2尝试上锁而不得,于是cpu2持续“自旋”,不停的检测锁状态……
cpu1访问结束,释放锁;
cpu2检测到锁状态改变,持有锁,访问临界区……

上面就是一个完整的自旋锁竞争流程。期间,两个cpu都没闲着,尤其是后来的那个cpu2,持续检测锁状态。
那么,设想此情况发生在单cpu系统上,第一个进程持有锁之后莫名其妙的丢掉了cpu的控制权,转移到了另外一个进程,又尝试获取该锁,好了至此,cpu彻底被搞死了。一方面,当前上下文独霸cpu不停的检测锁状态尝试获取该锁,而真正有能力释放该锁的上下文完全得不到机会继续执行来释放它,于是……

18.用户态与内核态

linux进程有4GB地址空间:

3G-4G大部分是共享的,是内核态的地址空间。这里存放整个内核的代码和所有的内核模块以及内核所维护的数据。
计算机系统中通常运行两种程序,一种是系统程序,一种是应用程序。
即用户态和内核态,但是在程序运行通常会在这两种状态中切换。为了区分操作级别,X86 cpu架构采用了 0 - 3 四种特权阶级,其中 0 级为最高特权。
特权指令:对内存空间的访问范围基本不受限制,不仅能访问用户存储空间,也能访问系统存储空间,特权指令只允许操作系统使用,不允许应用程序使用,否则会引起系统混乱。
非特权指令:一般应用程序所使用的都是非特权指令,它只能完成一般性的操作和任务,不能对系统中的硬件和软件直接进行访问,其对内存的访问范围也局限于用户空间。

用户态切换到内核态的唯一途径——>中断/异常/陷入
内核态切换到用户态的途径——>设置程序状态字

当进行I/O操作的时候,由内核态到用户态的拷贝转移是极其耗费 cpu 资源的,零拷贝是追求的目标。

Linux 的文件存储系统

1.最小单位是扇区,每个扇区512字节,
2.按块读取,每个块有八个扇区,即 4Kb,
3.inode 即索引节点,其中包含了文件的大小,创建等基本信息
4. 访问文件,通过inode来快速定位文件的地址,相关信息等。

硬链接 和 软连接:
硬链接 创建和源文件一样的文件,指向inode 节点,和源文件属于并行关系。
软连接 创建指向源文件inode 节点的文件,和源文件是串行的关系。
硬链接不能用着目录,会引起无限循环的问题。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: