【面经专栏】自己整理的操作系统面经

1、需要线程频繁加锁解锁的场景,怎么优化CPU性能

  1. 临界区处理很快,也许用 spin lock(自旋锁) 或者 lock-free 算法,临界区处理很慢, mutex 更合适
    • lock-free:是一种编程思想,指的是使用多线程的条件下,尽量少使用锁以降低线程之间互相阻塞的机会;在一系列访问 Lock-Free 操作的线程中,如果某一个线程被挂起,那么其绝对不会阻止其他线程继续运行
  2. 不要使用自旋锁,因为线程得不到锁的时候会一直在循环等待,不会进入休眠
  3. 使用互斥锁,结合条件变量,引入线程的等待与通知机制:执行的线程首先获取互斥锁,如果线程继续执行时,需要的条件不满足,则释放互斥锁,并进入等待状态;当线程继续执行需要的条件满足时,就通知等待的线程,重新获取互斥锁。但是在这种情况下,需要注意条件变量的虚假唤醒
  4. 通知的时候不要广播唤醒,只唤醒一个,缺点是可能永远不会唤醒某个线程
  5. 读多写少的场景,引入读写锁

2、listen函数的backlog含义

​ backlog参数表示内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不再接受新的客户端连接。在旧版本中,backlog指所有处于半连接状态(SYN_RCVD)和完全连接状态(ESTABLISHED)的socket上限。在新版本中,他只表示处于完全连接状态的socket上限,处于半连接状态的socket上限在一个系统文件中定义

3、将文件从内存中写入磁盘的系统调用

Linux内核为了达到最佳的磁盘操作效率,会把需要写入到磁盘的数据先在内存中缓存起来,在合适的时候才真正写入到磁盘中,这在绝大多数情况都是没有任何问题的,而且提高了系统的效率,但是如果系统宕机、掉电,就会有些文件内容不会保存下来。在Linux系统关机或者重启时,会自动把缓冲区的内容自动同步到磁盘中。我们也可以手工去执行sync命令,强制将文件缓冲内容写到磁盘,这个命令是通过调用sync系统调用来实现的

由于IO操作会首先将数据放入内核缓冲区,所以在写的时候如果出现系统故障则缓冲区的数据可能会丢失,所以为了防止这种情况发生,以上两个函数使得内核缓冲区的数据立即写入磁盘。

void sync(void);将所有缓冲排入写队列,然后立即返回

3-2、缓冲区是什么,存在哪

https://blog.csdn.net/tonglin12138/article/details/85534308

我们从磁盘里取信息,我们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。

又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的CPU可以处理别的事情。

缓冲区就是一块内存区, 它用在输入输出设备和CPU之间,用来缓存数据 。它 使得低速的输入输出设备和高速的CPU能够协调工作 ,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作。

4、页面置换算法

页面置换又叫缺页中断算法,是为了解决:

在地址映射过程中,若在页面中发现所要访问的页面不在虚拟内存中,则产生缺页中断。当发生缺页中断时,如果操作系统内存中没有空闲页面,则操作系统必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。而用来选择淘汰哪一页的规则叫做页面置换算法

  • 最佳置换法(OPT)
  • 先进先出置换法(FIFO)
  • 最近最久未使用置换法(LRU)
  • 时钟置换(CLOCK)

下面分别介绍

最佳置换(OPT)

每次选择淘汰的页面时以后永不使用的页面,或者是在最长时间内不会使用的页面,保证最低的缺页率

缺页率 = 缺页中断次数 / 访问页面总数

注意缺页中断次数≠页面置换次数,因为内存中还有空闲的内存块时,只发生了缺页中断,然后调入页,没有发生页面置换

但是只有在进程执行过程中预测不了下一次将要访问的页面是谁,所以该算法在实际应用上时实现不来的

先进先出置换法(FIFO)

每次置换出内存的页是进入内存时间最久的页

实现:内存页面根据调入内存的时间进行排序,进入内存时间最早的排在队首,这样每次只需要置换出队首页面,将置换进内存的页面排在最后

最近最久未使用置换法(LRU)

每次置换出内存的页面时最近最久没有使用到的页面

实现:和FIFO一样维护一个队列,但是排序规则不再是进入内存的时间了,每次页面访问后,若目标页面存在,将它排到队首,表示它是最近使用的一个页面,若目标页面不在内存中,将目标页面调入内存,排在队首(还表示它是最近使用的页面);将队尾页面(最不常使用)置换出内存

时钟置换(CLOCK)

使用链接指针将内存中的页面连成一个循环队列,为每个页面设置一个访问位。某页被访问后,访问位置1。淘汰页面时,检查页的访问位,若为0就置换出内存,若为1就将其置为0(不置换出内存),继续看下一个页面,直到第一轮扫描结束要是还没有置换出页面,就开始第二轮扫描(第二轮扫描一定会有被置0的页面存在,把它置换出去)

5、进程调度算法

无论是在批处理系统还是分时系统中,用户进程数一般都多于处理机数、这将导致它们互相争夺处理机。另外,系统进程也同样需要使用处理机。这就要求进程调度程序按一定的策略,动态地把处理机分配给处于就绪队列中的某一个进程,以使之执行。

常见的进程调度算法如下:

  • 先来先服务(FCFS)
  • 短作业优先(SJF)
  • 最短剩余时间优先(SRTN)
  • 时间片轮转
  • 高优先级优先
  • 多级反馈队列

下面分别介绍:

先来先服务(FCFS)

按照进程的请求顺序来调度CPU为为哪个进程服务

短作业优先(SJF)

非抢占式,每一次调度当前运行时间最短的进程(运行时间是估计出来的)。一个进程被调度后会一直执行完才会调度下一个

最短剩余时间优先(SRTN)

