1.阻塞/非阻塞IO,同步IO/异步IO,IO多路复用

1. IO请求的两个阶段:

  1. 等待资源阶段:IO请求一般需要请求特殊的资源(如磁盘、RAM、文件),当资源被上一个使用者使用没有被释放时,IO请求就会被阻塞,直到能够使用这个资源。
  2. 使用资源阶段:真正进行数据接收和发送,即数据在内核空间和数据空间之间的移动。

2. 在等待数据阶段,IO分为阻塞IO和非阻塞IO。(内核空间与外设进行交互过程来区分)

  1. 阻塞IO: 资源不可用时,IO请求一直阻塞,直到反馈结果(有数据或超时)。
  2. 非阻塞IO:资源不可用时,IO请求离开返回,返回数据标识资源不可用。在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。

3. 在使用资源阶段,IO分为同步IO和异步IO。(内核空间与用户空间的交互过程来区分)

  1. 同步IO:应用阻塞在发送或接收数据的状态,直到数据成功传输或返回失败。
  2. 异步IO:应用发送或接收数据后立刻返回,数据写入OS缓存,由OS完成数据发送或接收,并返回成功或失败的信息给应用。

4. IO多路复用

  • 也称为事件驱动IO,如平时用的select/epoll就是IO多路复用
  • IO多路复用的好处就在于单个进程就可以同时处理多个网络连接的IO
  • 它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

更多解释→http://blog.chinaunix.net/uid-28458801-id-4464639.html

阻塞和非阻塞模式下的read和write操作如下:

总结:

  • read总是在接收缓存区有数据的时候直接返回,而不是等到应用程序的数据充满才返回。如果此时缓冲区是空的,那么阻塞模式会等待,非阻塞则会返回-1并有EWOULDBLOCK或EAGAIN错误
  • 和read不太一样的是,在阻塞模式下,write只有在发送缓冲区足以容纳应用程序的输出字节时才会返回。在非阻塞的模式下,能写入多少则写入多少,并返回实际写入的字节数

2.进程、线程、协程、守护进程

进程

程序: 程序是一些保存在磁盘上的指令的有序集合,是静态的。

进程: 进程是程序执行的过程,包括了动态创建、调度和消亡的整个过程,进程是程序资源管理的最小单位 。

线程

线程就是我们程序执行的实体。 线程是操作系统能够进行运算调度的最小单位。线程被包含在进程之中,是进程中的实际运行单位, 一个进程内可以包含多个线程,一个进程至少有一个线程(主线程)线程是资源调度的最小单位

协程

协程是一种比线程更加轻量级的微线程。类比一个进程可以拥有多个线程,一个线程也可以拥有多个协程,因此协程又称为微线程。

守护进程

概述: 守护进程是脱离于终端并且在后台运行的进程

  • 脱离终端是为了避免在执行过程中的信息在终端上显示,并且进程也不会被终端所产生的终端信息所打断。

生命周期: 一般的守护进程的生命周期是从系统启动到系统终止运行,也可以通过杀死进程的方式结束守护进程的生命周期。

例子: 系统的服务进程

作用: 周期性执行某任务(只做某种单一的任务)

3.进程状态

  1. 新建状态:进程已经创建,但未被OS接纳为可执行进程。(还没有申请到相应的资源)
  2. 就绪态: 进程做好了准备,准备执行(只等待处理机)
  3. 执行状态:该进程正在执行。
  4. 阻塞状态:等待某事件发生才能执行,如等待I/O完成。
  5. 终止状态

4.进程间通信

1. 管道

管道的功能是将前一个命令的输出,当作另一个命令的输入。可以创建管道的时候创建两个文件描述符,一个作为管道的读端,一个作为管道的写端

  • 匿名管道没有实体,是一个伪文件,只能通过fork来复制父进程的fd文件描述符来达到通信的目的,所以它的通信范围是存在父子关系的进程
  • 有名管道,可以在不相关的进程间也能相互通信。因为命令管道,提前创建了一个类型为管道的设备文件,在进程里只要使用这个设备文件,就可以相互通信。

管道大小受限,是阻塞的,必须等其他进程来取才返回,而且是单向传输的,效率低下,不适合频繁的进程通信。 管道的优点是实现简单,能够保证我们的数据已经真的被其他进程拿走了。

2. 消息队列

消息队列解决了管道阻塞的问题,例如 a 进程要给 b进程发送消息,只需要把消息放在对应的消息队列里就行了,b 进程需要的时候再去对应的消息队列里取出来。
但消息队列通信过程中,存在用户态和内核态之间的数据拷贝开销,所以读大数据时很耗时间

3. 共享内存

