操作系统相关知识杂记

Posted by W-M on November 15, 2017

本文主要记录了学习操作系统相关知识中遇到的一些问题及个人理解,用作个人备忘,错误难免,敬请指出。

  操作系统是控制和管理计算机硬件和软件资源、合理的组织计算机的工作流程以方便用户使用的程序的集合。通常操作系统会为我们提供以下功能:进程管理、内存管理、文件管理、设备管理、网络通信管理等功能。
  由于操作系统向用户隐蔽了系统使用的硬件设备,因此操作系统要为它上面的应用软件提供一组命令或系统调用接口供用户使用。当计算机加载操作系统后,用户不再直接与计算机硬件打交道,而是利用操作系统提供的命令和功能去使用计算机。
  系统调用(sysytem call)把应用程序的请求传给内核,调用相应的内核函数完成所需的处理,将处理结果返回给应用程序。UNIX提供给应用程序员以100多个系统调用,用以在用户态下完成进程管理、文件管理等需要提升至内核态的操作,而windows下,应用程序员不能直接使用系统调用,而需要借用操作系统提供的WIN32 API来完成,每个API函数可以调用一个或多个系统调用也可以不调用任何系统调用,仅仅在用户态下完成操作,不同版本的相同API所调用的系统调用以及其调用方式也可能不同。
  linux系统调用举例:比如进程控制fork,文件系统控制chdir,系统控制reboot。
  WIN32 API使用举例:CreateProcess() 创建一个新的进程。