抢占式,一个新作业(进程请求CPU视为一个作业)到达时,与当前执行的作业比较剩余执行时间(也是估计的),谁最短就执行谁,说明当前进程有可能还没有执行完就挂起了

时间片轮转

所有就绪进程按照到达时间排队,每次调度队首的进程,分配给他一个时间片,时间片用完之后将该进程排在队尾(如果还没有执行完的话)

高优先级优先

每个进程分配优先级,每次调度优先级最高的进程

多级反馈队列

是时间片轮转与高优先级优先的结合

设置多个优先级队列,不同队列的优先级不同,不同队列的时间片也不同,如果进程在小时间片队列中没有完成作业,就把它排进时间片更大的队列中

6、动态分区分配算法

所谓动态分区分配,就是指内存在初始时不会划分区域,而是会在进程装入时,根据所要装入的进程大小动态地对内存空间进行划分,以提高内存空间利用率,降低碎片的大小

将空闲内存看做一张表的话,动态分区就是在不同的算法下按照某算法对应的规则从空闲内存表的一段取出来

  • 首次适应算法
  • 最佳适应算法
  • 最坏适应算法
  • 邻近适应算法

下面分别介绍:

首次适应算法

从低地址开始找,找到第一个满足大小的空闲分区

最佳适应算法

遍历空闲分区表,找到满足要求的最小的分区

最坏适应算法

遍历空闲分区表,找到满足要求的最大的空闲分区

邻近适应算法

每次分配内存都从上次查找结束的位置开始查找空闲分区表,找到第一个满足要求的空闲分区

7、磁盘调度算法

磁盘调度简言之,就是怎样移动磁头来寻找磁盘上的存储数据

常见的磁盘调度算法有:

  • 先来先服务
  • 最短寻道时间优先
  • 电梯扫描

下面分别介绍:

先来先服务

按照磁盘请求的顺序

最短寻道时间优先

优先调度与当前磁头所在磁道距离最近的磁道

电梯扫描

先按照一个方向(比如升序、降序)来进行磁盘调度,直到该方向上没有未完成的磁盘请求,然后再做反方向的(注意一定是从峰值/谷值做的反方向)

8、inode是什么

见纸质文档

9、原子操作

原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。将整个操作视作一个整体是原子性的核心特征。

多线程对int型变量x的操作中,x=1是原子操作。因为x是int类型,32位CPU上int占32位,在X86上由硬件直接提供了原子性支持。实际上不管有多少个线程同时执行类似x=1这样的赋值语句,x的值最终还是被赋的值(而不会出现例如某个线程只更新了x的低16位然后被阻塞,另一个线程紧接着又更新了x的低24位然后又被阻塞,从而出现x的值被损坏了的情况)。x++和++x不是原子操作。其实类似x++, x+=2, ++x这样的操作在多线程环境下是需要同步的。因为X86会按三条指令的形式来处理这种语句:从内存中读x的值到寄存器中,对寄存器加1,再把新值写回x 所处的内存地址。x=y不是原子操作。在X86上它包含两个操作:读取y至寄存器,再把该值写入x。读y的值这个操作本身是原子的,把值写入x也是原子的,但是两者合起来是不是原子操作呢?我个人认为x=y不是原子操作,因为它不是不可再分的操作

10、可重入锁

可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。

广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。

不可重入锁,与可重入锁相反,不可递归调用,递归调用就发生死锁

11、Linux ELF文件格式

对象文件(Object files)有三个种类:

  1. 可重定位的对象文件(Relocatable file)汇编之后的.o文件

    包含二进制代码和数据,能与其他可重定位对象文件在编译时合并创建出一个可执行文件。这是由汇编器汇编生成的 .o 文件。后面的链接器(link editor)拿一个或一些 Relocatable object files 作为输入,经链接处理后,生成一个可执行的对象文件 (Executable file) 或者一个可被共享的对象文件(Shared object file)。

  2. 可执行的对象文件(Executable file)链接以后的可执行文件

    包含可以直接拷贝进行内存执行的二进制代码和数据。用于提供程序的进程映像,加载的内存执行。文本编辑器vi、调式用的工具gdb、播放mp3歌曲的软件mplayer等等都是Executable object file

  3. 可被共享的对象文件(Shared object file)动态库文件 .so

    一种特殊的可重定位对象文件,能在加载时或运行时,装载进内存进行动态链接。连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。

12、哈希一致性

将服务器对232取模,232个数组成一个圆环,对要存放到服务器上的对象也对232取模,在环上标出对应的位置,将每个哈希后的换上对象存放到他顺时针方向的那个服务器上。这样就避免了某个服务器出现故障,使得对象哈希值与服务器哈希值不能对应的情况

13、单核多线程需要加锁么

在单核机器上写多线程程序,仍然需要线程锁。因为线程锁通常用来实现线程的同步和通信。在单核机器上的多线程程序,仍然存在线程同步的问题。因为在抢占式操作系统中,通常为每个线程分配一个时间片,当某个线程时间片耗尽时,操作系统会将其挂起,然后运行另一个线程。如果这两个线程共享某些数据,不使用线程锁的前提下,可能会导致共享数据修改引起冲突。

比如printf函数,当有两个线程A.B,当A写了一半时缓存满了,A会临时退出挂起,此时B就可能插入先写,然后迁入A继续,就会导致串行

14、为什么epoll的边沿触发模式必须使用非阻塞

ET 模式是一种边沿触发模型,在它检测到有 I/O 事件时,通过 epoll_wait 调用会得到有事件通知的文件描述符,对于每一个"有事”文件描述符,如可读,则必须将该文件描述符一直读到空(返回errno 或EAGAIN 为止),否则下次的 epoll_wait 不会返回余下的数据,会丢掉事件。