共享内存这个通信方式解决了拷贝所消耗的时间。共享内存的机制是拿出一块虚拟地址空间,映射到相同的物理内存中。这样这个进程写入的东西,另一个进程马上就能看到了。但共享内存又存在多进程竞争资源的问题。

4. 信号量

信号量的本质就是一个计数器,用来实现进程之间的互斥与同步,而不是用于缓存进程间通信的数据。

5. 信号

上面的进程间通信都是常规状态下的工作模式。对于异常情况下的工作模式,就需要使用“信号”来通知进程了。

6. socket

前面说到的通信机制,都是工作与一台主机,如果要与不同主机的进程间通信,那么就需要socket通信了。socket同时还可以用于本地主机进程间通信。

5.死锁必要条件、解决死锁策略,写出死锁代码

必要条件

  1. 互斥条件,要求各个资源互斥,即在一段时间内某资源仅为一个进程所占有。如果这些资源都是可以共享的,那么多个进程直接共享即可,不会存在等待的尴尬场景
  2. 非抢占条件,要求进程所占有的资源使用完后主动释放即可,其他的进程休想抢占这些资源。原因很简单,如果可以抢占,直接拿就好了,不会进入尴尬的等待场景
  3. 请求与保持条件:
    进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
  4. 循环等待条件,存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。

预防死锁

  1. 破坏请求条件:一次性分配所有资源,这样就不会再有请求了;

  2. 破坏请保持条件:只要有一个资源得不到分配,也不给这个进程分配其他的资源:

  3. 破坏不可剥夺条件:当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源;

  4. 破坏环路等待条件:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反。

6.分析静态链接的不足,以及动态链接的特点

静态库:

静态库可以简单的看成一组目标文件的集合,即很多目标文件经过压缩打包后形成的文件。比如在我们日常编程中,如果需要使用printf函数,就需要包stdio.h的库文件,使用strlen时,又需要包string.h的库文件,可是如果直接把对应函数源码编译后形成的.o文件直接提供给我们,将会对我们的管理和使用上造成极大不便,于是可以使用“ar”压缩程序将这些目标文件压缩在一起,形成libx.a静态库文件。
注:静态库命名格式:lib + “库名称”+ .a(后缀) 例:libadd.a就是一个叫add的静态库

  • 静态链接:
    对于静态库,程序在编译链接时,将库的代码链接到可执行文件中,程序运行时不再需要静态库。在使用过程中只需要将库和我们的程序编译后的文件链接在一起就可形成一个可执行文件。
    • 优点:
      1. 发布程序的时候,不需要提供对应的库,因为已经打包到了可执行程序中
      2. 加载库的速度快
    • 缺点:
    1. 内存和磁盘空间浪费:
      静态链接方式对于计算机内存和磁盘的空间浪费十分严重。假如一个c语言的静态库大小为1MB,系统中有100个需要使用到该库文件,采用静态链接的话,就要浪费进100M的内存,若数量再大,那浪费的也就更多。例如:程序1和程序2都需要用到Lib.o,采用静态链接的话,那么物理内存中就会存放两份对应此文件的拷贝。
    2. 更新麻烦:
      比如一个程序20个模块,每个模块只有1MB,那么每次更新任何一个模块,用户都得重新下载20M的程序。
动态库:

程序在运行时才去链接动态库的代码,多个程序共享库的代码。一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。

  • 动态链接:
    由于静态链接具有浪费内存和模块更新困难等问题,提出了动态链接。基本实现思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将他们链接在一起形成一个完整的程序,而不是像静态链接那样把所有的程序模块都链接成一个单独的可执行文件。所以动态链接是将链接过程推迟到了运行时才进行。
    同样,假如有程序1,程序2,和Lib.o三个文件,程序1和程序2在执行时都需要用到Lib.o文件,当运行程序1时,系统首先加载程序1,当发现需要Lib.o文件时,也同样加载到内存,再去加载程序2当发现也同样需要用到Lib.o文件时,则不需要重新加载Lib.o,只需要将程序2和Lib.o文件链接起来即可,内存中始终只存在一份Lib.o文件。
  • 优点:
    1. 毋庸置疑的就是节省内存;
    2. 减少物理页面的换入换出;
    3. 在升级某个模块时,理论上只需要将对应旧的目标文件覆盖掉即可。新版本的目标文件会被自动装载到内存中并且链接起来;
    4. 程序在运行时可以动态的选择加载各种程序模块,实现程序的扩展。
  • 缺点:
    1. 发布程序的时候,需要将动态库提供给用户
    2. 动态库没有被打包到应用程序中,加载速度相对较慢 (其实速度还是挺快的)

7.并行和并发

并行:

指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。