一、进程线程相关知识记录

  1.进程:资源分配的实体,这些资源包括:进程描述符、独立的内存地址空间、打开的文件描述符等。以进程描述符(用来描述进程的数据结构)为例,进程描述符与进程一一对应,记录了与进程相关的所有信息,比如进程的状态、进程的标识(PID)等。进程描述符一般较大,32位机可为1.7KB。

  2.线程:进程内一个独立执行的线路,CPU调度的实体。线程共享所属进程的资源,但也有其自己的私有资源,比如线程私有栈、CPU寄存器状态、CPU时间片、线程优先级、线程局部存储TLS。正是因为线程内存在这些私有资源,线程才能成为CPU调度的最小单位,在多核CPU上达到真正的并行执行。

  3.多线程共享所属进程的资源,多进程共享所属计算机的资源,比如CPU,文件等。Linux下线程是利用轻量级进程机制实现的,多个轻量级进程共享同一套资源,但具有不同的栈和CPU寄存器状态。只有在多核CPU情况下多线程才能并行执行,否则多线程也只是在同一个核心上并发执行。

  4.对于CPU密集型计算,使用多线程不一定可以提高效率;对于IO密集型计算,可以使用多线程提升吞吐量。

  5.在采用了多进程且每个进程支持一个以上线程的多线程机制的操作系统中(如Windows,Linux),多个进程并发执行时,在每个进程内部存在的多个线程也在并发执行。进程内部的线程调度机制类似于进程的调度,在某线程等待时,其它就绪线程会被调度执行。如果整个进程都 在等待态,被换出了缓冲区,则属于它的所有线程都将停止。

  6.有了多进程机制,为何操作系统还要采用多线程实现并发操作?换句话说,用线程实现并发操作与用进程实现并发操作有什么优势呢?
  (1)仅采用多进程实现并发操作存在的问题

  • 进程创建操作、内存分配操作、调度操作都十分费时
  • 进程间通信延迟很大,使得频度较高的通信过程效率低下
  • 操作系统对于进程数量的限制导致进程的并行度达不到人们理想的效果

  (2)采用多线程实现并发操作为何可以克服这些问题
  以编写一个需要与用户交互的界面程序为例解释多线程的优势:

  • 比如用户打开了一个界面,进行了一个写文件操作,而此写文件操作比较耗时,我们肯定不希望在这段时间之内界面被卡住,此时就需要采用并发操作来执行用户写文件过程中进行的其它指令。理论上来说我们此时采用多线程和多进程操作都可以实现这样的并发操作,但是多线程相比于多进程来讲,线程的创建、撤销、内存分配、调度操作无论从时间的长短上,还是从占用内存空间的大小上来看线程都有不可小看的优势。
  • 程序中两个界面之间需要通信,此时我们还是既可以使用进程间通信也可以使用线程间通信来做,但是线程间通信比起进程间通信无论是从实现难度上来讲还是从通信效率上来看都有优势的多。
  • 有些对于并发数量要求很高的应用,如一些大型网站,即使在不考虑进程线程的效率差距条件下,仅使用多进程来使用并发也是不可行的。操作系统对于进程并发最大数量有限制,通过在多进程中引入多线程机制,我们可以大大提升这个限制的最大数量。

  7.Linux下面线程是lightweight process,就是轻量级的进程。
  换句话说Linux中的线程仅仅是共享了部分资源(地址空间、文件句柄、信号量等等)的进程。Linux通过轻量级进程实现线程最大的好处是设计简单。 线程调度直接使用进程调度就可以了,没必要再搞一个进程内的线程调度器。 至于代码段,数据段,信号量,这些通过同用一个内存映射就可以了。在Windows中线程是实实在在的一种运行抽象,有属于自己的不同于进程的数据结构,提供了比进程更轻更快的调度单元。

  8.内核线程与用户线程

  • 内核线程就是内核的分身,一个分身可以处理一件特定事情。这在处理异步事件如异步IO时特别有用。内核线程的使用是廉价的,唯一使用的资源就是内核栈和上下文切换时保存寄存器的空间。支持多线程的内核叫做多线程内核(Multi-Threads kernel )。对内核线程,linux内核会把它当作进程来看待和调度。
  • 用户线程是完全建立在用户空间的线程库,用户线程的创建、调度、同步和销毁全又库函数在用户空间完成,不需要内核的帮助。因此这种线程是极其低消耗和高效的。对用户线程,调度的策略则与具体使用的线程库的实现有关系。
  • 用户级线程的缺点:1、同一进程的多个线程不能真正并行;2、由于线程对操作系统透明,调度处于进程级别,若进程中的一个线程通过系统调用进入操作系统受阻,那么将阻塞整个进程 3、线程之间的调度需要由用户使用的线程库来自行实现,由于在一个单独的进程内部,没有时钟中断,所以不可能用轮转调度的方式调度进程,线程调度不好实现。
  • 内核线程的缺点:即使在同一个进程的多个线程之间切换,也需要陷入内核,由于内核线程与用户线程相比上下文切换开销较大,当线程切换较为频繁时会导致线程调度效率降低。

  9.线程实现方式
  (1)在用户空间实现线程:即把整个线程包放在用户空间中来实现,内核对线程包一无所知。从内核角度考虑,就是按正常的方式管理。由用户进程自行负责其中的线程调度。
  (2)在内核空间实现线程:此时每个进程不需要在进程中维护自己的线程表,相反,在内核中有用来记录系统中所有线程的线程表,当每个线程希望创建一个新线程或撤销一个已有线程时,它进行一个系统调用,这个系统调用通过对线程表的更新来完成线程的创建或撤销工作。
  (3)混合实现:人们试图通过将用户级线程与某些或者全部内核级线程多路复用起来来将用户线程的优点和内核级线程的优点结合起来。即用户线程与内核线程之间的对应方式为N:M。
  (4)Java线程模型:JDK1.2之前,Java线程是基于称为“绿色线程”的用户线程来实现的,而在JDK1.2中,线程模型替换为基于操作系统原生线程模型来实现。因此在目前的JDK版本中,操作系统支持怎样的线程模型很大程度上决定了Java虚拟机的线程是怎样映射的。
  对于Sun JDK来说,它的Windows版与Linux版都是使用一对一的线程模型实现的,一条Java线程就映射到一条轻量级进程之中,因为Windows和Linux系统提供的线程模型就是一对一的。(指的是采用Windows和Linux提供的原生方法创建的线程采用的线程模型都是一对一的)

  10.操作系统进程时间片轮转调度实现原理?为何为每个进程采用多级页表?

  • 时间片轮转调度实现原理:操作系统进程调度是通过内核时钟中断及相应的中断服务程序来实现的。当一个正在运行的进程时间片耗尽时,内核时钟中断停止当前进程执行,进入中断服务程序。在中断服务程序中选择下一个待执行的进程(由操作系统提供的算法来选择),此中断服务程序执行之后不返回之前执行的程序而是执行在中断服务程序中被选中的下一个待执行的进程(通过改动中断服务程序返回地址为下一个进程起始地址来实现)。
  • 为每个进程采用多级页表的原因:通过使用多级页表可以减少页表的内存占用,加快进程创建速度。进程在初始创建时只需将其对应的一级页表加载到内存中,当用户访问的逻辑地址在页表中查询不到时再产生缺页异常,在异常处理程序中加载其对应的下级页表。原理上还是操作系统拖延战术的体现。

  11.操作系统什么时候会进行进程调度?
  操作系统进行进程调度可有以下三种情形:
  (1)进程主动放弃剩下的时间片。比如调用类似于sleep的函数。
  (2)时间片用完导致进程调度。正常调度方式。
  (3)由于CPU响应中断引发的进程调度。
  考虑场景:进程B调用read系统调用读磁盘进入阻塞状态,进程A开始运行。之后DMA将数据准备好后发出中断通知CPU,CPU响应中断。   问题:如果发生中断前执行的是进程A,CPU响应中断,进入中断处理程序,处理程序是不是做的仅仅是让进程B由阻塞态变为就绪态?中断处理程序执行完之后如果A进程时间片没有用完,A进程是不是还要继续执行?或者是中断处理程序运行结束之后如果B进程优先级比A进程优先级高,不管A进程时间片是否用完,都会抢占A进程开始执行?
  答案:(我的理解)中断会打断当前正在执行的进程,CPU响应中断,不管当前正在运行的进程时间片有没有用完,都将其从运行态变为就绪态,加入到就绪队列中;在中断处理程序中会在进程等待队列中寻找到刚刚因为read系统调用而阻塞的进程B,将其从阻塞态变为就绪态,也加入到就绪队列中。在执行完中断处理程序从内核态返回到用户态之前,内核schedule线程会进行进程调度,在当前就绪队列中选择某个进程开始执行。 需要注意的是,被中断打断的进程在中断处理程序执行之后不一定能抢到CPU继续运行。