而如果你的文件描述符如果不是非阻塞的,那这个一直读(或一直写)到最后读完了(或写完了)不会直接返回errno 或EAGAIN ,而是阻塞在那里等待新的数据到来,不会继续下一个while循环。

15、CPU是怎么知道进程快执行完了的(进程控制块)

通过进程控制块PCB

进程控制块(PCB)是系统为了管理进程设置的一个专门的数据结构。系统用它来记录进程的外部特征,描述进程的运动变化过程。同时,系统可以利用PCB来控制和管理进程,所以说,PCB(进程控制块)是系统感知进程存在的唯一标志

PCB保存了一切与进程运行状态相关的信息,包括程序计数器、运行时栈、CPU寄存器值以及文件描述符等相关资源,它代表进程接受操作系统的调度

15-1 进程控制块保存在哪里

  • 从物理上看,它可能存在于两个地方,一处是内存中,另一处是外存中的swap空间,当进程被挂起或者内存空间不够时,进程控制块可能会被置换到swap空间中去;
  • 从逻辑上看,它会来往于各个队列之中,例如正在运行时在ready queue中等待调度器的唤醒,例如在某个I/O的waiting queue中等待获得访问I/O资源的权限等等。

16、消息中间件(消息队列)的作用,具体分类,怎样传递消息

消息中间件也可以称消息队列。消息中间件是基于队列与消息传递技术,在网络环境中为应用系统提供同步或异步、可靠的消息传输的支撑性软件系统

发送者将消息发送给消息服务器,消息服务器将消息存放在若干队列中,在合适的时候再将消息转发给接收者

分类:

  1. 点对点:使用queue作为通信载体 。消息生产者生产消息发送到queue中,然后消息消费者从queue中取出并且消费消息。消息被消费以后,queue中不再存储,所以消息消费者不可能消费到已经被消费的消息。 Queue支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费。
  2. 发布/订阅:Pub/Sub发布订阅(广播):使用topic作为通信载体 。消息生产者(发布)将消息发布到topic中,同时有多个消息消费者(订阅)消费该消息。和点对点方式不同,发布到topic的消息会被所有订阅者消费。

17、线程之间可以相互访问么

从OS角度,所有线程都能访问进程整个地址空间;从C语言角度,线程可以访问全局变量,只能在进入函数时访问局部变量,每个线程只能访问自己的线程变量拷贝;但是,如果指针作恶,语言上的约束就失效了

19、Linux top中的used和free

使用中的内存总量(used)指的是现在系统内核控制的内存数,空闲内存总量(free)是内核还未纳入其管控范围的数量。纳入内核管理的内存不见得都在使用中,还包括过去使用过的现在可以被重复利用的内存,内核并不把这些可被重新使用的内存交还到free中去,因此在linux上free内存会越来越少,但不用为此担心

20、数据包从网卡到用户进程整个过程

网卡收到数据包,DMA到内核内存,中断通知内核数据有了,内核按轮次处理消耗数据包,一轮处理完成后,开启硬中断(开硬中断的作用是可以接受后续的硬件中断请求)

DMA:外部数据进入内存之前,通过DMA暂存(因为CPU可能很忙)

21、进程通信中共享内存是怎样实现的

进程间通信的主要方式有,管道,有名管道,消息队列,共享内存,socket等方式,共享内存是最高效的进程间通信的方式,因为把同一块物理内存的地址空间映射到不同进程的地址空间当中,那么不同的进程之间通信,通过直接修改地址空间当中的内存即可

共享内存不涉及进程之间的任务数据传输。

这种高效率的问题是:必须使用使用其他辅助手段来同步进程对共享内存的访问,否则会产生竞态条件。因此,共享内存通常和其他进程通信方式一起使用

共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。

22、Linux中统计某文件中某个字符串出现的次数

grep -o 字符串 文件名|wc -l

wc -l:统计输出信息的行数,因为已经过滤得只剩一般文件了,所以统计结果就是一般文件信息的行数,又由于一行信息对应一个文件,所以也就是文件的个数。

23、无锁编程

多线程间是通过互斥锁与条件变量来保证共享数据的同步的,互斥锁主要是针对过程加锁来实现对共享资源的排他性访问。很多时候,对共享资源的访问主要是对某一数据结构的读写操作,如果数据结构本身就带有排他性访问的特性,也就相当于该数据结构自带一个细粒度的锁,对该数据结构的并发访问就能更加简单高效,这就是C++11提供的原子数据类型< atomic >。下面解释两个概念:

原子操作:顾名思义就是不可分割的操作,该操作只存在未开始和已完成两种状态,不存在中间状态;
原子类型:原子库中定义的数据类型,对这些类型的所有操作都是原子的,包括通过原子类模板std::atomic< T >实例化的数据类型,也都是支持原子操作的。

  • 现在有了原子操作的支持,对单个基础数据类型的读、写访问可以不用锁保护了

  • 但对于复杂数据类型比如链表,有可能出现多个线程在链表同一位置同时增删节点的情况,这将会导致操作失败或错序。所以我们在对某节点操作前,需要先判断该节点的值是否跟预期的一致,如果一致则进行操作,不一致则更新期望值,这几步操作依然需要实现为一个RMW(Read-Modify-Write)原子操作,这就是前面提到的CAS(Compare And Swap)原子操作,它是无锁编程中最常用的操作

无锁编程是基于原子操作的,对基本原子类型的共享访问由load()与store(val)即可保证其并发同步,对抽象复杂类型的共享访问则需要更复杂的CAS来保证其并发同步,并发访问过程只是不使用锁机制了,但还是可以理解为有锁行为的,其粒度很小,性能更高。对于某个无法实现为一个原子操作的并发访问过程还是需要借助锁机制来实现。

