【操作系统,进程,多线程】


1.内存的页面置换算法   

(1)最佳置换算法(OPT)(理想置换算法):从主存中移出永远不再需要的页面;如无这样的页面存在,则选择最长时间不需要访问的页面。于所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。

(2)先进先出置换算法(FIFO):是最简单的页面置换算法。这种算法的基本思想是:当需要淘汰一个页面时,总是选择驻留主存时间最长的页面进行淘汰,即先进入主存的页面先淘汰。其理由是:最早调入主存的页面不再被使用的可能性最大。

(3)最近最久未使用(LRU)算法:这种算法的基本思想是:利用局部性原理,根据一个作业在执行过程中过去的页面访问历史来推测未来的行为。它认为过去一段时间里不曾被访问过的页面,在最近的将来可能也不会再被访问。所以,这种算法的实质是:当需要淘汰一个页面时,总是选择在最近一段时间内最久不用的页面予以淘汰。 

(4)时钟(CLOCK)置换算法:LRU算法的性能接近于OPT,但是实现起来比较困难,且开销大;FIFO算法实现简单,但性能差。所以操作系统的设计者尝试了很多算法,试图用比较小的开销接近LRU的性能,这类算法都是CLOCK算法的变体。

操作系统之页面置换算法 - fkissx - 博客园

  2.进程调度算法   

1.先来先服务和短作业(进程)优先调度算法

(1)先来先服务调度算法

       按照进程变为就绪状态的先后次序,分派CPU;

  当前进程占用CPU,直到执行完或阻塞,才出让CPU(非抢占方式)。

  在进程唤醒后(如I/O完成),并不立即恢复执行,通常等到当前作业或进程出让CPU。

      适用场景

  比较有利于长作业,而不利于短作业。因为长作业会长时间占据处理机。

  有利于CPU繁忙的作业,而不利于I/O繁忙的作业。

(2)短进程优先调度算法

短进程优先调度算法SPF,它们可以分别用于作业调度和进程调度。短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程优先(SPF)调度算法则是从就绪队列中选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一直执行到完成,或发生某事件而被阻塞放弃处理机时再重新调度。主要的不足之处是长作业的运行得不到保证

2. 高优先权优先调度算法

(1)优先权调度算法的类型:当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程

(2)高响应比优先调度算法:为每个作业引入前面所述的动态优先权,并使作业的优先级随着等待时间的增加而以速率a 提高,则长作业在等待一定的时间后,必然有机会分配到处理机

3.基于时间片的轮转调度算法

(1)轮转(round robin。RR)调度算法:  在分时系统中,最简单最常用的是时间片的轮转调度算法,该算法采用了非常公平的处理机分配方式,即让就绪队列上的每个进程仅运行一个时间片。

(2)多级反馈队列调度算法:  

1)、进程在进入待调度的队列等待时,首先进入优先级最高的Q1等待。  

2)、首先调度优先级高的队列中的进程。若高优先级中队列中已没有调度的进程,则调度次优先级队列中的进程。例如:Q1,Q2,Q3三个队列,只有在Q1中没有进程等待时才去调度Q2,同理,只有Q1,Q2都为空时才会去调度Q3。

3)、对于同一个队列中的各个进程,按照时间片轮转法调度。比如Q1队列的时间片为N,那么Q1中的作业在经历了N个时间片后若还没有完成,则进入Q2队列等待,若Q2的时间片用完后作业还不能完成,一直进入下一级队列,直至完成。

4)、在低优先级的队列中的进程在运行时,又有新到达的作业,那么在运行完这个时间片后,CPU马上分配给新到达的作业(抢占式)。

 3. 进程间通信方式   

参考自己文章进程间通信与线程间通信 - 简书

进程的通信机制主要有:管道、有名管道、消息队列、信号、信号量、共享内存、套接字。

4. 进程,线程  

(1)进程是资源的分配和调度的一个独立单元,而线程是CPU调度的基本单元

(2)同一个进程中可以包括多个线程,并且线程共享整个进程的资源(寄存器、堆栈、上下文),一个进程至少包括一个线程。【说一下寄存器,每个线程都有它自己的一组CPU寄存器,称为线程的上下文寄存器的作用是暂存指令、数据和地址】

(3)进程的创建调用fork或者vfork,而线程的创建调用pthread_create,进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束

(4)线程是轻量级的进程,它的创建和销毁所需要的时间比进程小很多,所有操作系统中的执行功能都是创建线程去完成的

(5)线程中执行时一般都要进行同步和互斥,因为他们共享同一进程的所有资源

(6)线程有自己的私有属性TCB,线程id,寄存器、硬件上下文,而进程也有自己私有的进程控制块PCB,这些私有属性是不被共享的,用来标示一个进程或一个线程的标志      