_ _ _

二、文件管理相关知识记录

  Linux中每个文件都采用三级索引形式存储,有一个IndexNode物理文件块,在目录文件中仅需存储此IndexNode物理文件块所在位置即可通过此文件块定位到此文件其它部分的存储位置。此Inode节点可称作为外存文件控制块。 AOP代理图

图1:Unix三级文件索引示意图

  关于文件描述符还可以参考:文件描述符简介
  inode也会消耗硬盘空间,所以硬盘格式化的时候,操作系统自动将硬盘分成两个区域。一个是数据区,存放文件数据;另一个是inode区(inode table),存放inode所包含的信息。
  每个inode节点的大小,一般是128字节或256字节。inode节点的总数,在格式化时就给定,一般是每1KB或每2KB就设置一个inode。假定在一块1GB的硬盘中,每个inode节点的大小为128字节,每1KB就设置一个inode,那么inode table的大小就会达到128MB,占整块硬盘的12.8%。
  由于每个文件都必须有一个inode,因此有可能发生inode已经用光,但是硬盘还未存满的情况。这时,就无法在硬盘上创建新文件。

  inode号码:每个inode都有一个号码,操作系统用inode号码来识别不同的文件。这里值得重复一遍,Unix/Linux系统内部不使用文件名,而使用inode号码来识别文件。对于系统来说,文件名只是inode号码便于识别的别称或者绰号。表面上,用户通过文件名,打开文件。实际上,系统内部这个过程分成三步:首先,系统找到这个文件名对应的inode号码;其次,通过inode号码,获取inode信息;最后,根据inode信息,找到文件数据所在的block,读出数据。

  硬链接实现原理:一般情况下,文件名和inode号码是”一一对应”关系,每个inode号码对应一个文件名。但是,Unix/Linux系统允许,多个文件名指向同一个inode号码。这意味着,可以用不同的文件名访问同样的内容;对文件内容进行修改,会影响到所有文件名;但是,删除一个文件名,不影响另一个文件名的访问。这种情况就被称为”硬链接”(hard link)。
  ln命令可以创建硬链接:ln 源文件 目标文件
  运行上面这条命令以后,源文件与目标文件的inode号码相同,都指向同一个inode。inode信息中有一项叫做”链接数”,记录指向该inode的文件名总数,这时就会增加1。
  反过来,删除一个文件名,就会使得inode节点中的”链接数”减1。当这个值减到0,表明没有文件名指向这个inode,系统就会回收这个inode号码,以及其所对应block区域。

  软链接实现原理:除了硬链接以外,还有一种特殊情况。文件A和文件B的inode号码虽然不一样,但是文件A的内容是文件B的路径。读取文件A时,系统会自动将访问者导向文件B。因此,无论打开哪一个文件,最终读取的都是文件B。这时,文件A就称为文件B的”软链接”(soft link)或者”符号链接(symbolic link)。这意味着,文件A依赖于文件B而存在,如果删除了文件B,打开文件A就会报错:”No such file or directory”。这是软链接与硬链接最大的不同:文件A指向文件B的文件名,而不是文件B的inode号码,文件B的inode”链接数”不会因此发生变化。
  ln -s命令可以创建软链接。ln -s 源文文件或目录 目标文件或目录

  为了方便对文件的操作,文件一旦被打开,打开文件机构在内存中需要维护一些数据结构来存放已打开文件的有关信息,以便对文件进行管理和控制。打开文件机构一般包括三种结构:系统打开文件表、进程打开文件表、内存文件控制块表。

  内存文件控制块表:当需要查询、修改外存上某文件控制块时,按一般方式,可将其临时装入内存,处理完毕后再写回外存。但是文件控制块的使用是非常频繁的,按这种方式进行很不经济。为此unix设置了内存文件控制块。内存文件控制块与外存文件控制块的节点结构基本相同,略有一些增删。
  当打开一个文件时,如果找不到其相应的内存文件控制块,就在内存文件控制块表中分配一个空闲表项,并将该文件的外存文件控制块的主要部分复制进去,并填入相应的外存文件控制块节点号。当需要查询、修改文件的控制信息时,直接在内存文件控制块中即可进行。当关闭文件时,如果内存文件控制块节点被更改过,则需要更新其对应的外存文件控制块。

  系统打开文件表:系统打开文件表用来存放已打开的文件信息,打开控制文件结构定义如下:

struct file {
	unsigned short f_flags;//文件操作标志
	unsigned short f_count;//共享该结构体的计数值
	struct inode * f_inode;//指向文件对应的内存inode
	loff_t f_pos;//文件的当前读写位置
}

  在系统打开文件表中:f_flags表示文件的打开方式,如读、写、读写;f_inode是一个指向内存文件控制块表的指针,内存文件控制块表是相应的磁盘索引节点在内存的驻留;f_count是当前正在访问该节点的引用计数;f_pos为文件的当前读写位置。

  进程打开文件表:unix中每一个进程都有一张打开的文件表,在UNIX系统中,它是进程数据结构中的一部分,具体表现为一个指针数组。在进程打开文件时,按下标由低到高的顺序使用数组中的某一空闲项(即指针为NULL的表项),在该表项中填入打开文件控制块file结构变量的地址,并打开文件描述字的值(就是该空闲项的下标值)。
  进程打开一个文件的过程如下:
  (1)系统要在内存文件控制块表中为其分配一个内存文件控制块节点,并将该文件的外存文件控制块节点中的主要部分复制进去,并填入相应的外存文件控制块节点号。
  (2)在系统打开文件表中分配一个空闲打开文件控制块file,并使f_inode指针指向内存文件控制块节点。
  (3)在进程打开文件表中找到一个空闲项,填入file结构的地址,并将此file转换为打开文件描述字返回给用户。

  为了便于理解进程打开文件表、系统打开文件表、内存文件控制块表之间的关系,举例如下:   一个进程执行如下代码:

fd1 = open("/etc/passwd", o_RDONLY);//以只读方式打开文件/etc/passwd
fd2 = open("pocal", o_WRONLY);//以写方式打开文件pocal
fd3 = open("/etc/passwd", o_RDWR);//以读写方式打开文件/etc/passwd

  上述代码的意思是:同一个文件/etc/passwd被打开了两次,一次以只读方式打开,一次以读写方式打开。一个文件的内存文件控制块节点是唯一的,但由于打开的方式不同,在进程打开文件表和系统打开文件表中却要占两个表项;而文件pocal是以写方式打开了一次。因此本例中进程打开文件表、系统打开文件表、内存文件控制块表之间的关系如图2所示: AOP代理图

图2:用户打开文件表与系统打开文件表之间的关系

  为防止多进程操作同一文件时产生错误,有时需要对文件进行加锁,具体可参考:Linux 2.6 中的文件锁


