面试之敌系列 7 操作系统基础知识

208 阅读31分钟

操作系统基础知识

操作系统的特性

四大特性

  1. 并发性:计算机系统中存在着许多并发的程序或者进程。并发指的是宏观上同个时间段内有多个程序在同时的运行。微观上,cpu对程序是串行运行的,也就是任意时刻,只有一个程序占有CPU资源。
  2. 共享性:系统中各个并发的程序可以共享计算机中的各种资源,主要有软件,硬件,设备等,因此,系统必须解决的是共享的时候资源分配的问题。
  3. 虚拟性:操作兄系统的主要特性。所谓的虚拟性,主要指的是可以将物理上的一台计算机虚拟处多台设备。虚拟终端的概念。
  4. 异步性:程序的运行不是一气呵成的,而是走走停停的,也就是程序以人们不可预知的速度前进。尽管如此,但只要运行环境相同,作业经过多次运行,都会获得完全相同的结果。

操作系统的主要功能

  1. 存储器管理
    1. 内存分配:多道程序的时候,每个程序都有自己的地址空间。逻辑上的地址空间。
    2. 地址保护:每个程序都可以在自己的空间中二不会被干扰。
    3. 地址映射:提供逻辑地址到物理地址的映射
    4. 虚拟内存:实现大作业的运行。通过换页的操作,可以在不装载全部页的情况下,正常的运行程序。
  2. 处理机管理
    1. 进程控制:当需要进行作业运行的时候,会先分配好除了处理机以外的所的资源,并将作业放入就绪队列。
    2. 进程的同步:对并发执行的进行的协调
    3. 进程通信:进程之间的通信
    4. 调度算法:就绪队列中的作业的调度算法。
  3. 设备管理
    1. 几乎所有的设备和处理机jiao交换信息的时候,会经过一个缓冲区。因为设备IO与处理机的速度是不匹配的。
    2. 设备的分配。根据用户的请求,将合适的设备分配给用户
    3. 设备处理:实现处理机和设备之间的通信
    4. 虚拟设备。通过某种技术,将当的设备虚拟成多个设备,提供给多个用户使用。
  4. 作业管理
    1. 作业的调度:根据调度算法对作业进行调度。这里的调度是将作业从后备的作业队列中调度到准备队列中,是将外围的作业调度到就绪队列中。
  5. 文件管理

文件系统

  现代操作系统中,都是通过文件管理系统来对计算机中储存的大量的程序和数据进行管理的。将程序和数据组织成一系列的文件来进行统一的管理。

  1. 数据项--->记录--->文件。可以这样理解,字段组合成一个行记录,行记录组成一个表文件。文件会包括文件名以及文件的存储位置等信息。一般通过文件的绝对路径可以找到对应的文件。这里需要注意区分文件名和文件的存储快Inode的关系。在软连接中,创建的软连接的Inode指向了一个文件块,这个文件块中存储的就是一个文件的全路径名。
  2. 文件管理系统管理的对象:文件和目录,以及磁盘的存储空间。提供跨文件系统的支持。
  3. 打开文件的操作:打开文件是指操作系统将指定名称的文件的属性从外村拷贝到内存中的一个表,并将这个表中的索引号返回给用户的过程。在内存中维护一个文件打开的表。

文件的类型

  1. 有结构的文件:用一定的数据结构组织的文件。例如顺序文件,索引文件等。这类文件主要用于特定的场景。
  2. 无结构的文件:源程序,可执行文件等都是无结构的文件。

文件在外存中的分配方式

  1. 连续分配:逻辑上连续的文件在分配的时候,物理存储上也是连续的。这样做的好处是IO的时候效率会很高。但是连续分配的话,可能会造成很多的空间浪费。而且连续分配的算法需要提前知道文件的大小等。实现起来比较困难。
  2. 链式分配的方式:采用链式的方式,将外存中的物理存储块管理起来,这样逻辑上连续的文件可以采用链式管理存储到分散的物理块中。链式分配的可靠性低,因为其中的一个链断开的话,有数据丢失的风险。
  3. 索引分配:索引分配为每个文件分配一个索引块,再将文件对应的盘块都记录在该索引块中。建立文件的时候,只需要维护该索引块即可。

目录管理

