操作系统

操作系统

  1. 进程间通信的方法都有什么

    • 信号量(semophore ) :

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

    • 消息队列( messagequeue ) :

    消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

    • 信号 (sinal ) :

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

    • 共享内存(shared memory ) :

    共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。

    • 管道( pipe ):

    管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

    • 套接字(socket ) :

    套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。

  2. 线程间通讯方式

    • 互斥量:(锁)

    互斥量有两种状态--解锁和加锁。当一个线程(或进程)需要访问临界区时,它调用互斥锁。如果该互斥量当前是解锁的(即临界区可用),此调用成功,调用线程可以自由进入该临界区。另一方面,如果该互斥量已经加锁,调用线程被阻塞,直到在临界区中的线程完成并调用互斥锁。如果多个线程被阻塞在该互斥量上,将随机选择一个线程并允许它获得锁。

    • 信号量:(OnlyChild)

    它允许同一时刻多个线程访问同一资源,但是需要一个计数器来控制可以使用某共享资源的线程数目。

    • 事件(信号):(notify/notifyAll)

    通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。

  3. 操作系统的竞态?自旋锁说一下

    并发的执行单元对共享资源(硬件资源和软件上的全局变量,静态变量等)的访问容易发生竞态

出现以下四种情况会产生死锁
1,相互排斥。一个线程或进程永远占有共享资源,比如,独占该资源。
2,循环等待。例如,进程A在等待进程B,进程B在等待进程C,而进程C又在等待进程A。
3,部分分配。资源被部分分配,例如,进程A和B都需要访问一个文件,同时需要用到打印机,进程A得到了这个文件资源,进程B得到了打印机资源,但两个进程都不能获得全部的资源了。
4,缺少优先权。一个进程获得了该资源但是一直不释放该资源,即使该进程处于阻塞状态。

竞态的解决办法

解决竞态问题的途径是保证对共享资源的互斥访问。访问共享资源的代码区域称为临界区,临界区要互斥机制保护。Linux设备驱动中常见的互斥机制有以下方式:中断屏蔽、原子操作、自旋锁和信号量等。

自旋锁

基本概念: 自旋锁是一种对临界资源进行互斥访问的手段。

工作原理:

为获得自旋锁,在某CPU上运行的代码需先执行一个原子操作,该操作测试并设置某个内存变量,由于其为原子操作,所以在该操作完成之前其他执行单元不可能访问这个内存变量,如果测试结果表明已经空闲,则程序获得这个自旋锁并继续执行,如果测试结果表明该锁仍被占用,程序将在一个小的循环内重复这个“测试并设置”操作,即进行所谓的“自旋”,通俗的说就是在“原地打转”。

互斥锁和自旋锁、信号量的区别?

  • 互斥锁和互斥量

在我的理解里没啥区别,不同叫法。广义上讲可以值所有实现互斥作用的同步机制。狭义上讲指的就是mutex这种特定的二元锁机制。互斥锁的作用就是互斥,mutual exclusive,是用来保护临界区(critical section)的 。所谓临界区就是代码的一个区间,如果两个线程同时执行就有可能出问题,所以需要互斥锁来保护。

  • 信号量(semaphore)

是一种更高级的同步机制,mutex(互斥锁) 可以说是 semaphore(信号量) 在仅取值0/1时的特例。Semaphore可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。

  • 自旋锁

是一种 互斥锁 的实现方式而已,相比一般的互斥锁会在等待期间放弃cpu,自旋锁(spinlock) 则是不断循环并测试锁的状态,这样就一直占着cpu。所以相比于自旋锁和信号量,在申请锁失败的话,自旋锁会不断的查询,申请线程不会进入休眠,信号量和互斥锁如果申请锁失败的话线程进入休眠,如果申请锁被释放后会唤醒休眠的线程。

  • 同步锁