PCB它记录了操作系统所需的、用于描述进程的当前状态和控制进程的全部信息。包括:

  (1)进程标识信息:用于唯一地标识一个进程,一个进程通常有两种标识符:内部标志符(由操作系统赋予每个进程的一个唯一的数字标识符)&  外部标识符(由创建者产生,是由字母和数字组成的字符串,为用户进程访问该进程提供方便。)

(2)处理机状态:  处理机状态信息主要由处理机的各个寄存器内的信息组成。进程运行时的许多信息均存放在处理机的各种寄存器中。其中程序状态字(PSW)是相当重要的,处理机根据程序状态寄存器中的PSW来控制程序的运行。

(3)进程调度信息:

      1)、进程状态。 标识进程的当前状态(就绪、运行、阻塞),作为进程调度的依据

      2)、 进程优先级。 表示进程获得处理机的优先程度。

       3)、为进程调度算法提供依据的其他信息。例如,进程等待时间、进程已经获得处理器的总时间和进程占用内存的时间等。

       4)、事件。 是指进程由某一状态转变为另一状态所等待发生的事件。

(4)进程控制信息

               1)、程序和数据地址。 是指组成进程的程序和数据所在内存或外存中的首地址,以便在调度该进程时能从其PCB中找到相应的程序和数据。 

               2)、进程同步和通信机制。 指实现进程同步和通信时所采取的机制,如消息队列指针和信号量等,他们可以全部或部分存在PCB中。

               3)、资源清单。 列出了进程所需的全部资源 已经分配给该进程的资源,但不包括CPU.        

               4)、链接指针。它给出了处于同一队列中的下一个PCB的首地址

总之,操作系统就是根据进程的PCB来感知进程的存在,并依此对进程进行管理和控制。 PCB是进程存在的唯一标识

进程和线程的应用场景:

1)需要频繁创建销毁的优先用线程 :   这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,如果要是用进程,创建和销毁的代价是很难承受的

2)需要进行大量计算的优先使用线程: 所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。这种原则最常见的是图像处理、算法处理。

3)可能要扩展到多机分布的用进程,多核分布的用线程

4)强相关的处理用线程,弱相关的处理用进程: 什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。

      进程上下文与线程上下文  :

      进程上下文:进程的物理实体与支持进程执行的物理环境合称为进程上下文。     

       线程上下文:每个线程都有它自己的一组CPU寄存器,称为线程的上下文

线程上下文切换和进程上下文切换:

线程上下文切换:对线程的上下文进行保存和恢复的过程就被称为上下文切换。

进程上下文切换:操作系统为了控制进程的执行,必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行,这种行为被称为进程上下文切换

两者最大的区别

Linux环境线程具体介绍:

线程创建及其属性特点
线程相关的一些函数 
设置线程分离实例


  5.父子进程、孤儿进程 、僵尸进程  

     父子进程: 通过fork函数创建的新进程是原进程的子进程,而调用fork函数的进程是fork函数创建出来的新进程的父进程。也就是说,通过fork函数创建的新进程与原进程是父子关系,fork就相当于一个凭证,有fork,就有父子关系。(父子进程永远共享的东西是(1)文件描述符 (2)内存映射区)

     孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。(为什么要让init进行领养呢?因为子进程自己运行结束后能够释放用户区空间,但是释放不了PCB,而PCB只有父进程才能释放,所以需要个养父。)

  僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid去回收子进程资源。这种进程称之为僵尸进程。

最简单的父进程回收子进程的方法: wait(NULL);

6.线程有哪几种状态?进程有哪几种状态?

多线程笔试面试概念问答 - CSDN博客

线程有四种状态:(1)新生状态、(2)可运行状态、(3)被阻塞状态、(4)死亡状态

进程有3种状态:就绪状态,运行状态,阻塞状态。

也可以说5种(创建,就绪,运行,阻塞,退出)

运行态:进程占用CPU,并在CPU上运行; 

就绪态:进程已经具备运行条件,但是CPU还没有分配过来; 

阻塞态:进程因等待某件事发生而暂时不能运行;

三种状态的进程

7. 线程同步和线程互斥的区别      

(1)线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。       

(2)线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步

8. 线程同步的方式

Linux:互斥锁、条件变量和信号量    Linux 线程同步的三种方法 - CSDN博客

(1)互斥锁(mutex): 通过锁机制实现线程间的同步。

  过程: 初始化锁(静态分配和动态分配)------>  加锁   ------->  解锁  -------->   销毁锁

初始化互斥锁,释放互斥锁
加互斥锁实例

(2)条件变量:用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。条件变量分为两部分: 条件和变量。条件本身是由互斥量保护的。主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。

生产者消费者模型中  条件变量+互斥锁  的结构
pthread_cond_wait(&cond,&metux)会对已经上锁的metux解锁;pthread_cond_signal(&cond)会对metux重新加锁