为了能够实现对文件的有效的管理,通过目录管理,可以快速地索引到对应的文件。目录管理可以实现以下的功能:

  1. 按名存取:用户提供文件名即可访问对应的文件。
  2. 提高对目录检索的速度
  3. 文件共享:
  4. 允许不同的用户创建同名的文件。

文件的存储空间的管理

  1. 空闲表法:空闲表属于连续分配,系统会维护一个空闲空间表,物理块上的存储位置会对应表中的一个项。
  2. 空闲链表法:维护两个链表 空闲盘块表,这个表示磁盘上的所有的空闲空间。空闲盘区:这个盘区包含若干盘块。
  3. 位示意法:利用位图来表示磁盘块的空闲与否状态。
  4. 详细的情况可以参见:空间管理

文件的打开

  在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件、目录文件、链接文件和设备文件。文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符。程序刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。如果此时去打开一个新的文件,它的文件描述符会是3。

  每个文件描述符会与一个打开的文件相对应。同时,不同的文件描述符也可能指向同一个打开的文件。系统会为每个进程维护一个文件描述符表,用于记录该进程所打开的文件。

系统中可能存在的三张表:

  1. 进程的文件描述符表
  2. 操作系统的打开文件描述符表
  3. 文件系统的i-node表 文件的打开详解 image.png

软连接和硬链接

这里需要先介绍一下文件是如何存储到磁盘中的。

  1. inode是什么 文件存储在硬盘上的时候,是以块为单位进行存储。磁盘会被划分成很多的块。这时候,我们需要在磁盘中划分出一个地方,来存放这些块的元信息,这个就是inode。i-node即使索引块。每个文件都有对应的inode,里面包括了文件的各种属性,权限等信息。我们通过inode就可以真正的找到文件存储的物理块。

  2. inode的大小 inode也是存放在物理块中的,在硬盘进行格式化的时候,便会划分成两个区,一个是inode区,一个数数据区。

  3. inode号码 每个inode都有个号码,操作系统用inode号码来识别不同的文件.。这里值得重复强调一遍,类Unix系统内部是不使用文件名的,而是使用inode号码来识别文件,对于系统来说,文件名只是inode号码便于识别的别称或者绰号(有点类似于域名和IP地址的关系).

  4. 硬链接和软连接:硬链接创建的时候,会直接将文件名对应到源文件的inode上,也就是硬链接文件和源文件使用相同的inode号码,因此他们是同一个文件。对文件进行删除的时候,由于inode中的引用计数大于0,因此只是简单的减少一次引用。文件没有被删除。而软连接创建的时候,会创建一个新的inode号码,并指向了新的物理块。只是这个新的inoe指向的物理块中,存放的是源文件的全路径名。因此,软连接其实是一个文件名指针,通过文件名来对源文件进行访问,这其实就是利用了文件系统的目录管理来进行加单的文件访问而已。

进程和线程

进的组成

  1. 程序的代码
  2. 程序需要处理的数据
  3. 程序计数器
  4. 系统资源 例如打开的文件等

进程和程序的区别

  1. 程序是产生进程的基础
  2. 进程是程序的功能体现,通过进程的形式实现程序的预期目标
  3. 进程是动态的,而程序只是代码的集合。
  4. 进程=程序+数据+进程控制块

进程控制块

  PCB是操作系统为了处理进程而维护的一个数据结构块,用于保存所有与该进程有管的信息。

  1. 进程表示信息:进程号,父进程号等。
  2. 处理机状态保存区:进程处理器切换的时候u,或者发生进程阻塞的时候,需要保存进程执行的现场信息,主要有寄存器的信息,由于的数据信息等。同时,进程自己需要维护的右程序计数器,程序状态等新。
  3. 进程控制信息:进程之间的通信,进程所打开的文件等。

PCB的组织方式

采用分转态进行组织的方式

  1. 链表:同一个转态的进程PCB组成一个链表。例如就绪的进程,阻塞的进程等。这样便于管理。
  2. 进程只能自己阻塞自己,并且等待别人的唤醒。因为只有进程自己知道自己合适需要进程阻塞。

进程的转态

  1. 挂起:把一个进程从内存转到外存。进程在挂起时,不会占用内存空间。处在挂起状态的进程映像在磁盘上。
  2. 简单来说,每个进程的PCB都根据它的状态加入到相应的队列中,当一个进程的状态发生变化时,它的PCB从一个状态队列中脱离出来,加入到另一个队列。

进程的通信

管道