好像没啥特殊说法,你可以理解为能实现同步作用的都可以叫同步锁,比如信号量。

  1. 服务调度中心的感知与动态上下线?

    • 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待 CPU 的进程运行。

    • 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行。

    • 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度。

    • 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行

    • 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序。

  2. 线程的上下文有哪些东西?

    • 前后两个线程属于不同进程。此时,因为资源不共享,所以切换过程就跟进程上下文切换是一样。
    • 前后两个线程属于同一个进程。此时,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。
  3. PHP数组的底层实现

    在 PHP 中,这种映射关系是使用散列表(HashTable)实现的,在 C 语言中,只能通过数字下标访问数组元素,而通过 HashTable,我们可以使用 String Key 作为下标来访问数组元素。简单地说,HashTable 通过映射函数将一个 Strring Key 转化为一个普通的数字下标,并将对应的 Value 值储存到下标对应的数组元素中。

    PHP 中的 HashTable 由 zend_array 定义,它的数据结构如下:

struct _zend_array {
    zend_refcounted_h gc;
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    flags,
                zend_uchar    nApplyCount,
                zend_uchar    nIteratorsCount,
                zend_uchar    reserve)
        } v;
        uint32_t flags;           /* 通过 32 个可用标识,设置散列表的属性 */
    } u;
    uint32_t     nTableMask;       /* 值为 nTableSize 的负数 */
    Bucket      *arData;           /* 用来储存数据 */
    uint32_t     nNumUsed;         /* arData 中的已用空间大小 */
    uint32_t     nNumOfElements;   /* 数组中的元素个数 */
    uint32_t     nTableSize;       /* 数组大小,总是 2 幂次方 */
    uint32_t     nInternalPointer; /* 下一个数据元素的指针,用于迭代(foreach) */
    zend_long    nNextFreeElement; /* 下一个可用的数值索引 */
    dtor_func_t  pDestructor;      /* 数据析构函数(句柄) */
};

该结构中的 Bucket 即储存元素的数组,arData 指向数组的起始位置,使用映射函数对 key 值进行映射后可以得到偏移值,通过内存起始位置 + 偏移值即可在散列表中进行寻址操作。Bucket 的数据结构如下:

typedef struct _Bucket {
    zval              val; /* 值 */
    zend_ulong        h;   /* 使用 time 33 算法对 key 进行计算后得到的哈希值(或为数字索引)   */
    zend_string      *key; /* 当 key 值为字符串时,指向该字符串对应的 zend_string(使用数字索引时该值为 NULL) */
} Bucket;

基本实现