并发:

指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

8.互斥锁、自旋锁、读写锁、悲观锁、乐观锁的应用场景

互斥锁

被锁住的代码执行时间比较长时,即上下文切换的时间比代码执行时间短,就应该用互斥锁

自旋锁

定义: 当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取(利用while循环)。这种采用循环加锁->等待的机制被称为自旋锁(spinlock)。自旋锁在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换。

使用场景: 被锁住的代码执行时间很短,即上下文切换的时间比你锁住的代码执行时间还要长,就应该选用互斥锁

读写锁

读写锁适用于能明确区分读操作和写操作的场景。读写锁在读多写少的场景,能发挥出优势。

悲观锁

悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。用于冲突概论比较高的场景

乐观锁

如果并发访问共享资源时,冲突概率非常低的话,就可以使用乐观锁,它的工作方式是,在访问共享资源时,不用先加锁,修改完共享资源后,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。(版本号机制)

但是,一旦冲突概率上升,就不适合使用乐观锁了,因为它解决冲突的重试成本非常高。

9.分页和分段

分段机制: 分段机制就是把虚拟地址空间中的虚拟内存组织成一些长度可变的称为段的内存块单元。段可以用来存放程序的代码、数据和堆栈,或者用来存放系统数据结构。

分页: 分页就是将这些段,例如代码段分成均匀的小块,然后这些给这些小块编号,然后就可以放到内存中去。相应地,内存空间分成若干个物理块,页和块的大小相等。可将用户程序的任一页放在内存的任一块中,实现了离散分配。

内存的分段和分页管理方式和由此衍生的一堆段页式等都属于内存的不连续分配。什么叫不连续分配?就是把程序分割成一块一块的装入内存,在物理上不用彼此相连,在逻辑上使用段表或者页表将离散分布的这些小块串起来形成逻辑上连续的程序。

在基本的分页概念中,我们把程序分成等长的小块。这些小块叫做“页(Page)”,同样内存也被我们分成了和页面同样大小的”页框(Frame)“,一个页可以装到一个页框里。在执行程序的时候我们根据一个页表去查找某个页面在内存的某个页框中,由此完成了逻辑到物理的映射。

分段和分页有很多类似的地方,但是最大的区别在于分页对于用户来说是透明的,分页是为了完成离散存储,所有的页面大小都一样,对程序员来说这就像碎纸机一样,出来的东西没有完整意义。但是分段不一样,分段不定长,分页由系统完成,分段有时在编译过程中会指定划分,因此可以保留部分逻辑特征,容易实现分段共享。

区别:

  1. 分页机制会使用大小固定的内存块,而分段管理则使用了大小可变的块来管理内存。
  2. 分页使用固定大小的块更为适合管理物理内存,分段机制使用大小可变的块更适合处理复杂系统的逻辑分区。
  3. 段表存储在线性地址空间,而页表则保存在物理地址空间。

分页存储的优缺点

页面是主存物理空间中划分出来的等长的固定区域。

优点是页长固定,因而便于构造页表、易于管理,且不存在外碎片。

缺点是页长与程序的逻辑大小不相关,且各页不是程序的独立模块,不便于实现程序和数据的保护

分段存储方式的优缺点

分页对程序员而言是不可见的,而分段通常对程序员而言是可见的,因而分段为组织程序和数据提供了方便。与页式虚拟存储器相比,段式虚拟存储器有许多优点:

优点:

  1. 段的逻辑独立性使其易于编译、管理、修改和保护,也便于多道程序共享。
  2. 段长可以根据需要动态改变,允许自由调度,以便有效利用主存空间。
  3. 方便编程,分段共享,分段保护,动态链接,动态增长

因为段的长度不固定,段式虚拟存储器也有一些缺点:

缺点:

  1. 主存空间分配比较麻烦。
  2. 容易在段间留下许多碎片,造成存储空间利用率降低。
  3. 由于段长不一定是2的整数次幂,因而不能简单地像分页方式那样用虚拟地址和实存地址的最低若干二进制位作为段内地址,并与段号进行直接拼接,必须用加法操作通过段起址与段内地址的求和运算得到物理地址。因此,段式存储管理比页式存储管理方式需要更多的硬件支持。

段页式存储

段页式存储组织是分段式和分页式结合的存储组织方法,这样可充分利用分段管理和分页管理的优点。

  1. 用分段方法来分配和管理虚拟存储器。程序的地址空间按逻辑单位分成基本独立的段,而每一段有自己的段名,再把每段分成固定大小的若干页。
  2. 用分页方法来分配和管理实存。即把整个主存分成与上述页大小相等的存储块,可装入作业的任何一页。程序对内存的调入或调出是按页进行的。但它又可按段实现共享和保护。
  3. 逻辑地址结构。一个逻辑地址用三个参数表示:段号S;页号P;页内地址d。
  4. 段表、页表、段表地址寄存器。为了进行地址转换,系统为每个作业建立一个段表,并且要为该作业段表中的每一个段建立一个页表。系统中有一个段表地址寄存器来指出作业的段表起始地址和段表长度。