管道是单向的、先进先出的、无结构的字节流,它把一个进程的输出和另一个进程的输入连接在一起。管道会利用两个文件进行数据的存储,一个用于读,一个用于写。管道是阻塞的,当没有数据的时候或者管道充满数据的时候,读写操作都会阻塞。管道有匿名和命名管道之分。 管道提供了简单的流控制机制。进程试图读一个空管道时,在数据写入管道前,进程将一直阻塞。同样,管道已经满时,进程再试图写管道,在其它进程从管道中读走数据之前,写进程将一直阻塞。

匿名管道和命名管道

  1. 匿名管道只能用于具有亲缘关系的进程之间的通信(也就是父子进程或者兄弟进程之间)。

  2. 命名管道可以在无情缘关系的进程之间进程通讯 image.png

  3. 管道的创建:创建管道之后,会返回两个用于读写的文件描述符,主要用于管道的读写。 image.png

  4. 匿名管道例子详解

  5. 创建管道:父进程调用pipe()函数创建一个管道

  6. 父进程通过fork()函数创建一子进程

  7. 子进程会继承父进程所创建的管道

  8. 确定管道的传输方向:在父、子进程中根据需要的传输方向关闭无关的读端或写端文件描述符. image.png image.png

消息队列

消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。 每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。

消息队列是在消息的传输过程中保存消息的容器。消息队列管理器相当于消息发送者和接收者的中介。消息队列的主要目的是创建路由并且保证消息可靠传递;如果发送消息时接收者不可用,消息队列会保留消息,直到有人接收它。

共享内存

共享内存是进程间通信中最简单的方式之一。共享内存允许两个或更多进程访问同一块内存,就如同 malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。 image.png

共享内存的特点:
  1. 共享内存是进程间共享数据的一种最快的方法。一个进程向共享的内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。
  2. 使用共享内存要注意的是多个进程之间对一个给定存储区访问的互斥。若一个进程正在向共享内存区写数据,则在它做完这一步操作前,别的进程不应当去读、写这些数据。
套接字

image.png 套接字是一种夸主机的进程通讯方式,我们知道,在本地中,进程可以采用进程id来唯一标志,但是在不同的主机间进行通讯的时候,进程ID用不了,这时候我们采用的就是ip+端口来唯一的确定一个网络中的进程。既然我们已经可以确定网络中的一个进程,那我们便可以直接使用传输层的协议来进行通讯。但是网络层的协议比较复杂,因此采用socket对此进行封装,这样就可以透明的来进行进程之间的通讯。

信号量通信

信号量(semaphore)是一种用于提供不同进程之间或者一个给定的不同线程间同步手段的原语。信号量多用于进程间的同步与互斥,简单的说一下同步和互斥的意思:

  1. 同步:处理竞争就是同步,安排进程执行的先后顺序就是同步,每个进程都有一定的个先后执行顺序。
  2. 互斥:互斥访问不可共享的临界资源,同时会引发两个新的控制问题(互斥可以说是特殊的同步)。
  3. 竞争:当并发进程竞争使用同一个资源的时候,我们就称为竞争进程。
  4. 共享资源通常分为两类:一类是互斥共享资源,即任一时刻只允许一个进程访问该资源;另一类是同步共享资源,即同一时刻允许多个进程访问该资源;信号量是解决互斥共享资源的同步问题而引入的机制。

线程的同步

读写锁
  1. 针对于读多写少的场景下,并发性能可以得到很大的提升。读写都会加锁,但是读是共享锁,写是排它锁。当一个进程获取读锁的时候,如果其他的进程需要进行读取,可以很简单的获取读锁,这时候可能会出现写饥饿的情况,因此当需要进行写加锁的时候,我们会采用占坑的形式,先wirte++;这样读锁的加锁会排到后面。
volatile
  1. volatile 可以保证有序性和可见性,但是这个有序性需要出发happen-before机制的时候才会生效。例如 i++这个就无法保证有序性,因为这时候的i是先读再写的。
  2. volatile可以保证可见性。此外,synchronize和lock都可以实现可见性,因为释放锁的时候,会将数据刷新到主存中。
有序性,可见性和原子性

有序性是jvm对程序员的一种承诺,但是在实际的执行过程中,根据as-if-searies 规则,还是有可能对指令进行重排的。只要保证幂等性即可。

可重入锁