散列表主要由储存元素的数组(Bucket)和散列函数两部分构成。

  • 随机读
    当指定一个 Key-Value 映射关系时,如果 Key 为 String 类型,则先通过 Time 33 算法将其转换为一个 Int 类型的整数,然后再先通过 PHP 中某种特定的散列算法将该 Int 映射为 Bucket 数组中的一个下标,最终将 Value 储存到该下标对应的元素中。
    通过 Key 访问数组时,只需要使用相同的算法计算出对应下标,然后取出对应元素中的 Value 值,即可实现随机读取。

    img

  • 顺序读
    由上面所讲可知,储存在 HashTable 中的元素是无序的,而 PHP 中的数组是有序的,PHP 是如何解决这个问题的呢?
    为了实现 HashTable 的有序性,PHP 为其增加了一张中间映射表,该表是一个大小与 Bucket 相同的数组,数组中储存整形数据,用于保存元素实际储存的 Value 在 Bucekt 中的下标。注意,加入了中间映射表后,Bucekt 中的数据是有序的,而中间映射表中的数据是无序的。这样顺序读取时只需要访问 Bucket 中的数据即可。

    img

    zend_array 中并没有单独定义中间映射表,而是将其与 arData 放在一起,数组初始化时并不只分配 Bucket 大小的内存,同时还会分配相同大小空间的数据来作为中间映射表,其实现方式如图:

    img

  1. PHP的垃圾回收机制

    根缓冲机制,即php启动时默认设置指定zval数量的根缓冲区(默认是10000),当php发现有存在循环引用的zval时,就会把其投入到根缓冲区,当根缓冲区达到配置文件中的指定数量(默认是10000)后,就会进行垃圾回收,以此解决循环引用导致的内存泄漏问题。

  2. 守护进程是什么,怎么实现。

    1. 创建子进程,父进程退出
      编写守护进程第一步,就是要使得进程独立于终端后台运行。为避免终端挂起,将父进程退出,造成程序已经退出的假象,而后面的工作都在子进程完成,这样控制终端也可以继续执行其他命令,从而在形式上脱离控制终端的控制。

    2. 子进程创建新会话
      经过上一步,子进程已经后台运行,然而系统调用 fork 创建子进程,子进程便复制了原父进程的进程控制块(PCB),相应地继承了一些信息,包括会话、进程组、控制终端等信息。尽管父进程已经退出,但子进程的会话、进程组、控制终端的信息没有改变。为使子进程完全摆脱父进程的环境,需要调用 setsid 函数。

  1. 改变当前目录
    使用fork创建的子进程继承了父进程的当前工作目录。由于在进程运行中,当前目录所在的文件系统(如“/mnt/usb”)是不能卸载的,这对以后的使用会造成诸多的麻烦(比如系统由于某种原因要进入单用户模式)。因此通常的做法是让根目录("/")作为守护进程的当前工作目录,这样就可以避免上述的问题。

  2. 重设文件权限掩码
    fork 函数创建的子进程,继承了父进程的文件操作权限,为防止对以后使用文件带来问题,需要重设文件权限掩码。

  3. 关闭文件描述符
    同文件权限掩码一样,子进程可能继承了父进程打开的文件,而这些文件可能永远不会被用到,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下,因此需要一一关闭它们。由于守护进程脱离了终端运行,因此标准输入、标准输出、标准错误输出这3个文件描述符也要关闭。

  4. 进程和线程

    img

    1. 进程构成

      • 线程:每个进程由多个线程组成
      • 逻辑内存:每个进程的内存都是相互独立的
      • 文件/网络句柄:所有进程共有
    2. 线程构成

      • PC(process counter):下一条执行指令(指向内存),进程只是容器
      • TLS(Thread Local Storage):线程独立内存,用来将数据与一个正在执行的指定线程关联起来
    3. 交互

      • 进程间交互使用TCP/IP
      • 线程有共享内存,通过指针访问
  5. 协程的优势,协程更轻体现在哪里

    • 协程是进程和线程的升级版,进程和线程都面临着内核态和用户态的切换问题而耗费许多切换时间, 而协程就是用户自己控制切换的时机,不再需要陷入系统的内核态。

      线程是抢占式执行,当发生系统调用或者中断的时候,交由OS调度执行;而协程是通过yield主动让出cpu所有权,切换到其他协程执行。

    • 协程的执行效率非常高。因为子程序切换不是线程切换,而是由程序自身控制。因此,没有线程切换的开销,和多线程相比,线程数量越多,相同数量的协程体现出的优势越明显。

    • 不需要多线程的锁机制。由于只有一个线程,也不存在同时写变量的冲突,在协程中控制共享资源不需要加锁,只需要判断数据的状态,所以执行效率远高于线程 ,对于多核CPU可以使用多进程+协程来尽可能高效率地利用CPU。

  6. 线程和协程的堆栈的异同

    假设有2个线程运行在一个处理器上,从运行一个线程(T1)切换到另一个线程(T2)时,一定会发生上下文切换。对于进程,我们需要将状态保存到进程控制块(PCB)中,现在我们需要一个或多个线程控制块(TCB)来保存每个线程的状态,但是和进程上下文切换相比,线程在进行上下文切换的时候地址空间保持不变(即不需要切换当前使用的页表)。一个拥有多线程的进程的地址空间,如下图所示,我们可以看到每个线程拥有有自己的栈。

    有栈协程就是实现了一个用户态的线程,用户可以在堆上模拟出协程的栈空间,当需要进行协程上下文切换的时候,主线程只需要交换栈空间和恢复协程的一些相关的寄存器的状态就可以实现一个用户态的线程上下文切换,没有了从用户态转换到内核态的切换成本,协程的执行也就更加高效。

  7. 网络IO的模型:同步,异步,阻塞,非阻塞,IO多路复用

    网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作。对于一次IO访问,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间,所以一般会经历两个阶段:

    • 等待所有数据都准备好或者一直在等待数据,有数据的时候将数据拷贝到系统内核;
    • 将内核缓存中数据拷贝到用户进程中;

    对于socket流而言:

    • 等待网络上的数据分组到达,然后被复制到内核的某个缓冲区;
    • 把数据从内核缓冲区复制到应用进程缓冲区中;