过程:初始化条件变量(静态初始化和动态初始化)------> 等待条件成立 ------->  激活条件变量  ------>  清除条件变量 

(3)  信号量(sem)

如同进程一样,线程也可以通过信号量来实现通信,虽然是轻量级的。信号量函数的名字都以"sem_"打头。线程使用的基本信号量函数有四个。

过程:  信号量初始化 ------>  等待信号量 (给信号量减1,然后等待直到信号量的值大于0) ------>释放信号量 (信号量值加1,并通知其他等待线程 ) ---------->销毁信号量 ( 我们用完信号量后都它进行清理, 归还占有的一切资源 )

9. fork进程时的操作

fork()是UNIX操作系统中的一个系统调用,用于创建新的进程。一个进程,包括代码、数据和分配给进程的资源。

fork()执行后通过复制原来进程的地址空间形成新的进程。而父进程和子进程都继续执行系统调用fork()之后的指令。

值得一提的是,fork()有两个返回值,如果是父进程,那么会返回为子进程的进程标识符(非零),而对于子进程,则会返回为0;   

fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。

fork系统调用图

fork()会产生一个和父进程几乎完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程内核只为新生的子进程创建复制于父进程的虚拟空间结构(就是那个4G空间结构),但是不会为该空间分配物理内存(包括堆,栈,代码空间等等),共享父进程的物理内存;在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间(就是那个4G的虚拟内存空间)不同,但其对应的物理空间(就是实际的物理内存,包括磁盘空间,内存等)是同一个。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。

fork时子进程获得父进程数据空间、堆和栈的复制,所以变量的地址(变量的虚拟地址)也是一样的,但是虚拟空间不一样。

子进程也有与父进程不同的属性:

(1) 进程号, 子进程号不同与任何一个活动的进程组号.

(2) 父进程号.

.(3)子进程继承父进程的文件描述符或流时, 具有自己的一个拷贝并且与父进程和其它子进程共享该资源.

(4)子进程的用户时间和系统时间被初始化为0.

(5). 子进程的超时时钟设置为0.

(6). 子进程的信号处理函数指针组置为空. (该处有问题。)

子进程继承父进程的处理函数(当一个进程调用fork时,因为子进程在开始时复制父进程的存储映像,信号捕捉函数的地址在子进程中是有意义的,所以子进程继承父进程的信号处理函数。

特殊的是exec,因为exec运行新的程序后会覆盖从父进程继承来的存储映像,那么信号捕捉函数在新程序中已无意义,所以exec会将原先设置为要捕捉的信号都更改为默认动作。)

(7). 子进程不继承父进程的记录锁.

exec()函数族   Linux进程 -- exec函数操作 - CSDN博客

(1)让父子进程执行不相关的操作

(2)能够替换进程地址空间中的源代码段

用fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用一个全新的程序替换了当前进程的正文、数据、堆和栈段(因为新的代码段会定义新的栈区,堆区变量)。

10.fork、vfork以及clone的区别 

A:从源码来分析,它们都用来创建linux轻量级进程的,vfork与fork的区别是,vfork共享父进程的地址空间,vfork之后父进程会让子进程先运行,因为vfork主要用于为了让子进程exec(运行),exec之后子进程会用新程序的数据将内存重新刷一遍,这样它就有了自己的地址空间。子进程exec之后,会向父进程发送信号,这个时候父进程就可以开始运行了,如果子进程修改了父进程地址空间的话,父进程唤醒的时候就会发现自己的数据被改了,完整性丢失,所以这是不安全的。clone的话呢,它提供选项,让你自己选择每次复制哪些东西,但是它调用的还是do_fork好像... vfork()系统调用:

  那么讲完了fork(),我们不妨和vfork()比较,并且学习总结一下vfork(.);   

vfork也是创建一个子进程,但是子进程共享父进程的空间在vfork创建子进程之后,父进程阻塞,直到子进程执行了exec()或者exit()。vfork最初是因为fork没有实现COW机制,而很多情况下fork之后会紧接着exec,而exec的执行相当于之前fork复制的空间全部变成了无用功,所以设计了vfork。而现在fork使用了COW机制,唯一的代价仅仅是复制父进程页表的代价,所以vfork不应该出现在新的代码之中。vfork创建出来的不是真正意义上的进程,而是一个线程,因为它缺少进程要素(4),独立的内存资源。

      vfork()在某些情况下,我们知道vfork()与fork()执行结果是一样的,  除了子进程会执行一次exec系统调用或者调用_exit(0)退出.   函数原型:pid_t vfork vfork(void), 具体返回值与其中fork()类似. 这个函数时是在没有实现写时赋值前提下,所以现在我们并不推荐使用vfork().