23-2、CAS的实现原理

CAS,是Compare and Swap的简称,在这个机制中有三个核心的参数:

  • 主内存中存放的共享变量的值:V(一般情况下这个V是内存的地址值,通过这个地址可以获得内存中的值)
  • 工作内存中共享变量的副本值,也叫预期值:A
  • 需要将共享变量更新到的最新值:B
img

如上图中,主存中保存V值,线程中要使用V值要先从主存中读取V值到线程的工作内存A中,然后计算后变成B值,最后再把B值写回到内存V值中。多个线程共用V值都是如此操作。CAS的核心是在将B值写入到V之前要比较A值和V值是否相同,如果不相同证明此时V值已经被其他线程改变,重新将V值赋给A,并重新计算得到B,如果相同,则将B值赋给V。

值得注意的是CAS机制中的这步步骤是原子性的(从指令层面提供的原子操作),所以CAS机制可以解决多线程并发编程对共享变量读写的原子性问题。

23-3 多个线程同时在对同一块内存进行CAS操作的话,那不就有可能出问题吗

首先,CAS操作是一个原子操作,所谓原子操作就是指在执行期间不会被其他线程打断,要么执行完毕,要么不执行

  • 在多个线程同时CAS的情况下是不会发生多个线程CAS成功的情况的,因为计算机底层实现保证了V指向内存的互斥性和立即可见性,可以理解为CAS操作是底层保证的线程安全

  • 一个线程T在CAS操作时,其他线程无法访问V指向的内存地址,并且一旦T更新了V指向内存中的值,其他所有线程的V指向内存都变得无效。

24、Linux线程挂掉对整个进程的影响

严格的说没有“线程崩溃”,只是触发了SIGSEGV (Segmentation Violation/Fault)。如果没有设置对应的Signal Handler操作系统就自动终止进程(或者说默认的Signal Handler就是终止进程);如果设置了,理论上可以恢复进程状态继续跑(用longjmp之类的工具)

SIGSEGV :段错误。进程进行了无效的内存访问,默认处理时终止进程

线程有自己的 栈,但是没有单独的 堆,也没有单独的虚拟地址空间。只有进程有自己的 虚拟地址空间,而这个 虚拟地址空间中经过合法申请的部分叫做 process space。Process space 之外的地址都是非法地址。当一个线程向非法地址读取或者写入,无法确认这个操作是否会影响同一进程中的其它线程,所以只能是整个进程一起崩溃。

25、linux下如何快速将文件每行倒序输出

tac命令,tac 是 cat 的反写,功能与 cat 命令刚好相反,cat 是顺序输出文件每一行到屏幕上,tac 是反序输出文件每一行到屏幕上

26、linux下如何查看指定端口有多少tcp连接、查看端口被哪个进程占用

统计80端口连接数

netstat -nat | grep -i "80" | wc -l

查看端口被哪个进程占用

netstat -tunlp | grep 端口号

27、什么是句柄

句柄就是个数字,一般和当前系统下的整数的位数一样,比如32bit系统下就是4个字节。这个数字是一个对象的唯一标示,和对象一一对应。这个对象可以是一个块内存,一个资源

句柄的作用就是在 C 语言环境下代替 C++ 的对象指针来用的。创建句柄就是构造,销毁句柄就是析构,用句柄调用函数相当于传入this指针。

28、如何设计一个高并发的分布式服务器

  1. 系统拆分:将系统根据业务场景拆分为多个子系统
  2. 系统拆分之后,可以令每一个系统使用一个数据库
  3. 引入缓存:使用数据库硬抗流量访问显然是不符合实际的,因此就需要引入缓存,令大量的读操作走缓存,写操作去走数据库,这样可以抗住更多的请求
  4. 考虑缓存使用不当而带来的问题,例如缓存穿透,缓存雪崩
  5. 使用消息队列:做一个异步削峰,来慢慢的处理请求,提升并发性能
  6. 当数据库中的数据量过于庞大之后,这时候尽管是没有并发的操作,也会使查询写入的速率变慢,这时候就需要考虑将数据库轻量化,做分库分表操作,这样不仅能够抗住更多的并发请求,也能加快SQL的执行效率
  7. 读写分离:将写库作为数据库的主数据,读库做为从库,是请求数据进一步分流,要考虑的问题就是数据同步,数据的主从一致性等

29、top命令详解

https://www.cnblogs.com/niuben/p/12017242.html

30、CPU分配时间片以谁为单位(进程/线程)

以进程为单位,时间片也属于资源,进程是CPU分配资源的最小单位。一个进程内的所有线程共享该进程的时间片

31、浏览器有缓存的话,客户端还需要和浏览器连接么

需要,需要访问服务端获知浏览器缓存是否过期,没有过期就去浏览器缓存中找(服务器返回304状态码),过期的话服务端就会发新的

32、fork之后父子进程的内存关系

调用fork之后,父子进程都会从fork()的返回处开始执行.

父子进程将执行相同的程序段,但是拥有各自不同的堆、栈、数据段,每个子程序都可修改各自的数据段,堆段,和栈段

调用fork()之后先执行哪个进程的是由Linux下专有文件/proc/sys/kernel/sched_child_runs_first的值来确定的(值为0父进程先执行,非0子进程先执行)

  • 代码段是相同的,所以代码段是没必要复制的,因此内核将代码段标记为只读,这样父子进程就可以安全的共享此代码段了。fork之后在进程创建代码段时,新子进程的进程级页表项都指向和父进程相同的物理页帧
  • 对于父进程的数据段,堆、栈中的各页,由于父子进程要相互独立,所以我们采用写时复制的技术,来最大化的提高内存以及内核的利用率。刚开始,内核做了一些设置,令这些段的页表项指向父进程相同的物理内存页。调用fork之后,内核会捕获所有父进程或子进程针对这些页面的修改企图(说明此时还没修改)并为将要修改的页面创建拷贝。系统将新的页面拷贝分配给被内核捕获的进程,还会对子进程的相应页表项做适当的调整,现在父子进程就可以分别修改各自的上述段,不再互相影响了