**阻塞和非阻塞**:是进程在访问数据的时候,数据内是否准备就绪的一种处理方式

- 阻塞:往往需要等待缓冲区中的数据准备好过后才处理其他的事情,否则一直等待。
- 非阻塞:当进程访问数据缓冲区的时候  数据没有准备好的时候   直接返回 不需要等待。数据有的时候 也直接返回.

**同步和异步**:都是基于应用程序和操作系统处理IO时间锁采用的方式。

- 同步:应用程序要直接参与IO读写的操作。必须阻塞在某个方法上(read  或 write)面等待我们的IO时间完成 (阻塞IO事件或则通过轮询IO事件的方式)。对线程的性能开销比较大。
- 异步:所有的IO读写交给操作系统去处理.。这个时候  我们可以去做其他的事情  并不需要去完成真正的IO操作,当操作完成IO后 会通知我们的应用程序。

**阻塞/非阻塞 和 同步/异步 区别**

- 阻塞与非阻塞是针对线程来说的,阻塞可能发生在IO期间也可能发生在IO之前。
- 同步与异步是针对IO操作来说的,同步是用户线程一直盯着IO直到完成,异步是用户线程在IO完成时会收到通知。



**IO多路复用**

读写事件交给一个单独的线程来处理。
线程完成IO事件的注册,并不断的去轮询我们的读写缓冲区  看是否有数据准备好,通知相应读写线程. 

- 同步阻塞IO

  - 同步体现在IO完成之前用户线程不能做别的事情。
  - 阻塞体现在用户线程从发送read请求开始一直到内核线程完成IO读写和数据拷贝都是堵住的。

  img

- 同步非阻塞IO

  - 同步体现在IO完成之前用户线程不能做别的事情。
  - 非阻塞体现在用户线程发送read请求之后没有被堵住而是立刻返回。
    这里体现了同步与阻塞的区别,即虽然线程返回了,但是线程在没拿到结果之前干不了别的事情。

  img