结论如下  1)vfork() 子进程与父进程共享数据段

                   2)vfork() 中是子进程先执行,父进程后执行.

               通常我们都是与exec函数在一起,在主进程中替换进程印象.

 clone是Linux为创建线程设计的(虽然也可以用clone创建进程)。所以可以说clone是fork的升级版本,不仅可以创建进程或者线程,还可以指定创建新的命名空间(namespace)、有选择的继承父进程的内存、甚至可以将创建出来的进程变成父进程的兄弟进程等等。clone函数功能强大,带了众多参数,因此由他创建的进程要比前面2种方法要复杂。

clone可以让你有选择性的继承父进程的资源,你可以选择像vfork一样和父进程共享一个虚内存空间,从而使创造的是线程,你也可以不和父进程共享,你甚至可以选择创造出来的进程和父进程不再是父子关系,而是兄弟关系。

clone和fork的区别:

     (1) clone和fork的调用方式很不相同,clone调用需要传入一个函数,该函数在子进程中执行。

      (2)clone和fork最大不同在于clone不再复制父进程的栈空间,而是自己创建一个新的。(void *child_stack,)也就是第二个参数,需要分配栈指针的空间大小,所以它不再是继承或者复制,而是全新的创造。

11.用户态与内核态的切换与区别

4G地址空间解析图

(1)内核态和用户态的区别

       当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核状态。此时处理器处于特权级最高的(0级)内核代码。当进程处于内核态时,执行的内核代码会使用当前的内核栈。每个进程都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户态。即此时处理器在特权级最低的用户代码中运行。

       当正在执行用户程序而突然中断时,此时用户程序也可以象征性地处于进程的内核态。因为中断处理程序将使用当前进程的内核态 内核态与用户态是操作系统的两种运行级别,跟intel cpu没有必然联系,intel cpu提供Ring0-Ring3三种级别运行模式,Ring0级别最高,Ring3级别最低。Linux使用了Ring3级别运行用户态。Ring0作为内核态,没有使用Ring1和Ring2.Ring3不能访问Ring0的地址空间,包括代码和数量。Linux进程的4GB空间,3G-4G部分大家是共享的,是内核态的地址空间,这里存放在整个内核代码和所有的内核模块,以及内核所维护的数据。用户运行一程序,该程序所创建的进程开始是运行在用户态的,如果要执行文件操作,网络数据发送等操作,必须通过write,send等系统调用,这些系统会调用内核中的代码来完成操作,这时,必须切换到Ring0,然后进入3GB-4GB中的内核地址空间去执行这些代码完成操作,完成后,切换Ring3,回到用户态。这样,用户态的程序就不能随意操作1内核地址空间,具有一定的安全保护作用。

(2)用户态和内核态的转换

用户态切换到内核态的3种方式

a.系统调用

这是用户进程主动要求切换到内核态的一种方式,用户进程通过系统调用申请操作系统提供的服务程序完成工作。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的ine 80h中断。

b.异常

当CPU在执行运行到用户态的程序时,发现了某些事件不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就到了内核态,比如缺页异常。

c.外围设备的中断

当外围设备完成用户请求的操作之后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条将要执行的指令转而去执行中断信号的处理程序,如果先执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

12.内部碎片和外部碎片

内部碎片:内部碎片就是已经被分配出去(能明确指出属于哪个进程)却不能被利用的内存空间;是处于(操作系统分配的用于装载某一进程的内存)区域内部的存储块。占有这些区域或页面的进程并不使用这个存储块。而在进程占有这块存储块时,系统无法利用它。直到进程释放它,或进程结束时,系统才有可能利用这个存储块

外部碎片:外部碎片指的是还没有被分配出去(不属于任何进程),但由于太小了无法分配给申请内存空间的新进程的内存空闲区域。外部碎片是处于任何两个已分配区域或页面之间的空闲存储块。这些存储块的总和可以满足当前申请的长度要求,但是由于它们的地址不连续或其他原因,使得系统无法满足当前申请。如图:

假设这是一段连续的页框,阴影部分表示已经被使用的页框,现在需要申请一个连续的5个页框。这个时候,在这段内存上不能找到连续的5个空闲的页框,就会去另一段内存上去寻找5个连续的页框,这样子,久而久之就形成了页框的浪费。称为外部碎片。

13. slab分配机制

        slab分配器中用到了对象这个概念,所谓  对象就是内核中的数据结构  以及   对该数据结构进行创建和撤销的操作。它的基本思想是将内核中经常使用的对象放到高速缓存中,并且由系统保持为初始的可利用状态。比如进程描述符,内核中会频繁对此数据进行申请和释放。当一个新进程创建时,内核会直接从slab分配器的高速缓存中获取一个已经初始化了的对象;当进程结束时,该结构所占的页框并不被释放,而是重新返回slab分配器中。如果没有基于对象的slab分 配器,内核将花费更多的时间去分配、初始化以及释放一个对象。

       slab分配器为每种对象分配一个高速缓存,这个缓存可以看做是同类型对象的一种储备。每个高速缓存所占的内存区又被划分多个slab,每个 slab是由一个或多个连续的页框组成。每个页框中包含若干个对象,既有已经分配的对象,也包含空闲的对象。slab分配器的大致组成图如下:

slab分配器的结构

14. PCB是什么呢?PID是什么?

进程控制块(PCB):

        每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息。其作用是使一个在多道程序环境下不能独立运行的程序成为一个能独立运行的基本单位或其他进程并发执行的进程。PCB是系统感知进程存在的唯一标识。Linux内核的进程控制块是task_struct结构体。task_struct是Linux内核的一种数据结构。它会被装载到RAM里并且包含着进程的信息。它定义在linux-2.6.38.8、include/linux/sched.h文件中。task_struct结构体包含了以下内容:

优先级:相对于其他进程的优先级。   程序计数器: 下一句要执行的指令地址。

内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。

上下文数据:进程执行时处理器的寄存器中的数据。上下文数据,也称处理器相关的环境信息。进程作为一个执行环境的综合,当系统调度某个进程执行,即为该进程建立完整的环境时,处理器的寄存器、堆栈等是必不可少的。因为不同的处理器对内部寄存器和堆栈的定义是不尽相同的,所以也叫“处理机状态”。当进程暂停时,处理机状态必须保存到进程的task_struct结构中,当进程被调度重新运行时再从中恢复这些环境,也就是恢复这些寄存器和堆栈的值。

struct thread_struct *tss;    //任务切换状态

I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。

记账信息:又称会计信息。如CPU与实际时间,使用数量、时限、账号、工作或进程号码。

进程标识符(PID)

        每个进程都有进程标识符,用户标识符,组标识符。内核通过这个标识符来识别不同的进程,同时,PID也是内核提供给用户程序的接口,用户通过PID对进程发号施令。PID是32位的无符号整数,他被顺序编号:新创建进程的PID通常是前一个进程的PID加1,然而,为了与16位硬件平台的传统Linux系统保持兼容,在Linux上允许的最大PID是32767,当内核在系统中创建第32768个进程时,就必须重新开始使用已闲置的PID。即最多可运行32767个进程。

15.内存分配

C++内存管理(超长,例子很详细,排版很好) - CSDN博客

生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

16.页和段的区别,哪些对程序员可见?(进一步了解一下分页和分段)

详细看     段和页区别 - CSDN博客      操作系统之分页分段介绍 - CSDN博客

区别:(1)页是信息的物理单位,分页是为实现离散分配方式,以消减内存的外零头,提高内存的利用率;或者说,分页仅仅是由于系统管理的需要,而不是用户的需要。段是信息的逻辑单位,它含有一组其意义相对完整的信息。分段的目的是为了能更好的满足用户的需要。

          (2)页的大小固定且由系统确定,把逻辑地址划分为页号和页内地址两部分,是由机器硬件实现的,因而一个系统只能有一种大小的页面。 段的长度却不固定,决定于用户所编写的程序,通常由编辑程序在对源程序进行编辑时,根据信息的性质来划分。

         (3)分页的作业地址空间是维一的,即单一的线性空间,程序员只须利用一个记忆符,即可表示一地址。 分段的作业地址空间是二维的,程序员在标识一个地址时,既需给出段名,又需给出段内地址。

分页:操作系统以内存页为单位管理内存,将虚拟内存空间和物理内存空间皆划分为大小相同的页面,如4KB、8KB或16KB等,并以页面作为内存空间的最小分配单位,一个程序的一个页面可以存放在任意一个物理页面里。

   分段通常对程序员而言是可见的, 而分页对程序员而言是不可见的


17.虚拟内存(也叫虚拟存储器)计算机底层知识拾遗(一)理解虚拟内存机制 - CSDN博客

虚拟内存(虚拟地址, 页表,换页...) - joannae - 博客园   (虚拟地址转换物理地址比较详细)

虚拟内存:是计算机系统内存管理的一种技术,它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。对虚拟内存的定义是基于对地址空间的重定义的,即把地址空间定义为“连续的虚拟内存地址”,以借此“欺骗”程序,使它们以为自己正在使用一大块的“连续”地址。

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

虚拟内存和物理内存以及磁盘的映射关系

从图中看出,虚拟内存实际上可以比物理内存大。当访问虚拟内存时,会访问MMU(内存管理单元)去匹配对应的物理地址(比如图5的0,1,2),而如果虚拟内存的页并不存在于物理内存中(如图的3,4),会产生缺页中断,从磁盘中取得缺的页放入内存,如果内存已满,还会根据某种算法将磁盘中的页换出。