可重入锁指的是该锁可以被多次获取而不用释放后再获取,例如在使用递归的时候,如果不是可重入的话,就会发生死锁的情况。

  1. 自旋锁和阻塞的区别:自旋锁值得是循环等待检测锁是否释放,阻塞时进程对自己的状态判断之后,自己进行阻塞。等待该持有锁的线程释放并唤醒自己的过程。
生产者和消费者模式
死锁的产生和解决
  1. 互斥量
  2. 持有并保持
  3. 不可抢占
  4. 循环等待
解决
  1. 检测是否形成了死锁
  2. 破话
  3. 预防 死锁详解

地址空间

IO模型

cache和buffer

  1. cahche:缓存,有一种预读的感觉,是将经常使用的数据存放在一个速度介于处理器和IO之间介质的技术。缓存有命中的概念。主要用于解决系统的两端处理速度不匹配的情况。
  2. buffer:缓冲区,解决流量整型的时候使用。起到一个调节的作用。

同步和异步,阻塞和非阻塞

同步和异步

同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

同步和异步,可以认为是针对指令执行顺序来说的。同步意味着发起调用后,没有得到结果之前,就不返回,自然不能执行其它指令;异步意味着调用发出后,调用方立刻返回,通过回调等措施拿到结果,这样调用方还可以执行其它指令。

阻塞和非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

阻塞和非阻塞,是针对用户进程(线程)的状态来说的。阻塞意味着用户进程(线程)被挂起,即让出CPU时间片;非阻塞意味着用户进程(线程)不会挂起,仍可以做其它工作。

五中IO模型

  首先确定这里的同步和异步主要指的是数据在内核到用户线程的过程中,这个过程一定会阻塞调用者,因此当用户线程需要直接经历这个步骤的时候,就认为是同步的。

IO的过程

  1. 等待数据准备好
  2. 从内核向进程复制数据

阻塞IO

image.png当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。 所以,blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。

非阻塞IO

image.png从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。 所以,在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。 非阻塞的接口相比于阻塞型接口的显著差异在于,在被调用之后立即返回。

IO多路复用

IO multiplexing这个词可能有点陌生,但是如果我说select/epoll,大概就都能明白了。有些地方也称这种IO方式为事件驱动IO(event driven IO)。我们都知道,select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图: image.png

由于同步非阻塞方式需要不断主动轮询,轮询占据了很大一部分过程,轮询会消耗大量的CPU时间,而 “后台” 可能有多个任务在同时进行,人们就想到了循环查询多个任务的完成状态,只要有任何一个任务完成,就去处理它。如果轮询不是进程的用户态,而是有人帮忙就好了。那么这就是所谓的 “IO 多路复用”。

异步IO

image.png 用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

用异步IO实现的服务器这里就不举例了,以后有时间另开文章来讲述。异步IO是真正非阻塞的,它不会对请求进程产生任何的阻塞,因此对高并发的网络服务器实现至关重要。 到目前为止,已经将四个IO模型都介绍完了。现在回过头来回答最初的那几个问题:blocking和non-blocking的区别在哪,synchronous IO和asynchronous IO的区别在哪。 先回答最简单的这个:blocking与non-blocking。前面的介绍中其实已经很明确的说明了这两者的区别。调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还在准备数据的情况下会立刻返回。 在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。Stevens给出的定义(其实是POSIX的定义)是这样子的:

  • A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
  • An asynchronous I/O operation does not cause the requesting process to be blocked; 两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。有人可能会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个系统调用。non-blocking IO在执行recvfrom这个系统调用的时候,如果kernel的数据没有准备好,这时候不会block进程。但是当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内进程是被block的。而asynchronous IO则不一样,当进程发起IO操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。

用户空间与内核空间

现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。

划分内核空间和用户空间的原因

在 CPU 的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,比如清内存、设置时钟等。如果允许所有的程序都可以使用这些指令,那么系统崩溃的概率将大大增加。 所以,CPU 将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通应用程序只能使用那些不会造成灾难的指令。比如 Intel 的 CPU 将特权等级分为 4 个级别:Ring0~Ring3。 其实 Linux 系统只使用了 Ring0 和 Ring3 两个运行级别(Windows 系统也是一样的)。当进程运行在 Ring3 级别时被称为运行在用户态,而运行在 Ring0 级别时被称为运行在内核态。 当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。

如何从用户空间进入内核空间