优点:

  1. 它提供了大量的虚拟存储空间。
  2. 能有效地利用主存,为组织多道程序运行提供了方便。

缺点:

  1. 增加了硬件成本、系统的复杂性和管理上的开消。
  2. 存在着系统发生抖动的危险。
  3. 存在着内碎片。
  4. 还有各种表格要占用主存空间。

https://www.cnblogs.com/shenckicc/p/6884921.html

10.虚拟内存的作用,分页系统实现虚拟内存原理

虚拟内存

虚拟内存是内存管理的一种方式, 它在磁盘上划分出一块空间由操作系统管理,当物理内存耗尽时充当物理内存来使用。它将多个物理内存碎片和部分磁盘空间重定义为连续的地址空间,以此让程序认为自己拥有连续可用的内存。当物理内存不足时,操作系统会将处于不活动状态的程序以及它们的数据全部交换到磁盘上来释放物理内存,以供其它程序使用。

虚拟内存,是指具有请求调入功能和置换功能,能从逻辑上对内存容量加以扩充的一种存储器系统。其逻辑容量是由内存和外存容量之和所决定,其运行速度接近于内存速度,而每位的成本却又接近于外存。

作用:

  1. 它将主存看成是一个存储在磁盘空间上的地址空间的高速缓存,主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据。
  2. 它为每个进程提供了一致的地址空间,简化了内存管理。
  3. 它保护了每个进程的地址空间不被其他进程破坏。(操作系统为每个进程提供一个独立的“页表”)

11.页面置换算法的原理

在进程运行过程中,若要访问的页面不在内存中,而需把它们调入内存,但内存已无空闲空间时,为了保证该进程能够正常运行,系统必须从内存中调出一页程序或数据送到磁盘的对换区中。但应将那个页面调出,须根据一定的算法确定。通常,把选择换出页面的算法称为页面置换算法。

最佳置换算法(理想最好的)

其选择的被淘汰页面将是以后永不使用的,或许是在最长(未来)时间内不再被访问的页面。

先进先出(FIFO)页面置换算法(性能最差的)

该算法总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面将被淘汰。该算法实现简单,只需把一个进程已调入内存的页面按先后次序链接成一个队列,并设置一个指针,称为替换指针,是它总是指向最老的页面。

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

LRU置换算法是选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间t,当需淘汰一个页面时,选择现有页面中其t值最大的,即最近最久未被使用的页面予以淘汰。

简单的Clock置换算法(LRU的近似算法)

当利用简单的Clock算法时,只需为每页设置一个访问位,再将内存中的所有页面都通过链接指针链接成一个循环队列。当某页被访问时,其访问位被置1。置换算法在选择一页淘汰时,只需检查页的访问位,如果是0,就选择该页换出;若为1,就重新将它置0,暂不换出,给予该页第二次驻留内存的机会,再按照FIFO算法检查下一个页面。当检查到队列中的最后一个页面时,若其访问位仍为1,则再返回到队首去检查第一个页面。

12.进程和线程的区别?

  1. 进程是资源分配的基本单位,线程是CPU调度的最小单位;
  2. 线程依赖于进程而存在,线程是进程的一个实体,一个线程只能属于一个进程,一个进程可以有一个或多个线程。
  3. 最本质的区别是:进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其他进程造成影响,而线程只是一个进程中的不同执行路径。线程有自己的栈空间和局部变量,但线程之间没有单独的地址空间,同一类线程共享进程的代码和数据空间,一个线程死掉就等于整个进程死掉。所以还有以下区别:
  4. 进程调试简单可靠性高,但创建开销大;线程正好相反,开销小,切换速度快,但是编程调试相对复杂。
  5. 通信机制上,因为线程之间互不干扰,相互独立,进程的通信机制相对复杂,需要使用管道、信号、共享内存、套接字等通信机制,而线程由于共享数据段,所以通信机制很方便。

13.内核态和用户态

概念

内核态:

运行操作系统程序,操作硬件。当一个进程因为系统调用陷入内核代码中执行时处于内核运行态(内核态),此时特权级最高,为0级。

用户态

运行用户程序。当一个进程在执行用户自己的代码时处于用户运行态(用户态),此时特权级最低,为3级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态。

指令划分