MMU:内存管理单元,它是中央处理器(CPU)中用来管理虚拟存储器、物理存储器的控制线路,同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权,多用户多进程操作系统。

CPU把虚拟地址转换成物理地址:一个虚拟地址,大小4个字节(32bit),分为3个部分:第22位到第31位这10位(最高10位)是页目录中的索引,第12位到第21位这10位是页表中的索引,第0位到第11位这12位(低12位)是页内偏移。一个一级页表有1024项,虚拟地址最高的10bit刚好可以索引1024项(2的10次方等于1024)。一个二级页表也有1024项,虚拟地址中间部分的10bit,刚好索引1024项。虚拟地址最低的12bit(2的12次方等于4096),作为页内偏移,刚好可以索引4KB,也就是一个物理页中的每个字节。

虚拟内存技术的实现 

虚拟内存中,允许将一个作业分多次调入内存。釆用连续分配方式时,会使相当一部分内存空间都处于暂时或“永久”的空闲状态,造成内存资源的严重浪费,而且也无法从逻辑上扩大内存容量。因此,虚拟内存的实需要建立在离散分配的内存管理方式的基础上虚拟内存的实现有以下三种方式: 

1)请求分页存储管理。  2)请求分段存储管理。  3)请求段页式存储管理。 

不管哪种方式,都需要有一定的硬件支持。一般需要的支持有以下几个方面: 

1)一定容量的内存和外存。 

2)页表机制(或段表机制),作为主要的数据结构。 

3)中断机制,当用户程序要访问的部分尚未调入内存,则产生中断。 

4)地址变换机制,逻辑地址到物理地址的变换。

逻辑地址,物理地址,虚拟地址

逻辑地址空间:逻辑地址空间是指一个源程序在编译或者连接装配后指令和数据所用的所有相对地址的空间。它是作业进入内存,其程序、数据在内存中定位的参数程序经过编译后,每个目标模块都是从0号单元开始编址,称为该目标模块的逻辑地址(也叫相对地址)

物理地址空间:物理地址空间是指内存中物理单元的集合,它是地址转换的最终地址,进程在运行时执行指令和访问数据都要通过物理地址从主存中存取。

虚拟地址: 4G虚拟地址空间中的地址,程序中使用的都是虚拟地址

虚拟寻址 (虚拟地址和物理地址之间转换)

使用虚拟寻址,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址在被送到内存之前会被内存管理单元(MMU)转换成合适的物理地址,从而找到目标地址。具体过程如下图

虚拟内存工作机制:

1).在正常使用的时候,系统内部的交换(缓存)文件通常保存在虚拟内存中;

2).自动把非活动的系统进程或者程序映射到虚拟空间;

3).当物理内存低于25%左右的时候,则把虚拟内存和物理内存合并,也就是说系统此时会把你的虚拟内存也识别成物理内存。

当虚拟内存设置的比物理内存还要大的时候,会导致系统提前使用你的虚拟空间,这会使你感觉的系统的速度下降,同时硬盘负担大大加重。(因为cpu的访问顺序是高速缓存到内存再到硬盘(虚拟内存实际就是硬盘存储空间),所以注定硬盘传输的速度比内存延迟,而且由于内部运行机制的不同,内存的内部运行速度远远快于硬盘)

虚拟内存的好处:

第一,使用虚拟地址可以更加高效的使用物理内存。在计算机系统中物理内存是有限的,对于一般的计算机来说,物理内存一般为4G或者8G. 对于现代多任务的通用系统来说这显然是不够的。试想假如我们需要看一个4G大小以上的视频,这时物理内存就不够了。在存在虚拟地址的情况下,可以为每个程序分配足够大小的虚拟地址空间,但是这些地址空间在一开始并没有真正的对应物理地址,而是在真正使用的时候才去对应。这样在访问后边的地址空间的时候就可以将前边当前没有在访问的物理页释放掉,或者交换到硬盘中。这样这个物理页又可以去对应新的虚拟地址。从而使物理内存可以充分的利用。

第二,使用虚拟地址可以使内存的管理更加便捷。在程序编译的时候就会生成虚拟地址,对于不同的机器或者对于同一台机器的不同时间,该虚拟地址可以对应不同的物理页。试想,如果没有虚拟地址,那么编译时产生的物理地址在某些机器上可能已经被占用而不能访问。从而导致程序需要重新编译。

第三,为了安全性的考虑。在使用虚拟地址的时候,暴露给程序员永远都是虚拟地址,而具体的物理地址在哪里,这个只有系统才了解。这样就提高了系统的封装性。

第四虚拟内存让每个进程有独立的地址空间,对于私有区域来说,当不同进程对该区域做修改时,会触发写时拷贝,为新进程维护私有的虚拟地址空间。

18. Linux父进程怎么知道子进程结束了

