• 操作系统复习知识


    一、进程和线程

    1. 进程和线程的区别:

      1. 进程是一个正在执行中的程序,包括程序计数器、寄存器和变量的当前值;一个进程包含一个或多个线程。
      2. 进程是操作系统分配资源的最小单位;而线程是作为独立运行和CPU调度的基本单位。
      3. 进程间的资源是独立的,而同一进程的各线程间资源是共享的;进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间、建立数据表来维护代码段、堆栈段和数据段,这种操作是非常昂贵的;而线程是共享进程中的数据的,使用相同的地址空间,因此线程间的切换和创建所产生的开销远比进程少。
      4. 同一进程下的线程间的通信通过共享内存来进行,而进程之间的通信需要以通信的方式进行(IPC)。
    2. 线程的状态、线程状态切换

      线程共包括一下5种状态:

      1. 新建状态(new):线程对象被创建后,就进入新建状态;
      2. 就绪状态(Runnable):也可被称为“可执行状态”,一个新建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法,当start()方法返回后,线程就处于就绪状态。
      3. 运行状态(Running):线程获取CPU权限进行执行,需要注意的是,线程只能从就绪状态进入运行状态
      4. 阻塞状态(Blocked):阻塞状态是因为某种原因放弃CPU使用权,暂时停止运行,直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
        1. 等待阻塞:通过调用线程的wait()方法,让线程等待某工作的完成。
        2. 同步阻塞:线程在获取synchronize同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
        3. 其它阻塞:通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或超时、或者I/O处理完毕时,线程重新转入就绪状态。
      5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
    3. 进程间通信:进程间通信(Inter Process Communication,IPS)是一个进程与另一个进程间共享消息的一种通信方式。主要目的有数据传输、共享数据、事件通知、资源共享、进程控制等。主要通信方式为:

      1. 管道(匿名管道,pipe):在内存中申请一块缓冲区,进程可以写入和读取;但是其是一种半双工的通信,数据只能单向流动;同时只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。一般使用fork函数实现父子进程的通信。

      2. 命名管道(FIFO):也是半双工的通信方式,但是它允许无亲缘关系进程间的通信;FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。FIFO的通信方式类似于在进程中使用文件来传输数据,只不过FIFO类型文件同时具有管道的特性。在数据读出时,FIFO管道中同时清除数据,并且“先进先出”。

      3. 消息队列:由消息组成的链表,存放在内核中并由消息队列标识符标识。消息队列可以实现消息的随机访问,也可以按消息的类型读取;消息队列独立于发送和接收进程,进程终止时,消息队列及其内容并不会被删除。

      4. 信号量:本质上就是一个计数器,可以用来控制多个进程对共享资源的访问。常作为一种锁机制,防止某进程正在访问共享资源时、其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段

      5. 共享内存:共享内存就是映射一段能被其他进程所能访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,不需要从用户态到内核态的频繁切换和拷贝数据,直接从内存中读取即可,它是针对其他进程间通信方式运行效率低而专门设计的。但共享内存是临界资源,所以共享内存经常和信号量结合在一起使用,共享内存用来传递数据,信号量用来同步对共享内存的访问。 共享内存主要通过内存映射或者共享内存机制来实现。

      6. 套接字(socket):也是一种进程间通信机制,不同的是,可用于不同机器间的进程通信。

      7. 信号(signal):是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生

    4. 线程间通信的方式:因为线程间可以共享资源,所以无需特别的手段进行通信。所以线程间通信的目的主要用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。同步方式如下:

      1. 锁机制:

        1. 互斥锁:提供了以排他方式防止数据结构被并发修改的方法,lock()、unlock();
        2. 读写锁:允许多个线程同时读共享数据,而对写操作是互斥的;
        3. 条件变量:使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正在被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足,对条件的测试是在互斥锁的保护下进行的,所以条件变量始终与互斥锁在一起使用。wait()、signal()
      2. 信号量机制:每次调用wait操作(相当于进程间通信的P)将会使semaphore值减1,而如果semaphore值为0时,则wait操作会阻塞;每一次调用post操作(相当于V操作)将会使semaphore值+1。

      3. 信号机制:类似于进程间的信号处理。

    5. 僵尸进程和孤儿进程:

      1. 僵尸进程:fork()产生的子进程。子进程先与父进程退出后,子进程的数据结构PCB需要其父进程释放,但是父进程并没有释放,则该子进程就成为了僵尸进程,无法正常结束(以root身份kill -9也无法杀死僵尸进程)。

      2. 孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么这些进程将会变成孤儿进程,孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。孤儿进程实际上是不占用资源的,因为它最终是被系统回收了,不会将僵尸进程那样占用进程ID,损害系统运行。

      3. 僵尸进程的危害及处理:危害:系统所能使用的进程号是有限的,如果产生大量的僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。处理:1)、暴力的办法就是杀死僵尸进程的父进程,使得僵尸进程变成“孤儿进程”,过继给1号进程init,init始终会负责清理僵尸进程(大多数情况下是不可取的,若其父进程为服务器程序,则得不偿失)。2)、SIGCHILD信号:实际上当子进程终止时,内核就会向它的父进程发送一个SIGCHILD信号,父进程可用选择忽略该信号,也可以提供一个接收到信号以后的处理函数。当父进程接收到SIGCHILD信号后就应该调用wait()或waitpid()函数对子进程进行善后处理,释放子进程占用的资源。

        1. wait()函数:wait函数是用来处理僵尸进程的,但是进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞父进程,直到有一个出现为止。
        2. waitpid函数:与wait函数起到同样的效果,不同点是:waitpid可以等待一个指定进程号的子进程,而wait只能等待任意的子进程;系统一旦调用wait函数就会阻塞父进程来等待,直到等待子进程的退出才停止阻塞,而waitpid提供了一种非阻塞方式的等待,也就是当第三个参数设置为WNOHANG,当子进程没有结束时,直接返回0,不再阻塞父进程;
    6. 进程调度算法

      1. 先来先服务调度算法(FCFS, First Come First Served):算法比较简单,可以实现基本上的公平;

      2. 短作业(进程)优先调度算法(SJF,Shortest Job First):优先调度所需时间较少的作业(进程)执行;

      3. 优先权调度算法:从后备队列中选取优先级高的作业执行。

      4. 最高响应比优先法(HRRN,Highest Response Ratio Next):响应比R=(W+T)/T=1+W/T,其中T为该作业估计需要的执行时间,W为作业在后备状态队列中的等待时间。每当要进行作业调度时,系统计算每个作业的响应比,选择R最大者投入执行。(等待时间除以执行时间,越大的越先执行)。

      5. 时间片轮转法(RR,Round-Robin):让就绪进程以FCFS的方式按时间片轮流使用CPU的调度方式,即将系统中所有的就绪进程按照FCFS原则,排成一个队列,每次调度时将CPU分派给队首进程,让其执行一个时间片,时间片的长度从几个ms到几百ms。在一个时间片结束时,发生时钟中断,调度程序据此暂停当前进程的执行,将其送到就绪队列的末尾,并通过上下文切换执行当前的队首进程,进程可以使用完一个时间片就让出CPU。

      6. 多级反馈队列(Multilevel Feedback Queue):UNIX操作系统采用的便是这种调度算法

        1. 进程在进入待调度的队列等待时,首先进入优先级最高的Q1等待;
        2. 首先调度优先级高的队列中的进程。若高优先级中队列已没有调度的进程,则调度次优先级队列中的进程。例如:Q1、Q2、Q3三个队列,只有在Q1没有进程等待时采取调度Q2,同理只有在Q1、Q2都为空时才会去调度Q3;
        3. 对于同一个队列中的各个进程,按照时间片轮转法调度。比如Q1队列中的时间片为N,那么Q1中的作业在经历了N个时间片后若还没完成,则进程Q2队列等待,若Q2的时间片用完后作业还不能完成,一直进入下一级队列,直至完成;
        4. 在低优先级的队列中的进程在运行时,又有新到达的作业,那么在运行完这个时间片后,CPU马上分配给新到达的作业(抢占式)。

    二、锁

    1. 死锁:指两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法继续执行。如下图所示,进程C等待资源T的释放,资源T却被进程D占用;进程D等待请求占用资源U,资源U却已经被线程C占用,从而形成环,导致死锁。

    2. 产生死锁的四个必要条件:

      1. 互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有,此时若有其他进程请求该资源时,则请求进程只能等待;
      2. 请求和保持条件:进程A已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被进程B占有,此时进程A的请求被阻塞,但是进程A对已经获得的资源保持不放;
      3. 不可剥夺条件:进程已获得的资源在未使用完之前不能被其他进程强行夺走,只能在使用完时由自己释放;
      4. 循环等待:死锁发生时,系统中一定有两个或两个以上的进程组成一个循环,循环中的每个进程都在等待下一个进程释放自己所需的资源,如上图所示。
    3. 处理死锁的方法:

      1. 预防死锁:通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个,来防止死锁的发生;
      2. 避免死锁:在资源的动态分配过程中,用某种方法去防止系统进程不安全状态,从而避免死锁的发生;
      3. 检测死锁:允许系统在运行过程中发生死锁,但可设置检测机构及时检测死锁的发生,并采取适当措施加以清除;
      4. 解除死锁:当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来。
    4. 预防死锁:通过破坏产生死锁的四个必要条件中的一个或几个来预防死锁的发生:

      1. 破坏互斥条件:就是在系统中取消互斥,若资源不被一个进程独占使用,那么死锁是肯定不会发生的。但一般来说,互斥条件不能被破坏,否则会造成结果的不可再现性。

      2. 破坏请求和保持条件:就是在系统中不允许进程在已获得某种资源的情况下,申请其他资源。所以需要想出一个办法,阻止进程在持有资源的同时申请其他资源;

        1. 方案一:创建进程时,要求它申请所需的全部资源,系统或满足其全部要求,或什么都不给它,这就是所谓的“一次性分配”方案;
        2. 方案二:要求每个进程提出新的资源申请前,释放它所占有的资源。这样,一个进程在需要资源S时,必须把它先前占有的R资源释放掉,然后才能提出对S的申请,即使它可能很快又要用到R资源;
      3. 破坏不可剥夺条件:就是允许对资源进行抢夺;

        1. 方案一:如果占有某些资源的进程A进行进一步资源请求时被拒绝,则进程A必须释放其最初占有的资源,如果有必要,可再次申请这些资源和另外的资源;
        2. 方案二:如果进程A请求当前被进程B占用的一个资源,则操作系统可以抢占进程B,要求它释放资源。这种方案只有在A的优先级高于B时才能使用。
      4. 破坏循环等待条件:将系统中的所有资源统一编号,进程可在任何时候请求资源,但必须按资源的编号递增的顺序请求资源,释放则相反。

    5. 避免死锁:预防死锁是设法破坏产生死锁的四个必要条件之一,严格的防止死锁的出现,而避免死锁则不那么严格的限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁,避免死锁就是在系统运行过程中注意避免死锁的最终发生。

      1. 技术:
        1. 加锁顺序:如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生;
        2. 加锁时限:在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限,该线程则放弃对该锁的请求;若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。
      2. 银行家算法:如果能顺利的找到一组满足所有进程所需资源分配的顺序,则称该系统处于安全状态,即不会产生死锁。但是需要进行在运行前就知道其所需的最大值,且进程数也是不固定的,因此使用有限。
    6. 从死锁中恢复:

      1. 通过抢占恢复:抢占把一个进程正在使用的资源转移到另一个进程中
      2. 通过回滚进行恢复:记录之前的资源状态,
      3. 杀死进程恢复:直接杀死一个死锁进程
      4. 选择一个环外的进程作为牺牲品来释放进程资源。
    7. 其他问题

      1. 活锁:一对并行的进程用到了两个资源,它们分别尝试获取另一个资源失败后,两个进程都会释放自己持有的资源,再次进程尝试。很显然这个过程中没有进程阻塞,但是进程仍然不会向下执行。
      2. 饥饿:是指一个可运行的进程尽管能继续执行,但被调度器无限期地忽视,而不能被调度执行的情况。饥饿可以通过先来先服务资源分配策略来避免。
    8. linux的四种锁

      1. 互斥锁:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入休眠,等待锁释放时被唤。
      2. 读写锁:rwlock,分为读锁和写锁,处于读操作时,可以允许多个线程同时获得读操作;但是同一时刻只能有一个线程可以获得写锁。其他获取写锁失败的线程都会进入休眠状态,直到写锁释放时被唤醒。注意,写锁会阻塞其他读写锁,当有一个线程获得写锁在写时,读锁也不能被其他线程获取;写优先于读(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)。读写锁适用于读取数据的频率远远大于写数据的频率的场合。
        1. 互斥锁和读写锁的区别:读写锁区分读者和写者,而互斥锁不分;互斥锁同时只允许一个线程访问该对象,无论读写,而读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。
      3. 自旋锁:spinlock,在任何时候同样只能有一个线程访问对象,但是当获取锁操作失败时,不会进入休眠状态,而是会在原地自旋,直到锁被释放。这样节省了线程从睡眠状态到被唤醒期间的消耗,在加锁时间短暂的环境下会极大的提高效率。但是如果加锁时间过长,则会非常浪费CPU。
      4. RCU:即read-copy-update,在修改数据时,首先要读取数据,然后生成一个副本,对副本进行修改。修改完成后,再将老数据update成新的数据(update之前的读获取的全是老数据,update之后的读获取的全是新数据,数据的一致性时可以保证的)。使用RCU时,读者几乎不需要同步开销,即不需要获得锁,也不使用原子指令,不会导致锁竞争,因此也就不会导致死锁问题。而对于写操作的同步开销较大,它需要复制被修改的数据,还必须使用锁机制同步并行其他写者的修改操作。在大量读、少量写的情况下效率非常高。(写和读可以并发进行)

    三、文件磁盘存储

    1. 在LINUX系统中有一个重要的概念:一切皆是文件。UNIX系统把每个硬件都看成是一个文件,通常称为设备文件,这样用户就可以用读写文件的方式实现对硬件的访问。

    2. linux正统的文件系统(如ext2、ext3)一个文件由目录项、inode、和数据库组成;

      1. 目录项:包括文件名和inode节点号;
      2. inode:又称为文件索引节点,是文件基本信息(读写属性、owner等)的存放地和数据库指针存放地;
      3. 数据块:文件的具体内容存放地。
      4. 这类系统将硬盘分区时会划分出目录块、inode table区块和data block数据区域,当查看某个文件时,会先从inode table中查出文件属性及数据存放点,再从数据块中读取数据。
    3. 硬链接与软连接:在linux中,元数据中的inode号才是文件的唯一标识,而非文件名。linux系统为解决文件的共享使用,采用了硬链接和软连接

      1. 硬链接:从技术上讲,他们公用一个inode。由于inode下的文件是通过索引节点(inode)来识别文件,硬链接也可以认为是一个指向文件索引节点的指针,系统并不为它重新分配inode。硬链接可由命令link或ln创建。
        1. 文件有相同的inode及data block;
        2. 只能对已存在的文件进行创建;
        3. 不能交叉文件系统进行硬链接的创建;
        4. 不能对目录进行创建,只可对文件创建;
        5. 删除一个硬链接文件并不影响其他有相同inode号的文件。
      2. 软连接:是一种特殊的文件类型,其中包含对另一个 文件/目录 以 绝对/相对 路径形式的引用。软连接可以看做是对一个文件的简洁指针,相当于windows下的快捷方式。创建文件的软链接时,会使用一个新的inode,所以软链接的inode号和文件的inode不同(表明他们是两个不同的文件)。创建方式,ln -s
      3. 比较:软链接的inode里存放着指向文件的路径,移动源文件到其他目录下,软链接也就无法使用;而硬链接就没这个缺陷,怎么移动都可以。硬链接和源文件是一个文件;软链接和源文件是2个不同的文件。
      4. 为了解决文件共享问题,Linux引入了软链接和硬链接。除了为Linux解决文件共享使用,还带来了隐藏文件路径、增加权限安全及节省存储等好处。若1个inode号对应多个文件名,则为硬链接,即硬链接就是同一个文件使用了不同的别名,使用ln创建。若文件用户数据块中存放的内容是另一个文件的路径名指向,则该文件是软连接。软连接是一个普通文件,有自己独立的inode,但是其数据块内容比较特殊,存放的是源文件的绝对路径,当访问软连接时,系统会自动将其替换成源文件的绝对路径。
      5. https://www.jianshu.com/p/dde6a01c4094
    4. 磁盘读取

      1. 轮询方式(循环检查IO):最古老的方式,CPU以一定的周期按照次序去查询每一个外设,看它是否有数据输入或输出。CPU和IO串行;

      2. 中断方式:比轮询方式更先进一些,进程放弃CPU等待相关IO操作完成。此时,进程调度程序会调度其他就绪进程使用CPU。当IO操作完成时,输入输出设备控制器通过中断请求向CPU发出中断信号,CPU收到中断信号后,转向预先设计好的中断处理程序,对数据传送工作进行相应的处理。CPU和IO并行

      3. DMA(Direct Memory Access,直接存储器访问):DMA有两个技术特征,首先是直接传送,其次是块传送。所谓直接传送,即在内存与IO设备间传送一个数据块的过程中,不需要CPU的任何中间干涉,只需要CPU在过程开始时向设备发出“传送块数据”的命令,然后通过中断来得知过程是否结束和下次操作是否准备就绪。所谓块传送,比中断先进的地方是每次可以读取一个块,而不是一个字。

      4. 通道:通道是一个独立于CPU的,专门管理I/O的处理机,它控制设备与内存直接进行数据交换。比DMA先进的地方是,每次可以处理多个块,而不只是一个块。

      5. https://blog.csdn.net/weixin_43715601/article/details/106269888

    5. 提升文件系统性能的操作(减少磁盘访问次数):

      1. 高速缓存: 检查全部的读请求,查看在高速缓存中是否有所需要的块。如果存在,可执行读操作而无须访问磁盘。如果检查块不在高速缓存中,那么首先把它读入高速缓存,再复制到所需的地方。
      2. 块提前读:第二个明显提高文件系统性能的是,在需要用到块之前,试图提前将其写入高速缓存,从而提高命中率。
      3. 减少磁盘臂运动:把有可能顺序访问的块放在一起,当然最好是同一个柱面上,从而减少磁盘臂的移动次数。
      4. 磁盘碎片整理:文件被不断的创建和清除,就会产生很多的碎片,在创建一个文件时,它使用的块会散布在整个磁盘上,降低性能。

    四、内存管理

    虽然计算机硬件技术一直在飞速发展,内存容量也在不断增大,但仍不可能将所有用户进程和系统所需的全部程序与数据放入主存,因此操作系统必须对内存空间进行合理的划分和有效的动态分配,这就是内存管理。

    1. 内存分配管理方案

      1. 连续分配管理方式:是指为一个用户程序分配一个连续的内存空间,主要包括单一连续分配、固定分区分配和动态分区分配。
        1. 单一连续分配:内存分为系统区和用户区,系统区仅供操作系统使用,在低地址部分;用户区为用户提供的、除系统区之外的内存空间。优点是简单、无外部碎片;缺点是只能用于单用户、单任务的操作系统中(内存中只能存在一个程序),有内部碎片,存储器利用率极低。
        2. 固定分区分配:将用户空间划分为若干固定大小的区域,每个分区只能装入一道作业。存在问题:程序可能太大而放不进任何一个分区中;主存利用率低,当程序小于固定分区大小时,也占用一个完整的内存分区空间,从而产生内部碎片(在内存中产生的碎片)
        3. 动态分区分配:不预先划分内存,而是在进程装入内存时,根据进程的大小动态地建立分区,并使分区的大小正好适合进程的需要。这种分区方法在开始分配时是很好的,但慢慢的会产生许多的外部碎片(在外存中产生的碎片)。可以通过紧凑技术来解决,即操作系统不时地对进程进行移动和整理。动态重定位分配算法与动态分区分配基本相同,增加了紧凑技术。
        4. 动态分区分配策略
          1. 首次适应(First Fit)算法:空闲分区以地址递增的次序连接,分配内存时顺序查找,找到大小能满足要求的第一个空闲分区;该种算法是最简单,通常也是最好和最快的,但是会使得在内存的低地址部分出现很多小的空闲分区。
          2. 最佳适应(Best Fit)算法:空闲分区以容量递增的方式形成分区链,找到第一个能满足要求的空闲分区;性能很差,会留下很多的外部碎片。
          3. 最坏适应(Worst Fit)算法:空闲分区以容量递减的次序链接,找到第一个能满足要求的空闲分区;会导致很快没有可用的大内存块。
          4. 邻近适应(Next Fit)算法:由首次适应算法演化而来,分配内存时从上次查找结束的位置开始继续查找。
      2. 非连续分配管理方式:允许一个程序分散地装入不相邻的内存分区。在连续分配管理方式中,即使内存中有足够的空间,但是若不是连续的话,也不能够被用户程序使用。而非连续分配管理方式就解决了这个问题。
        1. 分页存储管理方案:固定分区会产生内部碎片,动态分区会产生外部碎片,这两种对内存的利用率都比较低。

          1. 分页思想:把主存空间划分为大小相等且固定的块(称为页框、页帧),块相对较小,作为主存的基本单位;每个用户进程也以块(称为)为单位进行划分,分配内存时,以块为单位将进程中的若干个页分别装入到多个可用不相邻的物理块中。
          2. 基本概念:
            1. 进程中的块称为页,内存中的块称为页框,外存中的块称为块
            2. 页表(TLB):系统为每个进程建立的,记录页面在内存中对应的物理块号,一般放在内存中,用于将逻辑地址转换成物理地址
            3. 快表:为了减少存取数据、指令时访问内存的次数,在地址变换机构中增设一个具有并行查找能力的高速缓冲处理器--快表,用来存放当前访问的若干页表项,加速地址变换的过程。
        2. 分段存储管理方案:分页管理方式是从计算机角度考虑设计的,目的是提高内存的利用率,对用户完全透明;分段则是考虑用户和程序员。

          1. 分段思想:按照用户进程的自然段划分逻辑空间。逻辑空间分为若干个段,内存空间为每个段分配一个连续的分区。
          2. 在页式系统中,逻辑地址的页号和页内偏移量对用户是透明的,但在段式系统中,段号和段内偏移量必须由用户显示提供,在高级程序设计语言中,这个工作有编译程序完成。
        3. 段页式管理方案: 分页存储能有效地提高内存利用率;减少碎片的产生,分段存储能反应程序的逻辑结果并有利于段的共享。将这两种存储方法结合起来,便是段页式存储管理方案。

          1. 管理思想:用户进程的地址空间首先被分成若干逻辑段,每段有自己的段号,然后将每段分成若干固定大小的页;对内存空间的管理仍然和分页存储管理一样,将其分成若干页面大小相同的存储块,对内存的分配以存储块为单位。
          2. 地址转换:为了实现地址变换,系统为每个进程建立一张段表,而每个分段有一张页表。段表表项中至少包括段号、页表长度和页表起始地址,页表表项中至少包括页号和块号。
    2. 虚拟内存:

      1. 在程序装入时,将程序的一部分装入内存,其余部分留在外存,程序即可启动执行;在程序执行过程中,当所访问的信息不在内存时,由操作系统将所需要的部分调入内存,然后继续执行程序;同时,操作系统将内存中暂时不需要使用的内存换出到外存上,从而腾出空间存放将要调入内存的信息,这样系统好像为用户提供了一个比实际内存大得多的存储器。说白了,虚拟内存的目的就是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。

      2. 每个进程都有独立的逻辑地址空间,内存被分为大小相等的多个块,称为页,每个页都是一段连续的地址。对于进程来看,逻辑上貌似有很多内存空间,其中一部分对应物理内存上的一块(称为页框,通常页和页框大小相等),还有一些没加载在内存中的对应在硬盘上。

      3. 当访问虚拟内存时,会通过MMU(内存管理单元,把虚拟地址转换为物理地址的硬件设备)去匹配对应的物理地址,而如果虚拟内存的页并不存在于物理内存时,会产生缺页中断,从磁盘中取得缺的页放入内存中,如果内存已满,还会根据某种算法将内存中的页换出。

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

      1. 最佳置换算法(OPT):选择的淘汰页是以后永不使用的页面,或者在最长时间内不再访问的页面,以便保证获得最低的缺页率。但是人们无法预知进程在内存下的若干页面哪个是未来最长时间内不再被访问的,因而算法无法实现,通常用来评价其他算法。

      2. 先进先出置换算法(FIFO):优先淘汰最早进入内存的页面,即在内存中驻留时间最久的页面。该算法实现简单,但性能差,还会产生Belay异常,即所分配的物理块数增大而缺页故障数不减反增的异常现象。

      3. 最近最久未使用算法(LRU):选择最近最长时间未访问过的页面予以淘汰,根据程序局部性原理,刚被访问的页面,可能马上又要被访问;而较长时间内没有被访问的页面,可能最近不会被访问。性能接近于OPT算法,但实现起来比较困难,且开销大。(双向链表+map)

      4. 最近最少使用算法(LFU):选择在最近时期使用最少的页面作为淘汰页,当使用次数相同时,谁最先来淘汰谁。基于“如果一个数据在最近一段时间内使用次数很少,那么在将来的一段时间内被使用的可能性也很小”的思路。

    4. 进程地址空间:linux采用虚拟内存管理技术,每个进程都有各自独立的进程地址空间(即4G的线性虚拟空间),进程无法直接访问物理内存,可以起到保护操作系统、并且让用户程序可使用比实际物理内存更大的地址空间。

      1. 4G进程地址空间被划成两部分,内核空间和用户空间。用户空间从0-3G,内核空间从3-4G;
      2. 用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址。只有用户进程进行系统调用(代表用户进程在内核态执行)等情况可访问到内核空间
      3. 用户空间对应进程,所以当进程切换时,用户空间也会跟着变化;
      4. 内核空间是由内核负责映射,不会随着进程变化;内核空间地址有自己对应的页表,用户进程各自有不同的额页表。所以虚拟进程空间是通过查询进程页表来获得实际物理内存地址的,而虚拟内核空间是通过查询内核页表获取实际物理内存地址的。
    5. 进程与内存:所有的进程必须占用一定数量的内存,这些内存用来存放从磁盘载入的程序代码,或存放来自用户输入的数据等。内存可以提前静态分配和统一回收,也可以按需动态分配和回收。对于普通进程对应的内存空间包含5中不同的数据区:

      1. 一个程序本质上有BSS段、数据段、代码段组成:

        1. 代码段:又称文本段,存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且属于只读区域。
        2. 数据段:存储已初始化的全局变量和静态变量,数据段的数据生存期是随进程持续性,当进程创建就存在,进程死亡就消失。
        3. BSS段(未初始化数据区):存储未初始化的全局变量和未初始化的静态变量;BSS段属于静态分配,随进程持续性,即程序结束后静态变量资源由系统自动释放。
      2. 可执行程序在运行时又多出两个区域:堆区和栈区

        1. 栈区:由编译器自动释放,存放函数的参数值、局部变量等。每当一个函数被调用时,该函数的返回类型和一些调用信息被存放到栈中。栈区从高地址向低地址位增长,是一块连续的内存区域。
        2. 堆:用于动态分配内存,位于BSS和栈中间的地址区域。由程序员申请和释放(malloc、free),频繁的操作会造成内存空间不连续,产生碎片。当申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多。
    6. 其他参考skk面经

    7. 堆与栈的区别

      1. 栈由操作系统自动分配释放,用于存放函数的参数值、局部变量等;而堆由开发人员分配和释放,若开发人员不释放,程序结束时由OS回收,分配方式类似于链表,存储一些全局变量。
      2. 栈的内存地址在高地址,向低地址增长;堆的内存地址在地地址,向高地址增长。
      3. 每个进程拥有的栈的大小要远远小于堆的大小。
      4. 栈的分配效率要高于堆。

    五、IO多路复用

    所谓IO多路复用指的就是select/poll/epoll这一系列的多路选择器:支持单一线程同时监听多个文件描述符(I/O事件),阻塞等待,并在其中某个文件描述符可读写时收到通知。I/O复用其实复用的不是I/O连接,而是复用线程,让一个thread of control能够处理多个连接(I/O事件)。

    1. IO模式

    https://www.zybuluo.com/phper/note/595507

    2. select

    1. 源码解读,如图所示:

      1. 上半部分主要是网络服务器的创建(socket、bind、listen),以及监听(accept)会有哪些文件描述符fd到来,将fd放入数组fds中并记录最大的fd(fd其实就是一个数);这里fd其实就是一个socket连接的文件描述符

      2. 然后进入下半部分的while循环中,代码中的rset其实就是一个bitmap位图,总长度1024(假设有5个fd 12579,均有数据,则rset应该为0110101010...)。select()接收的五个参数含义分别为max+1(用于截取可能有数据的fd,不用遍历1024次)、读文件描述符集合(这里不是fds,而是rset)、写文件描述符集合、异常文件描述符集合、超时时间(网络服务器主要进行读操作,所以后三个可以传入null)。

      3. 在while循环中,select()函数将进行阻塞(阻塞原因就是select函数直接将rset放入内核态进行判断哪些fd是启动或者被监听的,一直等待有数据的到来),循环判断五个文件描述符中的哪些是启用的或者被监听的,当有一个或多个fd被启用(监听)时,将rset中对应的位置置为1(内核做的工作);然后select返回,去遍历判断哪个fd有数据(rset被置位了,是有数据的),将数据读出来并且进行相应的处理。

    2. 优点:将文件描述符收集过来交给内核进行判断是否有数据,效率比较高。

    3. 缺点:

      1. bitmap默认大小只有1024(linux内核定义的,如果需要超过,就得改内核代码重新编译),虽然可以调整,但是还是有限度的;
      2. select函数返回后,rset每次循环都必须重新置位为0,不可重复使用;
      3. 尽管是将rset放入内核态进行判断的,但是从用户态到内核态的拷贝还是有开销的;
      4. 当select返回时,并不知道哪个fd有数据(哪个socket上有数据),仍需要对文件描述符集合进行判断,效率比较低。

    3. poll

    poll的工作原理与select是一样的,主要改进就是使用了一个pollfd结构体,取消了bitmap

    struct pollfd {
       int fd;        // 文件描述符
       short events;  // 在意的事件是什么,如果在于读就是POLLIN,如果在于写就是POLLOUT
       short revents; // 对events的回馈,开始时为0,当有数据可读时就置为1,类似于rset,但是这里是可以重用的
    };
    
    1. 源码解读,如图所示:

      1. 上半部分还是和select的类似,唯一的区别是这里使用pollfds(pollfds是类型为pollfd结构体的数组),同样将accept产生的socket文件描述符存入数组中;由于是网络服务器,读取网络上的数据,所以这里pollfds[i].events=POLLIN;
      2. poll函数的第一个参数为fds数组,第二个参数表示5个文件描述符,第3个参数是超时时间;
      3. 然后进入while循环中,会将fds数组从用户态拷贝到内核态,poll会阻塞;如果有一个或多个fd被启用,则将其对应的revents置为1(POLLIN);然后poll返回
      4. 紧接着,循环遍历pollfds数组,读取revents为1的fd中的数据进行其他操作,同时将其revents置为0便于复用。
    2. 优点:解决了select中bitmap大小限制和rset不可重用的情况。由于两者原理相同,所以缺点3、4尚未解决。poll还一个特点是“水平触发”,如果报告了fd后没有被处理,那么下次poll时会再次报告该fd。

    3. 缺点: 和select的3、4缺点一样,同时poll和select都不是线程安全的。

    4. epoll

    struct epoll_events {
       int fd;
       short events;
    }
    
    1. 源码解读:

      1. 首先调用epoll_create在内核创建一个eventpoll对象,这个对象会维护一个epitem集合,可简单理解为fd集合;

      2. 调用epoll_ctl函数用于将fd封装成epitem加入这个eventpoll对象,由于epoll_ctl相对于epoll_wait是非频繁调用的,所以在这里插入的时候就实现用户态到内核态的拷贝,这确保了每一个fd在其生命周期只需要被拷贝一次。

        1. 在实现上,epoll采用红黑树来存储所有监听的fd,而红黑树本身插入和删除性能比较稳定(时间复杂度O(logN))。通过epoll_ctl函数添加进来的fd都会被放在红黑树的某个节点内;当fd添加进来的时候会完成关键的一步:该fd会和相应的设备驱动建立回调关系,会在这个fd状态改变时触发,使得该fd加入eventpoll的就绪列表rdlist。
        2. 当相应数据到来时,触发中断响应程序,将数据拷贝到fd的socket缓冲区,fd缓冲区状态发生变化,回调函数将fd对于的epitem加入rdlist就绪队列中。
      3. epoll_wait实际上就是去检查rdlist双向链表中是否有就绪的fd,有的话直接返回,否则(无就绪fd)阻塞等待或等待超时时间的到来。

      4. 大致工作原理如下:

    2. 优点:

      1. epoll是线程安全的
      2. 仅需要把就绪的fd从用户态拷贝到内核态,所以效率比较高;
      3. epoll会直接返回fd的就绪列表,提高数据处理的效率;
      4. epoll每秒处理请求的数量基本不会随着链接变多而下降的(轮询会下降)。
      5. 同时注意,epoll不是通过内核态和用户态共享内存来实现的,内核和用户进程通过mmap共享内存是一件极度危险的事情。

    参考文献:

    1. IO多路复用select/poll/epoll介绍(B站):https://www.bilibili.com/video/BV1qJ411w7du?from=search&seid=7583118679773390353; 视频笔记:https://www.processon.com/view/link/5f36856b5653bb06f2ce529f
    2. Go netpoller 原生网络模型之源码全面揭秘:https://mp.weixin.qq.com/s/3kqVry3uV6BeMei8WjGN4g;
    3. https://mp.weixin.qq.com/s?__biz=MzAxMTA4Njc0OQ==&mid=2651444736&idx=2&sn=262f63e85f9bc8edceca9b41c6ec730a&chksm=80bb08f2b7cc81e4ab1d87e78ffd5fa4e1407b40b493d0e411af1b0c9f7d3cc7180087ccc0c1&scene=132#wechat_redirect

    六、Linux常用命令

    1. ps查看进程

      1. ps命令(process status),查看系统中当前运行的进程
      2. ps -ef|grep python:表示查看所有进程里CMD是python的进程信息;
      3. ps -aux:显示所有进程和其状态,输出格式如下图
      4. ps -aux 和 ps -ef的区别:从输出内容来看,后者比前者少了cpu、mem的使用率。
    2. top 可以实时查看进程占用的资源,视图分为两部分:操作系统资源概况信息和进程信息

      1. 操作系统资源概况:包括系统当前的进程数、当前正在运行的进程数、当前睡眠中的进程数、CPU概览、内存概览;
      2. 进程占用资源信息:PID表示当前进程号;USER表示所属用户
      3. top -n 2:表示更新两次后终止更新显示
      4. top -d 3:表示更新周期为3s
      5. top -p 139:显示进程号为139的进程信息,CPU、内存占用率等
    3. free 查看系统内存,显示系统使用和空闲的内存情况,包括物理内存、交互区内存和内核缓冲区内存。free -h 将这些信息以便读的方式显示出来(即显示多少K、M、G)

    4. du 查看当前目录或文件占用的磁盘空间;

    5. df 查看系统磁盘

    6. 查看CPU占用率高的进程:top、ps -aux

    7. CPU使用率100%怎么排查?造成原因:从编程语言层次上,gc次数的增大或者死循环都有可能造成cpu负载增高。

      1. 先使用top命令,找到CPU占用最高的进程PID,假设为29099
      2. 然后查看某个进程内部线程CPU 占用情况, ps -mp (pid) -o THREAD,tid, time;找出CPU占用率最高的线程的TID,假如是29108
      3. jstack 29099 >> xxx.log,打印出该进程线程日志;(jstack用于打印出给定的java进程ID或core file或远程调试服务的java堆栈信息)
      4. 将2中查到的线程号tid 29108转换成16进制 ---- 71b4
      5. 进入下载好的xxx.log中,通过查找的方式找到对应线程,进行排查
    8. netstat:查看网络状态,用于显示与TCP、UDP协议相关的网络统计数据,一般用于检查本机各端口的网络连接情况。

      1. -a: 显示所有连线中的socket
      2. -l:显示为listening的socket
      3. -n:直接使用IP地址,而不通过域名服务器
      4. -t:显示TCP传输协议的连线状况
      5. -u:显示UDP的连线协议
      6. -p:显示正在使用socket的程序识别码和程序名称
      7. 列出所有的tcp、udp连接:netstat -tunlp
      8. 查看端口占用:netstat -anp |grep 8080 netstat -antp|grep ssh
      9. 统计80端口连接数:netstat -nat|grep -i "80"|wc -l,|grep有查找功能,|wc有统计的功能
    9. grep查找:可以与命令连起来用,也可以寻找字符串出现在文件中的部分或者行号:grep -n key filename

    10. lsof:(list open files)列出当前系统打开的文件。用于查看进程打开的文件。比如说某个文件删除不掉,想要查看被哪些进程占用,直接lsof filename;

    11. sed编辑文本文件:

      1. 大文件如何查看某一行的内容:sed -n '2p' temp.txt,仅仅显示第二行内容
      2. sed -n '1,3p' temp.txt 打印第1到3行内容
      3. A=$(sed -n '$=' a.txt) sed $(($A-10+1)), ${A}d a.txt: 删除文件最后十行
    12. 文件每行一个英文单词(单词可以重复),统计这个文件中出现次数最多的前10个单词。

    13. Linux下用vim打开一个超大文件是非常慢的,所以可以将其提前分割,然后再打开:

      1. 查看文件的前多少行:head -10000 slowquery.log > temp.log 把slowquery.log中的前10000行数据写入到tmp.log文件中
      2. 同理,查看文件的后多少行: tail -10000 slowquery.log > temp.log,把slowquery.log文件的后10000行数据写入到temp.log文件中
      3. 查看文件的几行到几行: sed -n '10, 10000p' slowquery.log > temp.log,把slowquery.log文件第10到10000行的数据写入到temp.log文件中。
      4. 根据查询条件导出:cat slowquery.log |grep '2021-04-13 16:54:52' > temp.log
      5. tail -f slowquery.log 实时监控文件输出

    七、常见面试题整理

    参考网址:https://www.nowcoder.com/tutorial/93/5268fb536d204ef3ad917d9d9ed92448;
    https://blog.csdn.net/zzxiaozhao/article/details/102990773

    1. 什么是DMA(直接内存访问)?什么是中断方式?有什么区别?

      1. 中断方式:当外设有需要传输的数据时,向CPU发出中断请求,CPU响应中断后进行数据传输。但如果传输较多的情况下,CPU会一直花费时间在数据传输上,造成CPU利用率低
      2. DMA方式:外设请求传输数据时,DMA向CPU发出总线控制请求,CPU把总线控制下发给DMA控制器。DMA利用总线进行数据的快速传输,传输完毕后把总线控制权还给CPU。优点:快、能大量传输数据而不降低CPU速度。
      3. 区别:DMA方式是硬件方式,中断方式是软件方式;DMA的优先级比中断方式高;DMA只占用CPU少部分时间,不浪费CPU资源,但是中断方式全程占用CPU;中断方式能处理异常事件,但是DMA只能处理传输数据。
        参考链接: https://blog.csdn.net/guomutian911/article/details/46291635;https://blog.csdn.net/qq_36187809/article/details/87857036
    2. 生产者消费者问题:很简单,就是生产者生产消息,消费者消费消息。但是需要注意的是,它们共享一个有限的缓冲区,问题的关键就是保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区空时消耗数据。在一个线程进行生产或消费时,其余线程不能再进行生产或消费等操作。

      // 使用单向通道来解决,对于生产者,定义一个只能写、不能读的通道,ch chan<-
      func producer(ch chan<- int) {
         for i := 0; i < 10; i ++ {
            ch <- i
         }
         close(ch)
      }
      // 对于消费者,定义一个只能读、不能写的通道,ch <-chan
      func consumer(ch <-chan int) {
         for num := range ch {
            fmt.Println(num)
         }
      }
      func main() {
         ch := make(chan int)
         // 新建一个goroutine,模拟生产者,生产数据,写入channel
         go producer(ch)
         // 主协程,消费消息
         consumer(ch)
      }
      
    3. 运行中的tcp服务器的系统发生大量缺页中断,可能的原因:一般运行中大量缺页,可能是mmap了大文件导致,或者内存不够了被频繁的换入换出。

    4. linux怎么查看缺页中断:通过命令ps -o majflt,minflt -c 程序名 或者pidstat。

  • 相关阅读:
    基础
    条件语句/变量和基本数据类型
    编程语言介绍
    asp.net中log4net使用方法
    web布到服务器上出错
    《转》IEnumerable、IEnumerator两个接口的认识
    异步ADO.NET
    Session的使用
    AJAX参数及各种HTTP状态值
    简易的抓取别人网站内容
  • 原文地址:https://www.cnblogs.com/xiaofeidu/p/14906253.html
Copyright © 2020-2023  润新知