特权指令:只能由操作系统使用、用户程序不能使用的指令。 举例:启动I/O 内存清零 修改程序状态字 设置时钟 允许/禁止终端 停机

非特权指令:用户程序可以使用的指令。 举例:控制转移 算数运算 取数指令 访管指令(使用户程序从用户态陷入内核态

特权环:R0、R1、R2和R3。

R0相当于内核态,R3相当于用户态;

内核态和用户态的区别

  • 处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理机是可被抢占的 ;
  • 而处于核心态执行中的进程,则能访问所有的内存空间和对象,且所占有的处理机是不允许被抢占的。

用户态和内核态的切换

  1. 系统调用
    • 这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作。
  2. 异常
    • 当cpu在执行运行在用户态下的程序时,发生了一些没有预知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关进程中,也就是切换到了内核态,如缺页异常。
  3. 外围设备的中断
    • 当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,

14.Reactor模式和Proactor模式

以读操作为例(类操作类似)。

在Reactor中实现读:

  • 注册读就绪事件和相应的事件处理器
  • 事件分离器等待事件
  • 事件到来,激活分离器,分离器调用事件对应的处理器。
  • 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。

    在Proactor中实现读:

  • 处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这种情况下,处理器无视IO就绪事件,它关注的是完成事件。
  • 事件分离器等待操作完成事件
  • 在分离器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自定义缓冲区,最后通知事件分离器读操作完成。
  • 事件分离器呼唤处理器。
  • 事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分离器。

两个模式不同的地方在于,Proactor用于异步IO,而Reactor用于同步IO。

参考http://www.cnblogs.com/dawen/archive/2011/05/18/2050358.html

总结:

  • Reactor模式,本质就是当IO事件(如读写事件)触发时,通知我们主动去读取,也就是要我们主动将socket接收缓存中的数据读到应用进程内存中。
  • Proactor模式,我们需要指定一个应用进程内的buffer(内存地址),交给系统,当有数据包到达时,则写入到这个buffer并通知我们收了多少个字节。

15.消息队列的作用?

解耦

异步

削峰

16.线程越多越好吗

线程多了,可以提高程序的执行效率,但并不是越多越好。

  • 虽然线程本身拥有很少的资源(在iOS中,默认主线程1M,子线程512K),但是更多的线程意味着更多的内存开销。创建线程也是需要CPU开销的。
  • 如果线程比核的数量多,则同一时间只能执行与核数量相等的线程数,线程过多会导致频繁的切换,消耗过多的CPU时间,降低了程序性能。
  • 使用多线程就可能出现线程安全问题,为了解决线程安全需要使用锁,进而可能会出现死锁问题。过多的线程会增加程序设计的复杂性,浪费更多精力去处理多线程通信和数据共享(多线程安全、多线程死锁)。

17.创建多少个线程合适呢?

CPU密集型

对于CPU密集型计算多线程本质上是提升CPU的使用率,所以对一个4核的CPU来说,理论上创建4个线程就可以了。在多创建只会增加线程切换的开销。所以对于CPU密集型的计算场景线程数 = CPU核数 + 1

1
2
3
加1的目的是计算(CPU)密集型的线程恰好在某时因为发生一个页
错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以确
保在这种情况下CPU周期不会中断工作。

I/O密集型

CPU和I/O的耗时如果是1:1那开2个线程是合适的,如果CPU和I/O的消耗是1:2那么开3个线程是合适的。

如图显示:CPU 在 A、B、C 三个线程之间切换,对于线程 A,当 CPU 从 B、C 切换回来时,线程 A 正好执行完 I/O 操作。这样 CPU 和 I/O 设备的利用率都达到了 100%。

线程等待时间所占比例越高,需要越多线程;线程CPU时间所占比例越高,需要越少线程。最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]

通过APM工具可以可以得到准确的耗时时间。

18.零拷贝

传统的文件传输

首先,期间共发生了4次用户态与内核态的上下文切换,因为发生了两次系统调用,一次是 read() ,一次是 write(),每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。

上下文切换到成本并不小,一次切换需要耗时几十纳秒到几微秒,虽然时间看上去很短,但是在高并发的场景下,这类时间容易被累积和放大,从而影响系统的性能。

其次,还发生了 4 次数据拷贝,其中两次是 DMA 的拷贝,另外两次则是通过 CPU 拷贝的,下面说一下这个过程:

  • 第一次拷贝,把磁盘上的数据拷贝到操作系统内核的缓冲区里,这个拷贝的过程是通过 DMA 搬运的。
  • 第二次拷贝,把内核缓冲区的数据拷贝到用户的缓冲区里,于是我们应用程序就可以使用这部分数据了,这个拷贝到过程是由 CPU 完成的。
  • 第三次拷贝,把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,这个过程依然还是由 CPU 搬运的。
  • 第四次拷贝,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程又是由 DMA 搬运的。