当子进程退出的时候,内核会向父进程发送SIGCHLD(sigchld)信号,子进程的退出是个异步事件(子进程可以在父进程运行的任何时刻终止)

如果不想让子进程变成僵尸进程可在父进程中加入:signal(SIGCHLD,SIG_IGN);

如果将此信号的处理方式设为忽略,可让内核把僵尸子进程转交给init进程去处理,省去了大量僵尸进程占用系统资源。

19. 守护进程及其创建过程

     守护进程(Daemon):是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。           

Linux系统的大多数服务器就是通过守护进程实现的。常见的守护进程包括系统日志进程syslogd web服务器httpd邮件服务器sendmail数据库服务器mysqld等。

守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。

Linux下完全可以利用 daemon() 函数创建守护进程,其函数原型

函数daemon接收两个参数:

  nochdir:  如果是0,将当前工作目录切换到根目录"/",否则工作目录不改变。

  noclose:  如果是0,将0(标准输入),1(标准输出),2(标准错误)重定向到/dev/null,否则不变。

守护进程的创建过程:【Linux编程】守护进程(daemon)详解与创建 - CSDN博客


(1)设置工作目录(不是必须),重设文件权限掩码(不是必须),关闭文件描述符(不是必须),其他的步骤都是必须的;(2)第一次创建子进程让父进程退出时为了让子进程创建新会话(因为当进程是会话组长时setsid()调用失败,父进程可能是会话组长,子进程则一定不是),第二次父进程退出时为了让子进程不能重新打开控制终端;

(1)fork()创建子进程,父进程exit()退出

这是创建守护进程的第一步。由于守护进程是脱离控制终端的,因此,完成第一步后就会在Shell终端里造成程序已经运行完毕的假象。之后的所有工作都在子进程中完成,而用户在Shell终端里则可以执行其他命令,从而在形式上做到了与控制终端的脱离,在后台工作。

(2)在子进程中调用 setsid() 函数创建新的会话

在调用了 fork() 函数后,子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端等并没有改变,因此,这还不是真正意义上的独立开来,而 setsid() 函数能够使进程完全独立出来。

(3)再次 fork() 一个子进程并让父进程退出。

现在,进程已经成为无终端的会话组长,但它可以重新申请打开一个控制终端,可以通过 fork() 一个子进程,该子进程不是会话首进程,该进程将不能重新打开控制终端。退出父进程。

(4)在子进程中调用 chdir() 函数,让根目录 ”/” 成为子进程的工作目录

这一步也是必要的步骤。使用fork创建的子进程继承了父进程的当前工作目录。由于在进程运行中,当前目录所在的文件系统(如“/mnt/usb”)是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入单用户模式)。因此,通常的做法是让"/"作为守护进程的当前工作目录,这样就可以避免上述的问题,当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp。改变工作目录的常见函数是chdir。

(5)在子进程中调用 umask() 函数,设置进程的文件权限掩码为0   Linux下的权限掩码umask - Mr_listening - 博客园

文件权限掩码是指屏蔽掉文件权限中的对应位。比如,有个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。由于使用fork函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。因此,把文件权限掩码设置为0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是umask。在这里,通常的使用方法为umask(0)。

语法格式::: umask 预设掩码数值   使用说明:::建立文件和文件夹的时候预设的掩码权限(你将要去掉的权限数值表达)   主要参数:::  -S 以文字的方式来表示权限掩码   应用实例:::  (1)设要生成的文件以rw- r-- r--这样的权限字出现  umask 133 (相当于777-644=133)  (2)设要生成的目录权限以rwxr-xr-x这样的权限字出现  umask 022 (相当于777-055=022)      (r、w、x分别是4、2、1)  目录基数是777,文件基数是666   

umask 后面跟的是被拿走的权限值

(6)在子进程中关闭任何不需要的文件描述符

同文件权限码一样,用fork函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。在上面的第二步之后,守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如printf)输出的字符也不可能在终端上显示出来。所以,文件描述符为0、1和2 的3个文件(常说的输入、输出和报错)已经失去了存在的价值,也应被关闭。

(7)守护进程退出处理

当用户需要外部停止守护进程运行时,往往会使用 kill 命令停止该守护进程。所以,守护进程中需要编码来实现 kill 发出的signal信号处理,达到进程的正常退出。