其实所有的系统资源管理都是在内核空间中完成的。比如读写磁盘文件,分配回收内存,从网络接口读写数据等等。我们的应用程序是无法直接进行这样的操作的。但是我们可以通过内核提供的接口来完成这样的任务。 比如应用程序要读取磁盘上的一个文件,它可以向内核发起一个 "系统调用" 告诉内核:"我要读取磁盘上的某某文件"。其实就是通过一个特殊的指令让进程从用户态进入到内核态(到了内核空间),在内核空间中,CPU 可以执行任何的指令,当然也包括从磁盘上读取数据。具体过程是先把数据读取到内核空间中,然后再把数据拷贝到用户空间并从内核态切换到用户态。此时应用程序已经从系统调用中返回并且拿到了想要的数据,可以开开心心的往下执行了。 简单说就是应用程序把高科技的事情(从磁盘读取文件)外包给了系统内核,系统内核做这些事情既专业又高效。

对于一个进程来讲,从用户空间进入内核空间并最终返回到用户空间,这个过程是十分复杂的。举个例子,比如我们经常接触的概念 "堆栈",其实进程在内核态和用户态各有一个堆栈。运行在用户空间时进程使用的是用户空间中的堆栈,而运行在内核空间时,进程使用的是内核空间中的堆栈。所以说,Linux 中每个进程有两个栈,分别用于用户态和内核态。

既然用户态的进程必须切换成内核态才能使用系统的资源,那么我们接下来就看看进程一共有多少种方式可以从用户态进入到内核态。概括的说,有三种方式:系统调用、软中断和硬件中断。这三种方式每一种都涉及到大量的操作系统知识,所以这里不做展开。image.png

进程的阻塞

正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。

磁盘的IO

磁盘IO

缓存IO

缓存I/O又被称作标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,数据先从磁盘复制到内核空间的缓冲区,然后从内核空间缓冲区复制到应用程序的地址空间。

  1. 读操作:操作系统检查内核的缓冲区有没有需要的数据,如果已经缓存了,那么就直接从缓存中返回;否则从磁盘中读取,然后缓存在操作系统的缓存中。
  2. 写操作:将数据从用户空间复制到内核空间的缓存中。这时对用户程序来说写操作就已经完成,至于什么时候再写到磁盘中由操作系统决定,除非显示地调用了sync同步命令(详情参考《【珍藏】linux 同步IO: sync、fsync与fdatasync》)。
  3. 缓存I/O的优点:1)在一定程度上分离了内核空间和用户空间,保护系统本身的运行安全;2)可以减少读盘的次数,从而提高性能。
  4. 缓存I/O的缺点:在缓存 I/O 机制中,DMA 方式可以将数据直接从磁盘读到页缓存中,或者将数据从页缓存直接写回到磁盘上,而不能直接在应用程序地址空间和磁盘之间进行数据传输,这样,数据在传输过程中需要在应用程序地址空间(用户空间)和缓存(内核空间)之间进行多次数据拷贝操作,这些数据拷贝操作所带来的CPU以及内存开销是非常大的。

直接IO

直接IO就是应用程序直接访问磁盘数据,而不经过内核缓冲区,这样做的目的是减少一次从内核缓冲区到用户程序缓存的数据复制。比如说数据库管理系统这类应用,它们更倾向于选择它们自己的缓存机制,因为数据库管理系统往往比操作系统更了解数据库中存放的数据,数据库管理系统可以提供一种更加有效的缓存机制来提高数据库中数据的存取性能。

直接IO的缺点:如果访问的数据不在应用程序缓存中,那么每次数据都会直接从磁盘加载,这种直接加载会非常缓存。通常直接IO与异步IO结合使用,会得到比较好的性能。(异步IO:当访问数据的线程发出请求之后,线程会接着去处理其他事,而不是阻塞等待)

无论是直接还是标准的,都会调用两个 read和write函数,只是指定好标志位之后就可以使用缓存页或者不适用缓存页。默认的是使用缓存页。同时,在很多时候,还有用户空间自己的缓存等。

同时,还有另一个简单的概念 用户角度的带不带缓存

select,poll,epoll的使用详解

IO多路复用之select、poll、epoll详解

select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。(这里啰嗦下)

select

select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但 是这样也会造成效率的降低。

poll

不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。

epoll

int epoll_create(int size)//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大;
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

int epoll_create(int size)

创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。 当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

函数是对指定描述符fd执行op操作。