- IO多路复用

  上面三个阶段都无法处理海量连接,IO多路复用技术实现了所有连接共用同一个阻塞对象,处理线程就在这个阻塞对象上等待。当某个连接有新的数据可以处理的时候操作系统会通知等待的处理线程进行IO读写和逻辑处理。
  IO多路复用不用一个线程去轮询所有连接,而是该线程阻塞在一个阻塞对象上等待通知,一般这个阻塞对象可以是select epoll poll等,这些对象会把IO封装成对应事件。阻塞对象就像一个过滤器,左边是内核线程的IO操作,右边是IO操作之后封装好的IO事件。

  img
  1. 多路IO复用

    • select

    select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。

    select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024。

    • poll

    poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

    • epoll

    epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

    epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

  2. 虚拟地址和物理地址

    每个进程的4G内存空间只是虚拟内存空间,每次访问内存空间的某个地址,都需要把地址翻译为实际物理内存地址。

    所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。

    进程要知道哪些内存地址上的数据在物理内存上,哪些不在,还有在物理内存上的哪里,需要用页表来记录
    页表的每一个表项分两部分,第一部分记录此页是否在物理内存上,第二部分记录物理内存页的地址(如果在的话)。

    当进程访问某个虚拟地址,去看页表,如果发现对应的数据不在物理内存中,则缺页异常。

    缺页异常的处理过程,就是把进程需要的数据从磁盘上拷贝到物理内存中,如果内存已经满了,没有空地方了,那就找一个页覆盖,当然如果被覆盖的页曾经被修改过,需要将此页写回磁盘。

    img

    优点

    • 既然每个进程的内存空间都是一致而且固定的,所以链接器在链接可执行文件时,可以设定内存地址,而不用去管这些数据最终实际的内存地址,这是有独立内存空间的好处;
    • 当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存;
    • 在程序需要分配连续的内存空间的时候,只需要在虚拟内存空间分配连续空间,而不需要实际物理内存的连续空间,可以利用碎片。
  3. 简述分页分段机制

    内存分段:Linux 使用虚拟地址空间,大大增加了进程的寻址空间,由低地址到高地址分别为:

    1、只读段:该部分空间只能读,不可写;(包括:代码段、rodata 段(C常量字符串和#define定义的常量) )

    2、数据段:保存全局变量、静态变量的空间;

    3、堆 :就是平时所说的动态内存, malloc/new 大部分都来源于此。其中堆顶的位置可通过函数 brk 和 sbrk 进行动态调整。

    4、文件映射区域 :如动态库、共享内存等映射物理空间的内存,一般是 mmap 函数所分配的虚拟地址空间。

    5、栈:用于维护函数调用的上下文空间,一般为 8M ,可通过 ulimit –s 查看。

    6、内核虚拟空间:用户代码不可见的内存区域,由内核管理(页表就存放在内核虚拟空间)。

    分页分段区别

    1.页是信息的物理单位,分页是由于系统管理的需要。段是信息的逻辑单位,分段是为了满足用户的要求。

    2.页的大小固定且由系统决定,段的长度不固定,决定于用户所编写的程序,通常由编译程序在对源程序紧进行编译时,根据信息的性质来划分。

    3.分页的作业的地址空间是一维的,程序员只需要利用一个记忆符,即可表示一个地址。分段的作业地址空间则是二维的,程序员在标识一个地址时,既需要给出段名,又需要给出段的地址值。

  4. 内存分配原理。

    1. 每个进程都有自己独立的4G内存空间,各个进程的内存空间具有类似的结构
    2. 一个新进程建立的时候,将会建立起自己的内存空间,此进程的数据,代码等从磁盘拷贝到自己的进程空间,哪些数据在哪里,都由进程控制表中的task_struct记录,task_struct中记录中一条链表,记录中内存空间的分配情况,哪些地址有数据,哪些地址无数据,哪些可读,哪些可写,都可以通过这个链表记录
    3. 每个进程已经分配的内存空间,都与对应的磁盘空间映射

    img

  5. 僵尸进程,孤儿进程。怎么预防,怎么解决。

    基本概念

    我们知道在unix/linux中,正常情况下,子进程是通过父进程创建的,子进程在创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束。 当一个 进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。

    孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

    僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

    个人理解:

    1、一般情况下,子进程是由父进程创建,而子进程和父进程的退出是无顺序的,两者之间都不知道谁先退出。正常情况下父进程先结束会调用 wait 或者 waitpid 函数等待子进程完成再退出,而一旦父进程不等待直接退出,则剩下的子进程会被init(pid=1)进程接收,成会孤儿进程。(进程树中除了init都会有父进程)。

    2、如果子进程先退出了,父进程还未结束并且没有调用 wait 或者 waitpid 函数获取子进程的状态信息,则子进程残留的状态信息( task_struct 结构和少量资源信息)会变成僵尸进程。

    3、守护进程( daemon) 是指在后台运行,没有控制终端与之相连的进程。它独立于控制终端,通常周期性地执行某种任务 。 守护进程脱离于终端是为了避免进程在执行过程中的信息在任何终端上显示并且进程也不会被任何终端所产生的终端信息所打断 。

    危害

    孤儿进程结束后会被 init 进程善后,并没有危害,而僵尸进程则会一直占着进程号,操作系统的进程数量有限则会受影响。

    解决

    一般僵尸进程的产生都是因为父进程的原因,则可以通过 kill 父进程解决,这时候僵尸进程就变成了孤儿进程,被 init 进程接收

    预防

    用两次fork(),然后使儿子进程直接退出,从而使孙子进程成为孤儿进程,进而由init进程负责清除这个孤儿进程。

  6. 操作系统进程状态有什么

    R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列 里。

    S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))。

    D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的 进程通常会等待IO的结束。

    T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可 以通过发送 SIGCONT 信号让进程继续运行。

    X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

    Z僵死状态(zombie)

  7. 操作系统进程调度

    1、时间片轮转调度(RR):给每个进程固定的执行时间,根据进程到达的先后顺序让进程在单位时间片内执行,执行完成后便调度下一个进程执行,时间片轮转调度不考虑进程等待时间和执行时间,属于抢占式调度。优点是兼顾长短作业;缺点是平均等待时间较长,上下文切换较费时。适用于分时系统。

    2、先来先服务调度算法(FCFS):根据进程到达的先后顺序执行进程,不考虑等待时间和执行时间,会产生饥饿现象。属于非抢占式调度,优点是公平,实现简单;缺点是不利于短作业。

    3、优先级调度算法(HPF):在进程等待队列中选择优先级最高的来执行。

    4、多级反馈队列调度算法:将时间片轮转与优先级调度相结合,把进程按优先级分成不同的队列,先按优先级调度,优先级相同的,按时间片轮转。优点是兼顾长短作业,有较好的响应时间,可行性强,适用于各种作业环境。

    5、高响应比优先调度算法:根据“响应比=(进程执行时间+进程等待时间)/ 进程执行时间”这个公式得到的响应比来进行调度。高响应比优先算法在等待时间相同的情况下,作业执行的时间越短,响应比越高,满足段任务优先,同时响应比会随着等待时间增加而变大,优先级会提高,能够避免饥饿现象。优点是兼顾长短作业,缺点是计算响应比开销大,适用于批处理系统。

  8. copy and write

    内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟究竟结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。

    img

    fork之后内核会通过将子进程放在队列的前面,以让子进程先执行,以免父进程执行导致写时复制,而后子进程执行exec系统调用,因无意义的复制而造成效率的下降。

  9. 操作系统的中断是否了解。

    在计算机运行时,内核是被信任的第三方,只有内核才可以执行特权指令,方便应用程序。

    • 系统调用(system call):应用程序主动向操作系统发出的服务请求

    • 异常(exception):非法指令或者其他原因导致当前指令执行失败 (如:内存出错)后的处理请求

    • 中断(hardware interrupt):来自硬件设备的处理请求

    img

    区别

    源头

    • 中断:外设引起

    • 异常:应用程序意想不到的行为

    • 系统调用:应用程序请求操作系统提供服务

    响应方式

    • 中断:异步

    • 异常:同步

    • 系统调用:异步或同步

    处理机制

    • 中断:持续,对用户应用程序是透明的

    • 异常:杀死或者重新执行意想不到的应用程序指令

    • 系统调用:等待和持续

    处理机制

    • 硬件处理:依据内部或者外部事件设置中断标志,然后依据中断向量调用相应的中断服务例程。

    • 软件处理

      • 首先进行现场保存(由编译器完成)
      • 然后进行中断服务处理(中断服务例程完成)
      • 接着清除中断标记(中断服务例程)
      • 最后进行现场恢复(编译器)
  10. 怎么杀死一个进程,kill杀不掉的呢。

    • 该进程是僵尸进程(STAT z),此时进程已经释放所有的资源,但是没有被父进程释放。僵尸进程要等到父进程结束,或者重启系统才可以被释放。

    • 进程处于“核心态”,并且在等待不可获得的资源,处于“核心态 ”的资源默认忽略所有信号。只能重启系统。

  11. nginx的location是什么。

    Nginx配置文件主要分为:全局设置、http块配置。全局配置用于配置进程、日志、工作模式、连接数等公共配置;http块配置用于配置日志格式、连接超时时间、gzip、缓冲等设置,通常包含1个或多个sever块。

    http块通常包含:server(主机设置)、upstream(上游服务器设置,主要为反向代理、负载均衡相关配置)。server又包含location

    server部分的指令主要用于指定虚拟主机域名、IP和端口;upstream的指令用于设置一系列的后端服务器,设置反向代理及后端服务器的负载均衡;location部分用于匹配网页位置(比如,根目录"/","/images",等等)。

    他们之间的关系是:

    server继承全局设置,location继承serverupstream既不会继承指令也不会被继承。它有自己的特殊指令,不需要在其他地方的应用。

    可以有多个server,location属于server子集。

    location指令的作用是根据用户请求的URI来执行不同的应用,也就是根据用户请求的网站URL进行匹配,匹配成功即进行相关的操作。

    语法规则:一个location关键字,后面跟着可选的修饰符,后面是要匹配的字符,花括号中是要执行的操作。

    location [ = | ~ | ~* | ^~ ] uri { ... }
    location @name { ... }
    

    修饰符

    • = 表示精确匹配。只有请求的url路径与后面的字符串完全相等时,才会命中。
    • ~ 表示该规则是使用正则定义的,区分大小写。
    • ~* 表示该规则是使用正则定义的,不区分大小写。
    • ^~ 表示如果该符号后面的字符是最佳匹配,采用该规则,不再进行后续的查找。

    匹配过程

    对请求的url序列化。例如,对%xx等字符进行解码,去除url中多个相连的/,解析url中的...等。这一步是匹配的前置工作。

    location有两种表示形式,一种是使用前缀字符,一种是使用正则。如果是正则的话,前面有~~*修饰符。

    具体的匹配过程如下:

    首先先检查使用前缀字符定义的location,选择最长匹配的项并记录下来。

    如果找到了精确匹配的location,也就是使用了=修饰符的location,结束查找,使用它的配置。

    然后按顺序查找使用正则定义的location,如果匹配则停止查找,使用它定义的配置。

    如果没有匹配的正则location,则使用前面记录的最长匹配前缀字符location。

    基于以上的匹配过程,我们可以得到以下两点启示:

    1. 使用正则定义的location在配置文件中出现的顺序很重要。因为找到第一个匹配的正则后,查找就停止了,后面定义的正则就是再匹配也没有机会了。
    2. 使用精确匹配可以提高查找的速度。例如经常请求/的话,可以使用=来定义location。
  12. nginx怎么负载均衡:通过upstream模块来实现的,内置实现了三种负载策略,配置还是比较简单的

    • 轮循(默认):Nginx根据请求次数,将每个请求均匀分配到每台服务器
    • 最少连接:将请求分配给连接数最少的服务器。Nginx会统计哪些服务器的连接数最少。
    • IP Hash:绑定处理请求的服务器。第一次请求时,根据该客户端的IP算出一个HASH值,将请求分配到集群中的某一台服务器上。后面该客户端的所有请求,都将通过HASH算法,找到之前处理这台客户端请求的服务器,然后将请求交给它来处理。