33、可重入与线程安全

线程安全函数

https://blog.csdn.net/csdnnews/article/details/82321777?ops_request_misc=&request_id=&biz_id=102&utm_term=%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-0-82321777.nonecase&spm=1018.2226.3001.4187

  • 当多个线程访问某个方法时,不管你通过怎样的调用方式、或者说这些线程如何交替地执行,我们在主程序中不需要去做任何的同步,这个方法的结果行为都是我们设想的正确行为,那么我们就可以说这个方法是线程安全的。
  • 确保线程安全:要确保函数线程安全,主要需要考虑的是线程之间的共享变量。属于同一进程的不同线程会共享进程内存空间中的全局区和堆,而私有的线程空间则主要包括栈和寄存器。因此,对于同一进程的不同线程来说,每个线程的局部变量都是私有的,而全局变量、局部静态变量、分配于堆的变量都是共享的。在对这些共享变量进行访 问时,如果要保证线程安全,则必须通过加锁的方式。
  • 线程不安全的后果: 线程不安全可能导致的后果——共享变量的值由于不同线程的访问,可能发生不可预料的变化,进而导致程序的错误,甚至崩溃

可重入函数

  • 概念:所谓“重入”,常见的情况是,程序执行到某个函数A()时,收到信号,于是暂停目前正在执行的函数A(),转到信号处理 函数B(),而这个信号处理函数的执行过程中,又恰恰也会进入到刚刚执行的函数A(),这样便发生了所谓的重入。此时如果A()能够正确的运行,而且处 理完成后,之前暂停的A()也能够正确运行,则说明它是可重入的。

  • 确保可重入:

    ​ 1、不在函数内部使用静态或全局数据
    ​ 2、不返回静态或全局数据,所有数据都由函数的调用者提供。
    ​ 3、使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
    ​ 4、不调用不可重入函数。

  • 不可重入的后果:像信号处理函数这样需要重入的情况中。如果信号处理函数中使用了不可重入的函数,则可能导致程序的错误甚至崩溃

可重入与线程安全

​ 可重入与线程安全并不等同。一般说来,可重入的函数一定是线程安全的,但反过来不一定成立。

img

34、Linux从文件中读取数据的过程

  1. 找到该文件对应的inode号
  2. 通过inode号,获得inode的信息(文件的字节数、拥有者ID、组ID、读写执行权限、时间戳、块存储位置)
  3. 根据inode信息,找到文件在磁盘上对应的块(block),读出数据

35、IO重定向

  • 输入的数据流:<– 标准输入(stdin(standard input)),默认接受来自键盘的输入
  • 输出的数据流:–> 标准输出(stdout(standard output)),默认输出到终端窗口

IO重定向就是:

  • 输入本来默认是键盘,我们改成其他输入,就是输入重定向 :例如从文本文件里输入。

  • 本来输出的位置是显示器,我们改成其他输出,就是输出重定向:例如输出到文件。

36、虚拟地址空间有多大

由操作系统位数决定,64位操作系统的虚拟内存空间是264字节(8GB),32位操作系统的是232字节(4GB)

37、Linux怎样监控网络情况

  • netstat命令

    列出系统上所有的网络套接字连接情况,包括 tcp, udp 以及 unix 套接字,另外它还能列出处于监听状态(即等待接入请求)的套接字。如果你想确认系统上的 Web 服务有没有起来,你可以查看80端口有没有打开。

    1. 列出所有连接:netstat -a
    2. 只列出 TCP 或 UDP 协议的连接:netstat -at #列出所有TCP协议的连接 netstat -au #列出所有UDP协议的连接
    3. 查看监听状态的连接:netstat -tuln
  • nethogs命令

    用于监控Linux系统上运行的每个进程或应用程序的实时网络流量带宽使用情况。 它仅提供基于每个进程的网络带宽使用情况的实时统计信息

    万一出现带宽使用突然激增的情况,用户迅速打开nethogs,就可以找到导致带宽使用激增的进程。nethogs可以报告程序的进程编号(PID)、用户和路径。

    命令:sudo nethogs

38、ICMP包探测对带宽的影响

  • ICMP是“Internet Control Message Protocol”(Internet控制消息协议)的缩写。它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。我们经常使用的用于检查网络通不通的Ping命令,这个“Ping”的过程实际上就是ICMP协议工作的过程。

  • 可以利用操作系统规定的ICMP数据包最大尺寸不超过64KB这一规定,向主机发起“Ping of Death”(死亡之Ping)攻击。“Ping of Death” 攻击的原理是:如果ICMP数据包的尺寸超过64KB上限时,主机就会出现内存分配错误,导致TCP/IP堆栈崩溃,致使主机死机。

  • 此外,向目标主机长时间、连续、大量地发送ICMP数据包,也会最终使系统瘫痪。大量的ICMP数据包会形成“ICMP风暴”,使得目标主机耗费大量的CPU资源处理,疲于奔命。

怎样应对

对于“Ping of Death”攻击,可以采取两种方法进行防范:

  1. 第一种方法是在路由器上对ICMP数据包进行带宽限制,将ICMP占用的带宽控制在一定的范围内,这样即使有ICMP攻击,它所占用的带宽也是非常有限的,对整个网络的影响非常少;
  2. 第二种方法就是在主机上设置ICMP数据包的处理规则,最好是设定拒绝所有的ICMP数据包。
    • 设置ICMP数据包处理规则的方法也有两种,一种是在操作系统上设置包过滤,另一种是在主机上安装防火墙