三、设备管理相关知识记录

  1、用户程序读取硬盘文件的流程:当用户程序试图读取一个硬盘文件时,需要通过操作系统实现这一操作。首先由操作系统中的设备无关软件检查高速缓存中有无要读的数据块,若没有,则调用设备驱动程序,向IO硬件发出一个请求。然后用户进程阻塞并等待磁盘操作的完成。当磁盘操作完成之后,硬件产生一个中断,转入操作系统提供的中断处理程序。中断处理程序检查中断的原因,认识到这时磁盘读取操作已经完成,于是唤醒用户进程取回从磁盘中读取的信息,从而结束此次IO请求。用户进程在得到了所需的硬盘文件内容之后,继续运行。

  DMA:是指数据在内存和I/O设备间的直接成块传送。即在内存与I/O设备间传送一个数据块的过程中,不需要CPU的任何干涉。DMA控制器从CPU完全接管对总线的控制,数据交换不经过CPU。   在非DMA时,打印2048个字节,至少需要执行2048次输出指令并加上2048次中断处理的代价。而在DMA情况下,若一次DMA可传送512个字节,则只需要执行4次输出指令和处理4次打印机中断;若一次DMA可传送字节数大于等于2048个字节,则只需要执行一次输出指令和处理一次打印机中断。
  磁盘IO一般采用DMA方式。使用DMA时,除了块的磁盘地址之外,CPU还给DMA控制器两项信息:块要存放的存储地址和要转移的字节数。在把整个块从设备读进它的缓冲区并且核准和校验之后,DMA控制器按照DMA存储器指定的存储地址,把第一个字节或字写入主存。然后,它用刚刚传送的字节数增加DMA的地址,减少DMA的计数。此过程一直重复到DMA的计数等于0为止,此刻DMA控制器才引发一个中断。CPU响应中断,运行操作系统提供的中断处理程序,当操作系统来处理该中断时,无需把块复制到内存,因为块已经由DMA存放在内存中了。

AOP代理图

图1:传统IO文件读取流程

  Java NIO文件内存映射:现代操作系统大都支持虚拟内存映射,这样,我们可以把内核空间地址与用户空间的虚拟地址映射到同一个物理地址,这样,DMA 硬件(只能访问物理内存地址)就可以填充对内核与用户空间进程同时可见的缓冲区了。如图2所示:

AOP代理图

图2:NIO文件内存映射

  这样做的好处是,我们在读取磁盘文件时,再也不用通过内核缓冲区到用户进程缓冲区的来回拷贝操作了。操作系统会通过一些页面调度算法来将磁盘文件载入对分页区进行高速缓存的物理内存。我们就可以通过映射后物理内存来读取磁盘文件了。

进程在读写磁盘的时候,大概的流程是:

  • 以write 为例:进程(用户空间) -> 系统调用,进入内核 -> 将要写入的数据从用户空间拷贝到内核空间的缓存区 -> 调用磁盘驱动 -> 写在磁盘上面。
  • 使用mmap之后:进程(用户空间)–> 读写映射的内存 –> 写在磁盘上面。

可以认为每次mmap读写操作相比于普通的read、write操作减少了一次系统调用以及内核缓冲区与用户缓冲区之间数据相互拷贝的开销。mmap还有一个好处就是进程把数据写入到MappedByteBuffer之后,如果进程被kill,这块已写入MappedByteBuffer的数据不会丢失,会由os将其回写到对应的文件中,但是如果机器直接宕机就没有办法了(比如直接断电)。

  中断:Linux中断分为两种:中断及异常。中断由外设硬件产生,异常由CPU内部产生。外设产生中断,CPU响应中断,操作系统提供中断服务程序。

  IO模式:对于进程的一次I/O访问(包含网络IO与文件IO),以read操作为例,有两个阶段可能发生阻塞:
  (1)等待要读取的设备状态变为可读。比如读取文件时要读取的文件被其它进程加了写锁,当前进程需要等待加锁进程操作完成后解锁使得文件变为可读,在另一个进程对文件进行操作的时候,当前进程会被阻塞。以网络I/O为例,对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来,这段时间当前进程就会被阻塞以等待socket变为可读。
  (2)将从设备中读取的数据从内核空间拷贝到用户空间供进程使用。比如读取文件时需要等待将DMA控制器放置在内核空间的相应数据复制到用户空间,网络I/O时需要等待数据从内核拷贝到用户内存,在这个过程中用户进程也会被阻塞。
  如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
  在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
  select、poll、epoll均是消除了第一个阶段的阻塞,第二个阶段上进程仍然会阻塞,本质上来讲仍属于同步I/O。

  关于select、poll、epoll还可参考:select、poll、epoll之间的区别总结
  关于I/O模式,还可参考:
  IO模型:同步、异步、阻塞、非阻塞
  Linux IO模式及 select、poll、epoll详解