要想提高文件传输的性能,就需要减少 「用户态与内核态的上下文切换」和「内存拷贝」的次数。

零拷贝具体方法:https://mp.weixin.qq.com/s/P0IP6c_qFhuebwdwD8HM7w

19.线程安全

什么是线程安全

多个线程访问同一个对象时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,都不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。

多线程编程中的三个核心概念

原子性

这一点,跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。

通过锁和同步可以保证原子性

可见性

可见性是指,当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到。缓存更新不及时可能会导致这个问题)

通过volatile关键字可以保证可见性

顺序性

顺序性指的是,程序执行的顺序按照代码的先后顺序执行。

处理器为了提高程序整体的执行效率,可能会对代码进行优化,其中的一项优化方式就是调整代码顺序,按照更高效的顺序执行代码。

可通过volatile在一定程度上保证顺序性,另外还可以通过synchronized和锁来保证顺序性。synchronized和锁保证顺序性的原理和保证原子性一样,都是通过保证同一时间只会有一个线程执行目标代码段来实现的。

20.硬链接与软链接的区别

概念:

硬链接(相当于别名): A是B的硬链接(A和B都是文件名),则A的目录项中的inode节点号与B的目录项中的inode节点号相同,即一个inode节点对应两个不同的文件名,两个文件名指向同一个文件,A和B对文件系统来说是完全平等的。如果删除了其中一个,对另外一个没有影响。每增加一个文件名,inode节点上的链接数增加一,每删除一个对应的文件名,inode节点上的链接数减一,直到为0,inode节点和对应的数据块被回收。

软连接(相当于快捷方式): A是B的软链接(A和B都是文件名),A的目录项中的inode节点号与B的目录项中的inode节点号不相同,A和B指向的是两个不同的inode,继而指向两块不同的数据块。但是A的数据块中存放的只是B的路径名(可以根据这个找到B的目录项)。A和B之间是“主从”关系,如果B被删除了,A仍然存在(因为两个是不同的文件),但指向的是一个无效的链接。

区别:

硬链接:

  1. 不能对目录创建硬链接,原因是文件系统不能存在链接环,存在环的后果是会导致例如文件遍历等操作的混乱;
  2. 不能对不同的文件系统创建硬链接,即两个文件名要在相同的文件系统下;
  3. 不能对不存在的文件创建硬链接

软连接:

  1. 可以对目录创建软链接,遍历操作会忽略目录的软链接(软连接是文件,虽然它指向目录项,但不会把它当成目录处理);
  2. 可以跨文件系统;
  3. 可以对不存在的文件创建软链接,因为放的只是一个字符串,至于这个字符串是不是对于一个实际的文件,就是另外一回事了。

21.僵尸进程与孤儿进程的区别

孤儿进程: 父进程结束了,而它的一个或多个子进程还在运行,那么这些子进程就成为孤儿进程。子进程的资源由init进程(进程号PID = 1)回收。

僵尸进程: 子进程退出了,但是父进程还活着,父进程不去释放子进程的pcb(进程控制块),孩子就变成了僵尸进程

22.poll,epoll和select的优缺点

select

  1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  2. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  3. select支持的文件描述符数量太小了,默认是1024

poll

poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,其他的都差不多。
select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。但它没有最大连接数的限制,原因是它是基于链表来存储的。

epoll

对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。

对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果)

对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,主要和系统内存有关。epoll底层使用红黑树实现的。

23.物理内存、虚拟内存的区别、虚拟地址空间概念及区别

物理内存(内存条):当打开程序时,系统会将这些程序加载到物理内存上。

虚拟内存(硬盘):虚拟的不是物理内存,而是代替物理内存行使存储的功能,物理内存的运行程序的功能是无法用虚拟内存来完成的。

物理内存与虚拟内存的关系:当运行程序过多,物理内存不够用时,系统会将一部分硬盘空间当内存使用,这部分空间就是虚拟内存。

虚拟地址空间(作用:解决物理内存稀缺问题):系统为每个进程所分配的4GB虚拟地址空间(32位系统),用来存放进程的虚拟地址,再通过MMU(内存管理单元)将虚拟地址映射到物理内存地址。

每个进程创建加载的时候,会被分配一个大小为4G的连续的虚拟地址空间,虚拟的意思就是,其实这个地址空间时不存在的,仅仅是每个进程“认为”自己拥有4G的内存,而实际上,它用了多少空间,操作系统就在物理内存上划出多少空间给它,等到进程真正运行的时候,需要某些数据并且数据不在物理内存中,才会触发缺页异常,进行数据拷贝