39、进程切换与线程切换的主要区别

进程切换涉及到虚拟地址空间的切换而线程切换则不会。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。

进程切换需要分两步:

  1. 切换页目录以使用新的地址空间
  2. 切换内核栈和硬件上下文

对于线程切换,第一步是不需要做的,第二步是进程和线程切换都要做的,所以进程对此比线程切换代价大。

上下文切换就是从当前执行任务切换到另一个任务执行的过程。但是,为了确保下次能从正确的位置继续执行,在切换之前,会保存上一个任务的状态

进程上下文切换要切换虚拟地址空间(通过刷新页表缓存实现),切换硬件上下文和栈

线程上下文切换仅仅切换硬件上下文和栈

进程硬件上下文切换,即保存当前进程运行的 cpu寄存器的配置,载入下一个要运行的进程的cpu寄存器配置

39-2、协程与线程的区别

协程(coroutine)是一种程序运行的方式,即在单线程里多个函数并发地执行

  1. 协程涉及到函数的切换, 多线程涉及到线程的切换, 所以都有执行上下文, 但是协程不是被操作系统内核所管理, 而完全是由程序所控制(也就是在用户态执行), 这样带来的好处就是性能得到了很大的提升, 不会像线程那样需要在内核态进行上下文切换来消耗资源,因此协程的开销远远小于线程的开销
  2. 协程适合执行大量的I/O 密集型任务, 而线程在这方面弱于协程
  3. 同一时间, 在多核处理器的环境下, 多个线程是可以并行的,但是运行的协程的函数却只能有一个其他的协程的函数都被suspend, 即协程是并发的
  4. 由于协程在同一个线程中, 所以不需要同步方法比如互斥锁、信号量等,并且不需要来自操作系统的支持
  5. 在协程之间的切换不需要涉及任何系统调用或任何阻塞
  6. 通常的线程是抢先式(即由操作系统分配执行权), 而协程是由程序分配执行权

https://www.cnblogs.com/theRhyme/p/14061698.html

39-3 协程的应用场景

协程的应用场景主要在于 :I/O 密集型任务

这一点与多线程有些类似,但协程调用是在一个线程内进行的,是单线程,切换的开销小,因此效率上略高于多线程;

当程序在执行 I/O 时操作时,CPU 是空闲的,此时可以充分利用 CPU 的时间片来处理其他任务;

在单线程中,一个函数调用,一般是从函数的第一行代码开始执行,结束于 return 语句、异常或者函数执行(也可以认为是隐式地返回了 None );

有了协程,我们在函数的执行过程中,如果遇到了I/O密集型任务,函数可以临时让出控制权,让 CPU 执行其他函数,等 I/O 操作执行完毕以后再收回其他函数的控制权

https://www.cnblogs.com/theRhyme/p/14061698.html

40、传统IO,零拷贝,mmap

传统IO的常规文件读操作(调用read函数)中,函数的调用过程:

  1. 进程发起读文件请求
  2. 进程找到该文件的inode
  3. 通过inode将文件读入内核缓冲区中
  4. 再将内核缓冲区中的数据复制到进程的虚拟地址空间中

这种用户态空间到内核态空间之间的复制是完全没有必要的(第3步),采用mmap进行内存映射就能实现零拷贝技术。

  • mmap将一个文件映射到进程的虚拟地址空间中,实现文件的磁盘地址与进程的虚拟地址空间中的一段虚拟地址之间的一一对应关系。进程采用指针的方式读写这段内存,系统自动更改该块内存对应的物理磁盘上的内容。
  • mmap适用于对同一块区域频繁读写的场景
  • mmap能够以此实现进程间共享内存

41、堆、栈和一级缓存、二级缓存

  • 为什么需要缓存
    CPU运行速率很快,内存的就很慢,所以就需要缓存,缓存分为一级二级三级,越往下优先级越低,成本越低,容量越大
  • CPU读写速率
     寄存器>一级缓存>二级缓存

栈是在一级缓存里面的,堆是属于二级缓存,所以栈的效率比堆的高

42、Linux查看文件的前20-30行

head -30 ett.txt | tail -11

命令解释:

head -n 30 xxx.txt 等效于 head -30 xxx.txt 取文件前30行内容
tail -11 xxx.txt 取文件后11行内容
| 管道命令连接 将head取出的30行内容作为tail的输入

43、进程通信的优缺点

无名管道简单方便.但局限于单向通信的工作方式.并且只能在创建它的进程及其子孙进程之间实现管道的共享

有名管道虽然可以提供给任意关系的进程使用.但是由于其长期存在于系统之中,使用不当容易出错.所以普通用户一般不建议使用。

信号
a、这种通信可携带的信息极少。不适合需要经常携带数据的通信。
b、不具备同步机制,类似于中断,什么时候产生信号,进程是不知道的。

消息队列可以不再局限于父子进程.而允许任意进程通过共享消息队列来实现进程间通信.并由系统调用函数来实现消息发送和接收之间的同步.从而使得用户在使用消息缓冲进行通信时不再需要考虑同步问题.使用方便,但是消息队列中信息的复制需要额外消耗CPU的时间.不适宜于信息量大或操作频繁的场合。

共享内存针对消息缓冲的缺点改而利用内存缓冲区直接交换信息,无须复制,快捷、信息量大是其优点。但是共享内存的通信方式是通过将共享的内存缓冲区直接附加到进程的虚拟地址空间中来实现的.因此,需要解决这些进程之间的读写操作的同步问题。必须由各进程利用其他同步工具解决。另外,由于内存实体存在于计算机系统中.所以只能由处于同一个计算机系统中的诸进程共享,不方便网络通信。