Nginx中master进程和worker进程是怎么通信的?

多进程(单线程) & 多路IO复用工作模式。

1、Nginx 在启动后,会有一个 master 进程和多个相互独立的 worker 进程。

2、接收来自外界的信号,向各worker进程发送信号,每个进程都有可能来处理这个连接。

3、 master 进程能监控 worker 进程的运行状态,当 worker 进程退出后(异常情况下),会自动启动新的 worker 进程。

img

为什么worker进程数和CPU数一致?

1)Ngnix一般只做高并发代理,基本没用IO操作,算是CPU密集操作,所以再处理时基本是瞬时完成的,很少出现IO阻塞。

2)进程与CPU调度的关系,单个核心处理多个进程的时候,是排队处理的,所以将Worker进程数量设置超过核心数是没有太大意义的。

 

每个worker进程都是单线程的进程,多workder进程可以充分利用多核系统架构,但若workder进程的数量多于CPU内核数,那么会增大进程间切换带来的消耗(linux是抢占式内核),一般情况下,用户要配置与CPU内核数相等的workder进程,并且使用下面的workder_cpu_affinity配置来绑定CPU内核,为什么要绑定workder进程到指定的CPU内核呢?假定每一个workder进程都是非常繁忙的,如果多个workder进程都在抢同一个CPU,那么这就会出现同步问题,反之,如果每一个worder进程都独享一个CPU,就在内核的调度策略上实现了完全的并发,例如,如果有四颗CPU内核,就可以进行如下配置:

```nginx
worker_processes 4;

worker_cpu_affinity 1000 0100 0010 0001;
```
  1. 线程池的线程数怎么确定?

    • IO密集型:如果存在ID,那么w/c>1,阻塞耗时一般都会比计算耗时的很多倍。如果不想做以上的计算,那么可以设置工作线程数为2倍cpu可用线程数。IO包括:数据库交互,文件上传下载,网络传输等

    • 计算密集型:工作线程数就是cpu可用核数。这样比较保险

  2. 传统IO拷贝次数

比如:读取文件,再用socket发送出去,传统方式实现:先读取、再发送,实际经过1~4四次copy。

1、第一次:将磁盘文件,读取到操作系统内核缓冲区;
2、第二次:将内核缓冲区的数据,copy到application应用程序的buffer;
3、第三步:将application应用程序buffer中的数据,copy到socket网络发送缓冲区(属于操作系统内核的缓冲区);
4、第四次:将socket buffer的数据,copy到网卡,由网卡进行网络传输。

img

你可能感兴趣的:(操作系统)