虚拟地址空间中进行空间划分的作用:保护数据和将数据分类

24.虚拟地址空间划分

1. 保留区(受保护的地址)

保留区即为受保护的地址,大小为0~4K,位于虚拟地址空间的最低部分,未赋予物理地址(不会与内存地址相对应,因此其不会放任何内容)。任何对它的引用都是非法的,用于捕捉使用空指针和小整型值指针引用内存的异常情况。大多数操作系统中,极小的地址通常都是不允许访问的,如NULL。

2. 代码段

代码段也称正文段或文本段,通常用于存放程序执行代码(即CPU执行的机器指令)。一般C语言执行语句都编译成机器代码保存在代码段。通常代码段是可共享的,因此频繁执行的程序只需要在内存中拥有一份拷贝即可。代码段通常属于只读,以防止其他程序意外地修改其指令(对该段的写操作将导致段错误)。某些架构也允许代码段为可写,即允许修改程序。

3.数据段(.data段)

.data存放初始化了的且初始化的值不为0的全局变量

4. .bss段

存放未初始化及初始化为0的全局变量

打印未初始化的全局变量会看到值为0,因为存放于.bss段,操作系统会将.bss段的数据全部赋值为0

5. 堆空间(heap)

在《深入理解计算机系统》中的名称是运行时堆(由malloc创建),也就是说这里的堆空间是暂时没有的,当程序运行,new或malloc之后才会分配堆内存,由低地址向高地址增长。堆中内容是匿名的,不能按名字直接访问,只能通过指针间接访问。

6. 内存映射段(共享库)

也就是动态链接库,Windows下是.dll,Linux下是so

7. 栈空间(stack)

用于存放局部变量(非静态局部变量,C语言称为自动变量),分配存储空间时从上往下。栈和堆都是后进先出的数据结构。

8. 命令行参数

该段用于存放命令行参数的内容:argc和argv。

9. 环境变量

用于存放当前的环境变量,在Linux中用env命令可以查看其值。

虚拟地址空间的好处

  1. 方便编译器和操作系统安排程序的地址;
  2. 方便实现各个进程空间之间的隔离,互不干扰,因为每个进程都对应自己的虚拟地址空间;
  3. 实现虚拟存储,从逻辑上扩大了内存。

25.内核接受网络数据全过程

如上图所示,进程在 Recv 阻塞期间:

  • 计算机收到了对端传送的数据(步骤 ①)
  • 数据经由网卡传送到内存(步骤 ②)
  • 然后网卡通过中断信号通知 CPU 有数据到达,CPU 执行中断程序(步骤 ③)
  • 此处的中断程序主要有两项功能,先将网络数据写入到对应 Socket 的接收缓冲区里面(步骤 ④),再唤醒进程 A(步骤 ⑤),重新将进程 A 放入内核的工作队列中。

详细: https://zhuanlan.zhihu.com/p/63179839

26.epoll实现过程

  1. 内核帮我们在epoll文件系统里建了个file结点,并在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个rdllist双向链表,用于存储准备就绪的事件
  2. 把要监听的socket放到对应的红黑树上,所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说相应事件的发生时会调用这里的回调方法。它会把这样的事件放到上面的rdllist双向链表中,即就绪列表。
  3. 判断就绪列表是否为空,如果rdllist链表不为空,则将这里的事件复制到用户态内存(使用共享内存提高效率)中,同时将事件数量返回给用户。最后清空就绪列表。
  4. 如果监听的事件是LT模式,它发现soket上还有未处理的事件,则在清理就绪列表后,重新把该socket放回刚刚清空的就绪列表。

27.epoll模型水平触发、边沿触发优缺点对比

水平触发优、缺点

  • 优点: 当进行socket通信的时候,保证了数据的完整输出,进行IO操作的时候,如果还有数据,就会一直的通知你。
  • 缺点: 由于只要还有数据,内核就会不停的从内核空间转到用户空间,所有占用了大量内核资源,试想一下当有大量数据到来的时候,每次读取一个字节,这样就会不停的进行切换。内核资源的浪费严重。效率来讲也是很低的。

边沿触发优、缺点及应用场景

  • 优点: 每次内核只会通知一次,大大减少了内核资源的浪费,提高效率。
  • 缺点: 不能保证数据的完整。不能及时的取出所有的数据。
  • 应用场景: 处理大数据。使用non-block模式的socket。

28.使用Linux epoll模型,水平触发模式;当socket可写时,会不停的触发socket可写的事件,如何处理?

第一种最普遍的方式:

需要向socket写数据的时候才把socket加入epoll,等待可写事件。
接受到可写事件后,调用write或者send发送数据。
当所有数据都写完后,把socket移出epoll。

这种方式的缺点是,即使发送很少的数据,也要把socket加入epoll,写完后在移出epoll,有一定操作代价。

一种改进的方式:

开始不把socket加入epoll,需要向socket写数据的时候,直接调用write或者send发送数据。如果返回EAGAIN,则说明数据较大,把socket加入epoll,在epoll的驱动下写数据,全部数据发送完毕后,再移出epoll。

这种方式的优点是:数据不多的时候可以避免epoll的事件处理,提高效率。

29.惊群效应

惊群效应(thundering herd)是指多进程(多线程)在同时阻塞等待同一个事件的时候(休眠状态),如果等待的这个事件发生,那么他就会唤醒等待的所有进程(或者线程),但是最终却只能有一个进程(线程)获得这个时间的“控制权”,对该事件进行处理,而其他进程(线程)获取“控制权”失败,只能重新进入休眠状态,这种现象和性能浪费就叫做惊群效应。

更多: 滴滴云:Linux惊群效应之Nginx解决方案

30.Linux每一个运行的程序(进程)操作系统都会在内存中为其分配一个0~4G的地址空间(虚拟地址空间)。cpu为什么要使用虚拟地址空间与物理地址空间映射?解决了什么样的问题

  1. 方便编译器和操作系统安排程序的地址分布。
    程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区
  2. 方便进程之间隔离
    不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一个进程使用的物理内存
  3. 方便OS使用你那可怜的内存
    程序可以使用一系列虚拟内存地址来访问可用物理内存的内存缓冲区。当物理内存的供应量变小时,内存管理会将物理内存页(通常大小为4KB)保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动。

31.查看端口被占用的命令

Linux 查看端口占用情况可以使用 lsof 和 netstat 命令。

lsof

lsof(list open files)是一个列出当前系统打开文件的工具。

lsof 查看端口占用语法格式:
lsof -i:端口号

netstat

netstat -tunlp | grep 端口号
用于显示 tcp,udp 的端口和进程等相关情况。

32.Linux常用的排查cpu、io、网络、内存常用的工具

https://www.jianshu.com/p/0bbac570fa4c

问题

1. 自旋锁通常会出现哪些问题?

如果某个线程拿着锁死不放手,其他线程没法拿到这把锁,只好等待获取锁的线程进入循环等待的状态,等待不是睡觉,还是会消耗CPU,等待久了就会导致CPU的使用率太高。

2. 自旋锁和其他锁有什么不同?

线程状态 来看,自旋锁的状态是运行-运行-运行。而非自旋锁的状态是运行—阻塞—运行,所以自旋锁会更高效。

不管是什么锁,都是为了实现保护共享资源而提出的一种锁机制,都是为了对某项资源的互斥使用。对于互斥锁而言,如果资源已经被占用,那么资源的申请者只会进入睡眠的状态。而自旋锁不会引起调用者睡眠,而是一直循环在那里查看该自旋锁的保持着是否已经释放了锁。

在多处理器环境中对持有锁时间较短的程序来说使用自旋锁代替一般的互斥锁往往能提高程序的性能。

3. 自旋锁有哪些优点?

  1. 因为运行在用户态,没有上下文的线程状态切换,线程一直处于active,减少了不必要的上下文切换,从而执行速度较快
  2. 因为非自旋锁在没有获取锁的情况下会进入阻塞状态,从而进入内核态,此时就需要线程的上下文切换,因为阻塞后进入内核调度状态,会导致用户态和内核态之间的切换,影响锁的性能。

4. 多线程不加锁出现数据紊乱的例子

最典型的是:
工作线程用来生产产品,生产完成之后,将产品计数加1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int prducts_counts=0;
void ProduceThreadFunc(){
//生产产品
products_counts++;
}
如果使用多线程不同步的话,可能会出现如下过程:
prducts_counts值为0
线程A生产了一个产品
线程A将products_counts变量读入寄存器
线程A将这个变量在寄存器中执行++运算(得到的是1)
线程A时间片到交出CPU
线程B获得CPU时间
线程B生产了一个产品
线程B将products_counts变量读入寄存器(0)
线程B将这个变量在寄存器中执行++运算(得到的是1)
线程B将寄存器内容写回内存
products_counts值为1
线程B时间片到,交出CPU
线程A继续执行
线程A将之前计算的值(1)再次写入内存
products_counts值为1

这种情况下生产了两个产品结果却是1
这还是在单核心的情况下
如果是多核心,执行过程更加混乱

—————————————-如有错误,欢迎指正!—————————————-

评论