socket
优点:传输数据为字节级,传输数据可自定义,数据量小;适合于客户端和服务器端之间信息实时交互;可以加密,数据安全性强

socket的缺点是:需对传输的数据进行解析;

不同的进程通信方式有不同的优点和缺点.因此.对于不同的应用问题,要根据问题本身的情况来选择进程间的通信方式。

44、以前是没有堆区的,为什么需要堆区

因为栈的数据是连续存储的,pop时后申请的内存必须早于先申请的内存失效,所以栈不利于动态地管理并且有效地利用宝贵的内存资源

45、给文件排序去重

单个文件,对其内容进行排序,使用sort命令:

sort a.txt

去重加-u选项

sort -u a.txt

输出到一个文件

sort -o a.txt_sort -u a.txt

如果是100个1G大小的文件呢?如何进行排序去重呢?

  • 第一步:先对单个文件进行排序去重
  • 第二步:使用sort的合并排序
sort -o a1.txt -u a1.txt
sort -o a2.txt -u a2.txt
...
sort -o a100.txt -u a100.txt

sort -o a.txt -u -m a*.txt
//-m选项 表示merge已经有序的文件,如果文件事先没有排好序,这个可能会出错。

在执行merge的时候,你可能会遇到空间不足,无法写/tmp/sortXXXX的错误。因为是多个大文件的合并操作,内存不够用,肯定是要写临时文件的。默认sort是写到/tmp目录下。

可以通过-T 选项指定一个较大空间的磁盘目录作为sort的临时文件目录。

sort -o a.txt -T /disk/temp -u -m a*.txt

https://www.jianshu.com/p/7a16d56544c3

46、内核栈和用户栈

每个进程有两个栈:用户栈、内核栈

用户栈在用户地址空间中,内核栈在内核地址空间中。

用户栈不难理解,用户栈是用户空间中的一块区域,用于保存用户进程的子程序间相互调用的参数、返回值以及局部变量等信息。在linux系统中,用户栈的大小一般为8M。可以通过ulimit -s来手动设置。

进程用户栈和内核栈的切换

当进程由于中断或系统调用从用户态转换为内核态时,进程所使用的栈也要从用户栈切换到内核栈。系统调用实质就是通过指令产生中断(软中断)。进程由于中断而陷入到内核态,**进程进入内核态之后,首先把用户态的堆栈地址保存在内核态堆栈中,然后设置堆栈寄存器地址为内核栈地址,这样就从用户栈转换成内核栈。**当进程从内核态转换到用户态时,将堆栈寄存器的地址再重新设置成用户态的堆栈地址(即终端前进程在用户态执行的位置),这一过程也成为现场恢复

46-2、进程几个堆几个栈、线程几个堆几个栈

根据操作系统,Linux的进程有2个栈:一个用户栈一个内核栈,进程只有一个堆

一个进程中的某个线程拥有自己的栈,所有的线程共享所属进程的堆

47、Linux分析系统状态

  • CPU:运行 top 命令,按下大写的 P,可以按 CPU 使用率来排序显示
  • 内存:运行 top,然后按下大写的 M 可以按内存使用率来排序显示
  • 磁盘:使用iotop命令,查看哪个进程使用磁盘读写最多
  • 网络:使用nethogs找出使用带宽最多的进程

https://blog.csdn.net/gaofei0428/article/details/118851250?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522163050090016780255221384%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=163050090016780255221384&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_v2~rank_v29-1-118851250.pc_search_result_hbase_insert&utm_term=Linux%E5%88%86%E6%9E%90%E7%B3%BB%E7%BB%9F%E7%8A%B6%E6%80%81&spm=1018.2226.3001.4187

48、进程优先级字段,CPU调度与优先级的关系

  • PRI表示进程的优先级,NI即nice,表示可被执行进程的优先级的修正值。

PRI值越小越快被执行

49、多进程和多线程的区别

  • 多进程中占用内存多,切换复杂,CPU利用率低;多线程中占用内存少,切换简单,CPU利用率高。

  • 多进程中编程简单,调试简单;多线程中编程复杂,调试复杂。

  • 多进程中进程间不会相互影响;多线程中一个线程挂掉将导致整个进程挂掉。

50、linux 下coredump文件生成定位方法

https://blog.csdn.net/u011721450/article/details/108276884

当程序运行的过程中异常终止或崩溃,操作系统会将程序当时的内存状态记录下来,保存在一个文件中,这种行为就叫做Core Dump(中文有的翻译成“核心转储”)。我们可以认为 core dump 是“内存快照”,但实际上,除了内存信息之外,还有些关键的程序运行状态也会同时 dump 下来,例如寄存器信息(包括程序指针、栈指针等)、内存管理信息、其他处理器和操作系统状态和信息。core dump 对于编程人员诊断和调试程序是非常有帮助的,因为对于有些程序错误是很难重现的,例如指针异常,而 core dump 文件可以再现程序出错时的情景。

定位步骤为:

  1. 开启core dump
  2. 执行./main, 生成的core
  3. GDB调试core文件,查看程序挂在哪个位置。当core dump 之后,使用命令 gdb program core 来查看 core 文件,其中 program 为可执行程序名,core 为生成的 core 文件名。

51、一个进程可以开多少个线程

默认情况下,一个线程的栈要预留1M的内存空间

如果一个进程的内存空间时2G的话,理论上一个进程中最多可以开2G/1M=2048个线程,但是内存当然不可能完全拿来作线程的栈,所以实际数目要比这个值要小。