20.  (Java内容)什么是序列化, IO的序列化方式, 为什么需要序列化(包括在网络传输的情况下

        简单来说序列化就是一种用来处理对象流的机制。所谓对象流也就是将对象的

内容进行流化,流的概念这里不用多说(就是I/O)。我们可以对流化后的对象进行读写

操作,也可将流化后的对象传输于网络之间(注:要想将对象传输于网络必须进行流化)!

在对对象流进行读写操作时会引发一些问题,而序列化机制正是用来解决这些问题的!

21. 单核CPU执行多线程的实现机制

1、锁--------在单核上,多个线程执行锁或者临界区时,实际上只有一个线程在执行临界区代码,而核心也只支持一个线程执行,因此不存在冲突。如果某个线程持有锁,那其他线程不会被调度到CPU上执行,影响的只是持有和释放锁的时间,处理器时刻在运行着。但是在多核上运行时,锁或临界区会导致其余处理器空闲而只允许一个处理器执行持有锁的那个线程,这是一个串行的过程,会影响性能。

2、负载均衡--------单核上不用考虑负载均衡,因为各个线程轮流执行,当一个线程执行完时,则会执行另外一个线程,不存在线程等待问题。即使各个线程的任务非常不均衡,也不会影响总执行时间。而在多核上执行时,此时最终时间由运行时间最长的线程决定;

3、任务调度-------单核上,任务调度完全是操作系统的工作,无需软件开发人员干预,通常有时间片轮转、优先级算法等。而在多核上运行时,软件开发人员要合理地在核心间分配任务,以尽量同时结束计算(操作系统转向软件开发人员)

4、程序终止--------多线程环境下,程序终止时需要确定各个线程都已经计算完成。

22.  Nginx介绍

什么是Nginx-----------Nginx是一个http服务器。是一个使用c语言开发的高性能的http服务器及反向代理服务器。Nginx是一款高性能的http 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。由俄罗斯的程序设计师Igor Sysoev所开发,官方测试nginx能够支支撑5万并发链接,并且cpu、内存等资源消耗却非常低,运行非常稳定。

Nginx之(一)Nginx是什么 - CSDN博客

Nginx内置的负载均衡策略有3种轮询加权轮询IP hash  。同时支持扩展策略,完全可以自己写一套规则交给Nginx去执行。

23.怎么查看是否出现了内存泄漏

c++内存泄露检测 - CSDN博客

用Windows的任务管理器(Task Manager)。运行程序,然后在任务管理器里面查看 “内存使用”和”虚拟内存大小”两项,当程序请求了它所需要的内存之后,如果虚拟内存还是持续的增长的话,就说明了这个程序有内存泄漏问题。

24.多线程的程序如果出现了死锁怎么去调试

多线程死锁调试小技巧 - KingsLanding - 博客园

25. 实现线程安全的方式有哪些?并介绍一下实现? 

基本思路:

(1)给共享的资源加把锁,保证每个资源变量每时每刻至多被一个线程占用。

(2)让线程也拥有自己的资源,不用去共享进程中的资源。

具体实现:

(1). 多实例、或者是多副本(ThreadLocal):可以为每个线程的维护一个私有的本地变量;

(2). 使用锁机制 synchronize、lock方式:为资源加锁

(3). 使用 java.util.concurrent 下面的类库:有JDK提供的线程安全的集合类


26. 了解线程池不?线程池的基本参数有哪些?

线程池----------------把一堆开辟好的线程放在一个池子里统一管理,就是一个线程池。线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理。

27. 线程池是解决什么问题的?

如果每个请求都创建一个线程去处理,那么服务器的资源很快就会被耗尽,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

28. 读写锁特性

29. 系统调用或者说中断的过程  ( 软中断, 硬中断 ) 

30.自旋锁   自旋锁与互斥锁的对比、手工实现自旋锁 - CSDN博客

采用让当前线程不停的在循环体内执行实现,当循环的条件被其它线程改变时才能进入临界区

优点:由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。

缺点:但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。

自旋锁是一种用于保护多线程共享资源的锁,与一般互斥锁(mutex)不同之处在于当自旋锁尝试获取锁时以忙等待(busy waiting)的形式不断地循环检查锁是否可用。在多CPU的环境中,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。

31. CPU是怎么执行指令的

cpu执行指令的过程详解

  计算机每执行一条指令都可分为三个阶段进行。即   

        取指令-----分析指令-----执行指令

  (1)取指令:根据程序计数器PC中的值从程序存储器读出现行指令,送到指令寄存器。

  (2)分析指令:将指令寄存器中的指令操作码取出后进行译码,分析其指令性质。如指令要求操作数,则寻找操作数地址。

  (3)执行指令:计算机执行程序的过程实际上就是逐条指令地重复上述操作过程,直至遇到停机指令可循环等待指令。

32. 互斥量(锁),临界区,事件,信号量的区别

(1)互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部使用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。 (锁或者说互斥量可以不仅可以在统一进程的不同线程中使用,还可以跨进程对不同进程使用,而临界区只能在进程内部针对不同线程使用)

(2)互斥量(Mutex),信号灯(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以可以使用WaitForSingleObject来等待进程和线程退出。

(3)通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号灯对象可以说是一种资源计数器。

推荐阅读更多精彩内容