进程:是指在操作系统中正在运行的一个应用程序,是系统资源调度和分配的基本单位。
系统对服务提出请求(如启动I/O操作,访问文件系统),而进行阻塞;正在执行的进程需要从其他合作进程(网络新数据)获得的数据未到达,从而阻塞本进程(如等待网络新数据到达)。
管道;系统IPC(消息队列,信号量,信号,共享内存);套接字socket。
非抢占式调度和抢占式调度。
非抢占方式:当某一进程正在处理机上执行时,即使有某个更为重要或紧迫的进程进入就绪队列,该进程仍继续执行,直到其完成或发生某种事件而进入完成或阻塞状态时,才把处理机分配给更为重要或紧迫的进程。
抢占方式:当某一进程正在处理机上执行时,若有某个更为重要或紧迫的进程进入就绪队列,则立即暂停正在执行的进程,将处理机分配给这个更为重要或紧迫的进程。
可以参考这篇文章: 进程调度策略.
先来先服务调度算法; 非抢占
短作业(进程)优先调度算法; 非抢占
最短剩余时间优先调度算法;抢占
高优先权优先调度算法;抢占
高响应比优先调度算法;抢占
时间片轮转法; 抢占
多级反馈队列调度算法。抢占
由程序、数据和进程控制块(PCB)(堆栈?)组成。其中PCB通常记载进程的相关信息,包括:
以上四条,缺一不可。若缺少第四条,那么就称其为“线程”。如果完全没有用户空间,称其为“内核线程”;如果是共享用户空间,则称其为“用户线程”。
线程是指进程内独立执行某个任务的一个单元,是CPU调度和分配的基本单元。
理论上,一个进程可用虚拟空间是2G,默认情况下,线程的栈的大小是1MB,所以理论上最多只能创建2048个线程。
一个进程可以创建的线程数由可用虚拟空间和线程的栈的大小共同决定。
在windows系统中,线程间的通信一般采用四种方式:全局变量方式、消息传递方式、参数传递方式和线程同步法(信号量)。
消息队列,是最常用的一种,也是最灵活的一种,通过自定义数据结构,可以传输复杂和简单的数据结构。每一个线程都拥有自己的消息队列。 利用系统的提供的事件、信号等通知机制、使用同步锁和自定义数据结构等来实现。
全局变量/共享内存,进程中的线程间内存共享,这是比较常用的通信方式和交互方式。对于标准类型的全局变量,建议使用volatile修饰符,它告诉编译器无需对该变量作任何的优化,即无需将它放到一个寄存器中,并且该值可被外部改变。
使用事件CEvent类,Event对象有两种状态:有信号和无信号,线程可以监视处于有信号状态的事件,以便在适当的时候执行对事件的操作。
1)创建一个CEvent类的对象:CEvent threadStart;它默认处在未通信状态;
2)threadStart.SetEvent();使其处于通信状态;
3)调用WaitForSingleObject()来监视CEvent对象
CPU是如何处理多线程的运行:分配时间段,每个线程运行一个时间段换另一个时间段,从整体看好像都在运行,其实同一时间只在运行一个线程。
调度规则:平均分配,平均分配每个线程占用的cpu的时间片;抢占式(按优先级),优先让优先级高的线程占用CPU。
饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。
多线程的执行是通过cpu的时间片分配,每个线程会分配到一个时间片,循环执行这些线程,线程时间片消耗完了就会进入等待状态,直到分配到新的时间片,因为时间片的时间非常短,所以cpu不停的切换线程。或者有更高优先级的线程进入就绪队列。
上下文切换:就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
是指多线程通过特定的设置(如互斥量,事件对象,临界区)来控制线程之间的执行顺序(即所谓的同步),也可以说是在线程之间通过同步建立起执行顺序的关系。
线程互斥:是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
(1)互斥量
互斥量是最简单的同步机制,即互斥锁。多个进程(线程)均可以访问到一个互斥量,通过对互斥量加锁,从而来保护一个临界区,防止其它进程(线程)同时进入临界区,保护临界资源互斥访问。互斥量是内核对象。
(2)条件变量/事件对象?
与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。条件变量适合多个进程(线程)等待同一事件发生,然后去干某事。
条件变量必须配合互斥量(锁)一起工作。条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。
(3)读写锁
读写锁适合于使用在读操作多,写操作少的情况,比如数据库。读写锁读锁可以同时加很多,但是写锁是互斥的。当有进程或者线程要写时,必须等待所有的读进程或者线程都释放自己的读锁方可以写。
(4)信号量
它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。可以理解为带计数的条件变量。当信号量的值小于0时,工作进程或者线程就会阻塞,等待资源释放。信号量也是内核对象。
线程具有许多传统进程所具有的特征,所以又称为轻型进程(Light-Weight Process)或进程元。相应地把传统进程称为重型进程(Heavy-Weight Process),传统进程相当于只有一个线程的任务。通常一个进程都拥有若干个线程,至少也有一个线程。
概念:进程是指在操作系统中正在运行的一个应用程序,是系统资源调度和分配的基本单位。线程是指进程内独立执行某个任务的一个单元,是CPU调度和分配的基本单元。一个线程只能属于一个进程, 而一个进程可以有多个线程, 但至少有一个线程。
独立性:同一进程中的多个线程独立性比不同进程间的独立性差很多。进程间不会相互影响;线程一个线程挂掉将导致整个进程挂掉。
拥有资源:每个进程都有独立的地址空间和资源。多个线程共享进程的内存,同一进程下多线程共享进程下的资源。线程仅拥有独立运行需要的资源,比如线程中的TCB。
系统开销:进程和线程的创建、撤销,系统都要为之分配和回收资源,比如内存空间、IO设备等。而线程切换只须保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作。操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。需要注意是同一个进程中的线程切换不会引起进程切换,但是不同进程中的线程切换,会导致进程切换。
通信:由于同一进程中的多个线程具有相同的地址空间,致使它们之间的同步和通信的实现,也变得比较容易。进程间通信 IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。在有的系统中,线程的切换、同步和通信都无须操作系统内核的干预。
协程:是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序员自己写程序来管理的轻量级线程叫做[ 用户空间线程 ],具有对内核来说不可见的特性。协程拥有自己的寄存器上下文和栈。如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。
适用于被阻塞的,且需要大量并发的场景。 但不适用于大量计算的多线程,遇到此种情况,最好是用线程去解决。
多进程并发是指,存在多个单线程的进程,将应用程序分为多个。多进程一个很大的优点就是进程间非常独立,除了通信之外基本不会互相影响,即当一个进程崩溃时不会影响主进程和其他进程。
并发的另一个途径是单个进程中运行多个线程,每个线程可以相互独立运行,但是进程中的所有线程都享有共同的地址空间,并且线程间拥有不少共享数据。线程间的同步比较复杂,并且加锁之类的操作也有成本,会耗费一些资源;同时安全性问题也存在,一个线程挂掉,其他的也会同时挂掉。
为了性能而使用并发:为了在硬件条件允许的情况下为了提高性能,在同一时刻干好几个任务,肯定比一个一个任务执行来的快。
为了划分关注点而使用并发:将任务拆解,将关注点划分开,易于管理。
多线程指的是在单个程序中可以同时运行多个不同的线程,执行不同的任务。
系统接受实现多用户多请求的高并发时,通过多线程来实现。
大任务处理起来比较耗时,这时候可以采用多个线程并行 加快处理。
CAS是解决多线程并行情况下使用锁造成性能损耗问题的一种机制。无锁队列的内部实现实际也是原子操作,可以避免多线程调用出现的不可预知的情况。
CAS主要有三个操作数,当前值A、内存值V和要更改的新值B。当当前值A跟内存值V相等,那么就将内存值V改成B;当 当前值A和内存值V不想等的话,要么就重试,要么放弃更新B。实际就是多线程操作的时候,不加锁,多线程操作了共享的资源之后,在实际修改的时候判断是否修改成功。
内存一直是计算机系统中宝贵而又紧俏的资源,内存能否被有效、合理地使用,将直接影响到操作系统的性能。
内存管理的目的:一是方便用户使用;二是提高存储器的利用率。
页式存储管理将程序逻辑地址空间划分为固定大小的页(page),而物理内存划分为同样大小的页框(page frame)。为方便地址转换,页面大小应是2的整数幂。每一个作业有一个页表,用来记录各个页在内存中所对应的块(页框)。
优点:没有外碎片,每个内碎片不超过页的大小。
缺点:程序全部装入内存,要有硬件支持。如地址变换、缺页中断的产生和选择淘汰页面等都要求有相应的硬件支持。增加了机器成本和系统开销。
地址结构包含两部分:前一部分为页号P,后一部分为页内偏移量W。其中页号与页内偏移量所占多少位,与页面的大小和主存的最大容量有关。
每页大小为4KB,主存大小为4GB。则地址长度为32 位,其中011位为页内地址,即:1231位为页号,地址空间最多允许有2^20页。
地址变换机制:若页表全部放在内存中,则存取一个数据或一条指令至少要访问两次内存:一次是访问页表,确定所存取的数据或指令的物理地址,第二次才根据该地址存取数据或指令。显然,这种方法比通常执行指令的速度慢了一半。
局部性原理:
时间局部性:如果执行了程序中的某条指令, 那么不久之后这条指令很有可能再次执行; 如果某个数据被访问过, 不久之后该数据很可能再次被访问。
空间局部性:一旦程序访问了某个存储单元, 在不久之后, 其附近的存储单元也很有可能被访问到。
为此,在地址变换机构中增设了一个具有并行查找能力的高速缓冲存储器——快表,又称联想寄存器(TLB),用来存放当前访问的若干页表项,以加速地址变换的过程。而内存中的页表常称为慢表。
请求分页系统建立在基本分页系统基础之上,为了支持虚拟存储器功能而增加了请求调页功能和页面置换功能。请求分页是目前最常用的一种实现虚拟存储器的方法。在请求分页系统中,只要求将当前需要的一部分页面装入内存,便可以启动作业运行。在作业执行过程中,当所要访问的页面不在内存时,再通过调页功能将其调入,同时还可以通过置换功能将暂时不用的页面换出到外存上,以便腾出内存空间。
页面置换算法:
段式存储管理要求每个作业的地址空间按照程序自身的逻辑划分为若干段,每个段都有一个唯一的内部段号,每段从0开始编址。以段为单位进行分配, 每个段在内存中占据连续空间, 但各段之间可以不相邻。
优点:可以分别编写和编译;可以针对不同类型的段采取不同的保护;可以以段为单位来进行共享,包括通过动态链接进行代码共享。
缺点:会产生碎片。
逻辑地址由段号S(16-31)与段内偏移量W(0-15)两部分组成。段号和段内偏移量都为16位,则一个作业最多可有2^16=65536个段,最大段长为64KB。
程序分为多个段, 各个段离散地装入内存, 为了保证程序能正常运行, 就必须能从物理内存中找到各个逻辑段的存放位置。为此, 需为每个进程建立一张段映射表, 简称"段表"。每个段对应一个段表项, 记录着该段在内存中的起始位置 (基址) 和 段长。各个段表项的长度是相同的, 因此和页号一样, 段号是"隐含"的, 不占据存储空间。
分段和分页的对比:
在段页式存储中,每个分段又被分成若干个固定大小的页。
优点:没有外碎片,每个内碎片不超过页的大小,能提高存储空间的利用率。可以分别编写和编译;可以针对不同类型的段采取不同的保护;可以以段为单位来进行共享,包括通过动态链接进行代码共享。有利于实现程序的动态连接。
缺点:由于管理软件的增加,复杂性和开销也增加。另外需要的硬件以及占用的内存也有所增加,使得执行速度下降。存在系统抖动的危险。
逻辑地址由段号S(16-35)、段内页号P(12-15)与段内偏移量W(0-11)三部分组成。段号的位数决定了每个进程最多可以分为几个段;页号位数决定了每个段最大有多少页;页内偏移量决定了页面大小和内存块的大小。
"分段"对用户是可见的, 而将各段"分页"对用户是不可见的, 系统会根据段内地址自动划分页号和段内偏移量, 因此段页式管理的地址结构是"二维"的。
每一个进程对应一个段表, 每一个段又对应一个页表, 因此一个进程可能对应多个页表。
查找过程:
虚拟内存:在程序装入时, 将程序中很快会用到的部分装入内存, 暂时用不到的部分留在外存, 就可以让程序开始执行。在程序执行过程中, 当所访问的信息不在内存时, 由操作系统负责将所需信息由外存调入内存, 然后继续执行程序。内存空间不够时, 操作系统负责将内存中暂时用不到的信息换出到外存。在用户看来, 就有一个比实际内存大很多的内存, 这就叫虚拟内存。
在早期的计算机中,是没有虚拟内存的概念的。我们要运行一个程序,会把程序全部装入内存,然后运行。当运行多个程序时,经常会出现以下问题:
虚拟内存的最大容量是由计算机的地址结构 (CPU的寻址范围) 确定的, 虚拟内存的实际容量 = min(内存容量+外存容量, CPU寻址范围)
在请求分页操作系统中, 每当要访问的页面不在内存时, 便产生一个缺页中断, 然后由操作系统的缺页中断处理程序处理中断。此时缺页的进程阻塞, 放入阻塞队列, 调页完成后再将其唤醒, 放回就绪队列。
如果内存中有空闲块, 则为进程分配一个空闲块, 将所缺页面装入该块, 并修改页表中相应的页表项。
如果内存中没有空闲块, 则由页面置换算法选择一个页面淘汰, 若该页面在内存期间被修改过, 则要将其写回外存, 未修改过的页面不用写回外存。
缺页中断是因为当前执行的指令想要访问目标页面未调入内存而产生的, 因此属于内中断。
请求分页存储管理器中给进程分配的物理块的集合。(系统给进程分配了n各物理块 ----的另一种表述 : 驻留集大小为n)。
采用虚拟存储技术的系统中, 驻留集的大小一般小于进程的总大小。如果驻留集太小, 会导致缺页频繁, 系统要花大量的时间来处理缺页, 实际用于进程推进的时间很少。如果驻留集太大, 会导致多道程序并发度下降, 资源利用率降低。
固定分配全局置换:系统为每个进程分配一定数量的物理块,在整个运行期间都不改变。
可变分配全局置换:只要缺页就给进程分配新物理块。
可变分配局部置换:根据发生缺页的频率来动态增加或减少进程的物理块。
文件区用于调入不会被修改的数据, 对换区用用于调入可能被修改的数据。
一般来说 驻留集 的大小不能小于 工作集 的大小, 否则进程运行过程中将频繁缺页。
CPU 寄存器和程序计数器就是 CPU 上下文,因为它们都是 CPU 在运行任何任务前,必须的依赖环境。
CPU 寄存器是 CPU 内置的容量小、但速度极快的内存。
程序计数器则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。
上下文切换:就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
CPU 上下文切换的类型:进程、线程、中断上下文切换。
Linux 按照特权等级,把进程的运行空间分为内核空间和用户空间,分别对应着下图中, CPU 特权等级的 Ring 0 和 Ring 3。
内核空间(Ring 0)具有最高权限,可以直接访问所有资源;
用户空间(Ring 3)只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用陷入到内核中,才能访问这些特权资源。
进程既可以在用户空间运行,又可以在内核空间中运行。进程在用户空间运行时,被称为进程的用户态;而陷入内核空间的时候,被称为进程的内核态。
下图中灰色部分即为进程上下文切换:
从用户态到内核态的转变,需要通过系统调用来完成。比如,当我们读写文件内容时,就需要多次系统调用来完成:首先调用 open() 打开文件,然后调用 read() 读取文件内容,并调用 write() 将内容写到标准输出,最后再调用 close() 关闭文件。
在这个过程中就发生了 CPU 上下文切换,整个过程是这样的:
1、保存 CPU 寄存器里原来用户态的指令位
2、为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。
3、跳转到内核态运行内核任务。
4、当系统调用结束后,CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程。
一次系统调用的过程,其实是发生了两次 CPU 上下文切换。(用户态-内核态-用户态)。
系统调用过程中,并不会涉及到虚拟内存等进程用户态的资源,也不会切换进程。这跟进程上下文切换是不一样的:进程上下文切换,是指从一个进程切换到另一个进程运行;而系统调用过程中一直是同一个进程在运行。系统调用过程通常称为特权模式切换,而不是上下文切换。系统调用属于同进程内的 CPU 上下文切换。系统调用过程中,CPU 的上下文切换还是无法避免的。
首先,进程是由内核来管理和调度的,进程的切换只能发生在内核态。所以,进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。
因此,进程的上下文切换就比系统调用时多了一步:在保存内核态资源(当前进程的内核状态和 CPU 寄存器)之前,需要先把该进程的用户态资源(虚拟内存、栈、全局变量等)保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。但保存上下文和恢复上下文的过程并不是“免费”的,需要内核在 CPU 上运行才能完成。
线程与进程最大的区别在于:线程是CPU调度和分配的基本单位,而进程则是系统进行资源调度和分配的基本单位。所谓内核中的任务调度,实际上的调度对象是线程;而进程只是给线程提供了虚拟内存、全局变量等资源。
所以,对于线程和进程,可以这么理解: - 当进程只有一个线程时,可以认为进程就等于线程。 - 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。 - 另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。
前后两个线程属于不同进程时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。前后两个线程属于同一个进程时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据(如栈、寄存器)等不共享的数据。
发生线程上下文切换的场景:
为了快速响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要将进程当前的状态保存下来,这样在中断结束后,进程仍然可以从原来的状态恢复运行。
跟进程上下文不同,中断上下文切换并不涉及到进程的用户态。所以,即便中断过程打断了一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文,其实只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。
对同一个 CPU 来说,中断处理比进程拥有更高的优先级,所以中断上下文切换并不会与进程上下文切换同时发生。同样道理,由于中断会打断正常进程的调度和执行,所以大部分中断处理程序都短小精悍,以便尽可能快的执行结束。
另外,跟进程上下文切换一样,中断上下文切换也需要消耗 CPU,切换次数过多也会耗费大量的 CPU,甚至严重降低系统的整体性能。所以,当发现中断次数过多时,就需要注意去排查它是否会给你的系统带来严重的性能问题。
字节序:指多字节数据在计算机内存中存储或网络传输时各字节的存储顺序。
网络字节序定义:收到的第一个字节被当作高位看待,这就要求发送端发送的第一个字节应当是高位。而在发送端发送数据时,发送的第一个字节是该数字在内存中起始地址对应的字节。可见多字节数值在发送前,在内存中数值应该以大端法存放。
是指数据的低位(就是权值较小的后面那几位)保存在内存的高地址中,而数据的高位,保存在内存的低地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放。
是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致。
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为 8bit。对于位数大于 8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。我们常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
Linux中判断大小端的一种方法:常用的方法有使用联合体和指针法。
static union { char c[4]; unsignedlong mylong; } endian_test = {{'l','?','?','b' } }; #define ENDIANNESS ((char)endian_test.mylong) if(ENDIANNESS == ‘b’) cout << ”It’s big endian.” << endl; else cout << ”It’s little endian.” << endl;
I/O多路复用(multiplexing)的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。即通过I/O多路复用让一个进程同时为多个客户端端提供服务。本质上是同步I/O。
Linux API 提供了三种 I/O 复用方式:select、poll 和 epoll。
Unix四种I/O模型:阻塞I/O,非阻塞I/O,I/O多路复用,异步I/O 。
现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。任何进程都是在操作系统内核的支持下运行的,与内核紧密相关,且进程切换是非常耗费资源的。
正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。进程进入阻塞状态不占用CPU资源。
是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
缓存I/O又称为标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存中,即数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。select()的机制中提供一种fd_set的数据结构,实际上是一个long类型的数组,每一个数组元素都能与一打开的文件句柄(不管是Socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据I/O状态修改fd_set的内容,由此来通知执行了select()的进程哪一Socket或文件可读。
从流程上来看,使用select函数进行I/O请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。
使用select以后最大的优势是:用户可以在一个线程内同时处理多个socket的I/O请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个I/O请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
存在的问题:
poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。也就是说,poll只解决了上面的问题3,并没有解决问题1,2的性能开销问题。
poll改变了文件描述符集合的描述方式(链表),使用了pollfd结构而不是select的fd_set结构,使得poll支持的文件描述符集合限制远大于select的1024。
epoll的核心是3个API:epoll_create;epoll_ctl;epoll_wait。核心数据结构是:1个红黑树和1个链表。
epoll在Linux2.6内核正式提出,是基于事件驱动的I/O方式,相对于select来说,epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用I/O接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核I/O事件异步唤醒而加入Ready队列的描述符集合就行了。
epoll除了提供select/poll那种I/O事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存I/O状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
水平触发(LT):默认工作模式,即当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;下次调用epoll_wait时,会再次通知此事件
边缘触发(ET):当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次通知此事件。(直到你做了某些操作导致该描述符变成未就绪状态了,也就是说边缘触发只在状态由未就绪变为就绪时通知一次)。ET模式很大程度上减少了epoll事件的触发次数,因此效率比LT模式下高。
epoll是Linux目前大规模网络并发程序开发的首选模型。在绝大多数情况下性能远超select和poll。目前流行的高性能web服务器Nginx正是依赖于epoll提供的高效网络套接字轮询服务。但在并发连接不高的情况下,多线程+阻塞I/O方式可能性能更好。
ping用于确定本地主机是否能与另一台主机成功交换(发送与接收)数据包,再根据返回的信息,就可以推断TCP/IP参数是否设置正确,以及运行是否正常、网络是否通畅等。原理:利用网络上机器IP地址的唯一性,给目标IP地址发送一个数据包,要求对方返回一个同样大小的数据包来确定两台机器是否连接相通,时延是多少。
要监测主机A(192.168.0.1)和主机B(192.168.0.2)之间网络是否可达,那么在主机A上输入命令:ping 192.168.0.2。此时,ping命令会在主机A上构建一个 ICMP的请求数据包(数据包里的内容后面再详述),然后 ICMP协议会将这个数据包以及目标IP(192.168.0.2)等信息一同交给IP层协议。IP层协议得到这些信息后,将源地址(即本机IP)、目标地址(即目标IP:192.168.0.2)、再加上一些其它的控制信息,构建成一个IP数据包。 IP数据包构建完成后,还需要加上MAC地址,因此,还需要通过ARP映射表找出目标IP所对应的MAC地址。当拿到了目标主机的MAC地址和本机MAC后,一并交给数据链路层,组装成一个数据帧,依据以太网的介质访问规则,将它们传送出去。
当主机B收到这个数据帧之后,会首先检查它的目标MAC地址是不是本机,如果是就接收下来处理,否则丢弃。接收之后会检查这个数据帧,将数据帧中的IP数据包取出来,交给本机的IP层协议,然后IP层协议检查完之后,再将ICMP数据包取出来交给ICMP协议处理,当这一步也处理完成之后,就会构建一个ICMP应答数据包,回发给主机A。
在一定的时间内,如果主机A收到了应答包,则说明它与主机B之间网络可达;如果没有收到,则说明网络不可达。除了监测是否可达以外,还可以利用应答时间和发起时间之间的差值,计算出数据包的延迟耗时。
操作系统实现的所有系统调用所构成的集合即程序接口或应用编程接口(Application Programming Interface,API)。是应用程序同系统之间的接口。Linux系统调用,包含了大部分常用系统调用和由系统调用派生出的的函数。
系统调用执行过程:用户在程序中使用系统调用,给出系统调用名和函数后,即产生一条相应的陷入(异常)指令,通过陷入处理机制调用服务,引起处理机中断,然后保护处理机现场,取系统调用功能号并寻找子程序入口,通过入口地址表来调用系统子程序,然后返回用户程序继续执行。
系统调用是最底层的应用,是面向硬件的。而库函数的调用是面向开发的,相当于应用程序的API(即预先定义好的函数)接口;
各个操作系统的系统调用是不同的,因此系统调用一般是没有跨操作系统的可移植性;而库函数的移植性良好(c库在Windows和Linux环境下都可以操作);
库函数属于过程调用,调用开销小;系统调用需要在用户空间和内核上下文环境切换,开销较大;
库函数调用函数库中的一段程序,这段程序最终还是通过系统调用来实现的;系统调用 调用的是系统内核的服务。
系统调用是发生在内核空间的,所以如果在用户空间使用系统调用进行文件操作,必然会引起用户空间到内核空间切换的开销。(系统调用是一个很费时的操作)事实上,即使在用户空间使用库函数对文件进行操作,因为文件总是存在于存储介质上,因此,读写操作都是对硬件(存储器)的操作,所以肯定会引起系统调用,也就是说,库函数对文件的操作是通过系统调用来实现的,(即库函数封装了系统调用的很多细节)。例如C库函数的fopen()就是通过系统调用open ()来实现的。
文件的读写操作通常是大量的数据(大量是底层实现而言),这时,使用库函数可以大大减少系统调用的次数。这一结果源于缓冲区技术,在内核空间和用户空间,对文件操作都使用了缓冲区,例如用fwrite()写文件,都是先将内容写到用户空间缓冲区,当用户空间缓冲去写满或者写操作结束时,才将用户缓冲区的内容写到内核缓冲区,同样的道理,当内核缓冲区写满或者写结束时才将内核缓冲区的内容写到文件对应的硬件媒介上。
中断是指CPU对系统发生的某个事件做出的一种反应,CPU暂停正在执行的程序,保存现场后自动去执行相应的处理程序,处理完该事件后再返回中断处继续执行原来的程序。中断是多程序并发执行的前提条件。用户态到核心态的转换就是通过中断机制实现的,并且中断是唯一途径。
中断的处理过程:
(1)执行完每个指令后,CPU都要检查当前是否有外部(未响应)中断信号。
(2) 如果检测到外部中断信号,则需要保护被中断进程的CPU环境(如程序状态字PSW、程序计数器、各种通用寄存器)。
(3) 根据中断信号类型转入相应的中断处理程序,进行中断处理。
(4) 恢复进程的CPU环境并退出中断,返回原进程继续往下执行。
(1)ps命令:查看静态的进程统计信息。
ps aux命令:以简单列表的形式显示进程信息。
ps -elf命令:以长格式显示系统中的进程信息。
(2)top命令:查看进程的动态信息(3s刷新一次),类似于任务管理器。
(3)pgrep命令:查询进程信息。结合 “-l” 选项可同时输出对应的进程名;结合 “-U” 选项查询指定用户的进程。
(4)pstree命令:查看进程树。
pstree -aup 命令:显示各进程对应的完整命令、用户名、PID 号等信息。
(1)vim命令
vim filename命令:打开vim并创建名为filename的文件。
命令模式:使用 Vim 编辑文件时,默认处于命令模式。在此模式下,可以使用上、下、左、右键或者 k、j、h、l 命令进行光标移动,还可以对文件内容进行复制、粘贴、替换、删除等操作。
输入模式:在输入模式下可以对文件执行写操作,类似在 Windows 的文档中输入内容。进入输入模式的方法是输入 i、a、o 等插入命令,编写完成后按 Esc 键即可返回命令模式。
编辑模式:要保存、查找或者替换一些内容等,就需要进入编辑模式。编辑模式的进入方法为:在命令模式下按":“键,Vim 窗口的左下方会出现一个”:“符号,这时就可以输入相关的指令进行操作了。指令执行后会自动返回命令模式。
保存和退出:字母"w”:保存不退出;字母"q":不保存退出;字符"!":强制性操作;
(2)echo命令
echo “字符串”,功能:在屏幕上打印 字符串。
echo ‘hello linux’ >> /data/hello.txt :单行内容追加到文件结尾。
一个大于号>,是覆盖重定向,会清除文件里的所有以前数据,增加新数据。
两个大于号>>,是追加重定向,文件结尾加入内容,不会删除已有文件内容。
linux的ip命令和ifconfig类似,但前者功能更强大,并旨在取代后者。
查看IP:ifconfig -a命令;ip addr命令
修改IP:vim /etc/sysconfig/network-scripts/ifcfg-eth0
cat:由第一行开始显示内容,并将所有内容输出;
tac:从最后一行倒序显示内容,并将所有内容输出;
more:根据窗口大小,一页一页的现实文件内容;
less:和more类似,但其优点可以往前翻页,而且进行可以搜索字符;
head:只显示头几行;
tail:只显示最后几行。
grep命令是一种强大的文本搜索工具,它能使用正则表达式搜索文本,并把匹配的行打印出来。
glibc是GNU发布的libc库,即c运行库。glibc是linux系统中最底层的api,几乎其它任何运行库都会依赖于glibc。glibc除了封装linux操作系统所提供的系统服务外,它本身也提供了许多其它一些必要功能服务的实现。
Netstat 命令用于显示各种网络相关信息,如网络连接,路由表,接口状态 (Interface Statistics),masquerade 连接,多播成员 (Multicast Memberships) 等等。
常见参数:
-a (all)显示所有选项,默认不显示LISTEN相关
-t (tcp)仅显示tcp相关选项
-u (udp)仅显示udp相关选项
-n 拒绝显示别名,能显示数字的全部转化成数字。
-l 仅列出有在 Listen (监听) 的服务状态
-p 显示建立相关链接的程序名
-r 显示路由信息,路由表
-e 显示扩展信息,例如uid等
-s 按各个协议进行统计
-c 每隔一个固定时间,执行该netstat命令。
提示:LISTEN和LISTENING的状态只有用-a或者-l才能看到
单核CPU仍然存在线程切换,且不能保证调度的顺序性和任务的原子性,因此仍然存在线程不安全问题。
i++过程:将i值取出放到寄存器 —> 将寄存器中的值返回 —> 寄存器中的值加1 —> 使用寄存器值修改i的值。
++i过程:将i值取出放到寄存器 —> 寄存器中的值加1 —> 将寄存器中的值返回并修改i的值。
如果线程1在执行第一条代码的时候,线程2访问i变量,这个时候,i的值还没有变化,还是原来的值,所以是不安全的。
如果CPU 持续跑高,则会影响业务系统的正常运行,带来企业损失。
方法一:
第一步:使用 top 命令,然后按shift+p按照CPU排序,找到占用CPU过高的进程的pid。
第二步:使用 top -H -p [进程id] 找到进程中消耗资源最高的线程的id;
第三步:使用 echo ‘obase=16;[线程id]’ | bc或者printf “%x\n” [线程id] 将线程id转换为16进制(字母要小写);bc是linux的计算器命令;
第四步:执行 jstack [进程id] |grep -A 10 [线程id的16进制]” 查看线程状态信息。
方法二:
第一步:使用 top 命令,然后按shift+p按照CPU排序,找到占用CPU过高的进程;
第二步:使用 ps -mp pid -o THREAD,tid,time | sort -rn 获取线程信息,并找到占用CPU高的线程;
第三步:使用 echo ‘obase=16;[线程id]’ | bc或者printf “%x\n” [线程id] 将需要的线程ID转换为16进制格式;
第四步:使用 jstack pid |grep tid -A 30 [线程id的16进制] 打印线程的堆栈信息。
vmstat -n 1 # -n 1 表示结果一秒刷新一次。