52、线程通信方式

  • 消息队列:是最常用的一种,也是最灵活的一种,通过自定义数据结构,可以传输复杂和简单的数据结构。
  • 全局变量:进程中的线程间内存共享,这是比较常用的通信方式和交互方式。注:定义全局变量时最好使用volatile来定义,以防编译器对此变量进行优化
  • 互斥锁:互斥锁,条件变量都只用于同一个进程的各线程间
  • 条件变量
  • 信号量:可用于不同进程间的同步,当信号量用于进程间同步时,要求信号量建立在共享内存区。

53、怎样减少内存碎片

https://www.cnblogs.com/luntai/p/6287579.html

内部碎片(占了不用):内部碎片就是已经被分配出去(能明确指出属于哪个进程)却不能被利用的内存空间;

外部碎片(太小&不连续 -》没法用):外部碎片指的是还没有被分配出去(不属于任何进程),但由于太小了无法分配给申请内存空间的新进程的内存空闲区域。

从编程层面来说:

1.尽可能少开辟内存空间,尽量复用内存空间

2.使用完内存空间后,尽快释放掉。

3.合理使用Union可以减小内存占用

4.合理安排struct或者class中的成员变量的位置

5.已知大小的数组静态开辟,未知大小的采用动态开辟

6.可以自己手动维护内存分配。(在申请的内存很小时 有用)一次申请一大块内存,然后分成将其分成小块,每次用的时候将小块分配出去,释放的时候大块一起释放。

从OS层来说:

1.段页式内存管理: 现在普遍采用的段页式内存分配方式就是将进程的内存区域分为不同的段,然后将每一段由多个固定大小的页组成。通过页表机制,使段内的页可以不必连续处于同一内存区域,从而减少了外部碎片,然而同一页内仍然可能存在少量的内部碎片,只是一页的内存空间本就较小,从而使可能存在的内部碎片也较少。

54、CPU缓存

https://baike.baidu.com/item/CPU%E7%BC%93%E5%AD%98/3728308?fr=aladdin

在计算机系统中,CPU高速缓存(英语:CPU Cache,在本文中简称缓存)是用于减少处理器访问内存所需平均时间的部件。在金字塔式存储体系中它位于自顶向下的第二层,仅次于CPU寄存器。其容量远小于内存,但速度却可以接近处理器的频率

当处理器发出内存访问请求时,会先查看缓存内是否有请求数据。如果存在(命中),那么不用访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器。

缓存之所以有效,主要是因为程序运行时对内存的访问呈现局部性(Locality)特征。这种局部性既包括空间局部性(Spatial Locality),也包括时间局部性(Temporal Locality)。有效利用这种局部性,缓存可以达到极高的命中率。

55、SWAP 交换分区

https://baike.baidu.com/item/SWaP/2666174?fromtitle=%E4%BA%A4%E6%8D%A2%E5%88%86%E5%8C%BA&fromid=5188359&fr=aladdin

Linux中Swap(即:交换分区),类似于Windows的虚拟内存,就是当内存不足的时候,把一部分硬盘空间虚拟成内存使用,从而解决内存容量不足的情况。

56、Linux内存满了怎么办

释放缓存

什么是Cache Memory(缓存内存):

当你在读写文件的时候,Linux内核为了提高读写效率与速度,会将文件在内存中进行缓存,这部分内存就是Cache Memory(缓存内存)。即使你的程序运行结束后,Cache Memory也不会自动释放。这就会导致你在Linux系统中程序频繁读写文件后,你会发现可用物理内存会很少。

其实这缓存内存(Cache Memory)在你需要使用内存的时候会自动释放,所以你不必担心没有内存可用。如果你希望手动去释放Cache Memory(缓存内存)的话也是有办法的。

链接:https://www.jianshu.com/p/7a6781bd081c

57、Linux页大小,大页

https://blog.csdn.net/ybhuangfugui/article/details/106846269

Linux 会以页为单位管理内存,无论是将磁盘中的数据加载到内存中,还是将内存中的数据写回磁盘,操作系统都会以页面为单位进行操作,哪怕我们只向磁盘中写入一个字节的数据,我们也需要将整个页面中的全部数据刷入磁盘中。

Linux 同时支持正常大小的内存页和大内存页(Huge Page),绝大多数处理器上的内存页的默认大小都是 4KB,虽然部分处理器会使用 8KB、16KB 或者 64KB 作为默认的页面大小,但是 4KB 的页面仍然是操作系统默认内存页配置的主流;除了正常的内存页大小之外,不同的处理器上也包含不同大小的大页面,我们在 x86 处理器上就可以使用 2MB 的内存页。

过小的页面大小会带来较大的页表项增加寻址时 TLB(Translation lookaside buffer)的查找速度和额外开销;

过大的页面大小会浪费内存空间,造成内存碎片,降低内存的利用率;

58、内旋锁的内部实现

自旋锁的实现基于共享变量。一个线程通过给共享变量设置一个值来获取锁,其他等待线程查询共享变量是否为0来确定锁是否可用,然后在等待循环中自旋直到锁可用为止

对于锁时间短的可以用自旋锁,毕竟效率原高于互斥锁。而对于线程睡眠时间长的用互斥锁

58-2、互斥锁的底层实现

通过一个标志位,进行原子分配,用时置0,释放置1

59、中断处理函数中使用自旋锁

https://bbs.csdn.net/topics/350203885

在中断处理程序中可以使用自旋锁

但是在中断处理函数中不加限制地使用自旋锁会带来一个问题:假设自旋锁锁定了一个临界区,而且一个进程正在持有该锁,此时发生中断,中断服务函数中又申请该自旋锁,那岂不是会造成死锁,因为中断服务函数在等待自旋锁,但是进程将永远得不到释放自旋锁的机会。

解决方案:

在中断中使用自旋锁时,内核会先禁止本地中断

猜你喜欢

转载自blog.csdn.net/weixin_44484715/article/details/120825055