操作系统

212 阅读29分钟
  1. 文件权限

    1. Linux文件的基本权限就有九个,分别是owner/group/others.
    2. 三种身份各有自己的read/write/execute权限,修改权限指令:chmod。每种身份(owner/group/others)
    3. 各自的三个权限(r/w/x)分数是需要累加的
  2. 软链接和硬链接的区别

    1. 定义不同
      1. 软链接又叫符号链接,这个文件包含了另一个文件的路径名。可以是任意文件或目录,可以链接不同文件系统的文件。
      2. 硬链接就是一个文件的一个或多个文件名。把文件名和计算机文件系统使用的节点号链接起来。因此我们可以用多个文件名与同一个文件进行链接,这些文件名可以在同一目录或不同目录。
    2. 限制不同
      1. 硬链接只能对已存在的文件进行创建,不能交叉文件系统进行硬链接的创建;
      2. 软链接可对不存在的文件或目录创建软链接;可交叉文件系统;
    3. 创建方式不同
      1. 硬链接不能对目录进行创建,只可对文件创建;
      2. 软链接可对文件或目录创建;
    4. 影响不同
      1. 删除一个硬链接文件并不影响其他有相同 inode号的文件。
      2. 删除软链接并不影响被指向的文件,但若被指向的原文件被删除,则相关软连接被称为死链接(即 dangling link,若被指向路径文件被重新创建,死链接可恢复为正常的软链接)。
  3. 静态库和动态库

    1. 静态库的制作:
      1. gcc hello.c -c //这样就生成hello.o目标文件
      2. ar rcs libhello.a hello.o//生成libhello.a静态库
    2. 静态库的使用:
      1. gcc main.c -lhello -o staticLibrary//main.c和hello静态库链接,生成staticLibrary执行文件
      2. main.c: 是指main主函数
      3. -lhello: 是我们生成的.a 文件砍头去尾(lib不要 .a也不要)前面加-l
      4. -L: 是指告诉gcc编译器先从-L指定的路径去找静态库,默认是从/usr/lib/ 或者 /usr/local/lib/ 去找。
      5. ./: 是指当前路径的意思
      6. staticLibrary: 是最后想生成的文件名(这里可随意起名字)
    3. 动态库的制作:
      1. gcc -shared -fpic hello.c -o libhello.so
      2. -shared 指定生成动态库
      3. -fpic :fPIC选项作用于编译阶段,在生成目标文件时就得使用该选项,以生成位置无关的代码。
    4. 动态库的使用:
      1. gcc main.c -lhello -L ./ -o dynamicDepot
      2. main.c: 是指main主函数
      3. -lhello: 是我们生成的.so 文件砍头去尾(lib不要 .so也不要)前面加-l
      4. -L: 是指告诉gcc编译器先从-L指定的路径去找静态库,默认是从/usr/lib/ 或者 /usr/local/lib/ 去找。
      5. ./: 是指当前路径的意思
      6. dynamicDepot: 是最后想生成的文件名(这里可随意起名字)
    5. 区别:
      1. 静态库代码装载的速度快,执行速度略比动态库快。
      2. 动态库更加节省内存,可执行文件体积比静态库小很多。
      3. 静态库是在编译时加载,动态库是在运行时加载。
      4. 生成的静态链接库,Windows下以.lib为后缀,Linux下以.a为后缀。生成的动态链接库,Windows下以.dll为后缀,Linux下以.so为后缀。
  4. GDB调试

    1. GDB调试:gdb调试的是可执行文件,在gcc编译时加入 -g ,告诉gcc在编译时加入调试信息,这样gdb才能调试这个被编译的文件 gcc -g tesst.c -o test
    2. GDB命令格式:
      1. quit:退出gdb,结束调试
      2. list:查看程序源代码
      3. list 5,10:显示5到10行的代码
      4. list test.c:5, 10: 显示源文件5到10行的代码,在调试多个文件时使用
      5. list get_sum: 显示get_sum函数周围的代码
      6. list test,c get_sum: 显示源文件get_sum函数周围的代码,在调试多个文件时使用
      7. reverse-search:字符串用来从当前行向前查找第一个匹配的字符串
      8. run:程序开始执行
      9. help list/all:查看帮助信息
      10. break:设置断点
      11. break 7:在第七行设置断点
      12. break get_sum:以函数名设置断点
      13. break 行号或者函数名 if 条件:以条件表达式设置断点
      14. watch 条件表达式:条件表达式发生改变时程序就会停下来
      15. next:继续执行下一条语句 ,会把函数当作一条语句执行
      16. step:继续执行下一条语句,会跟踪进入函数,一次一条的执行函数内的代码
      17. 条件断点:break if 条件 以条件表达式设置断点
      18. 多进程下如何调试:用set follow-fork-mode child 调试子进程或者set follow-fork-mode parent 调试父进程
  5. 进程调度算法

    1. 先来先服务调度算法
    2. 短作业(进程)优先调度算法
    3. 高优先级优先调度算法
    4. 时间片轮转法
    5. 多级反馈队列调度算法
  6. 操作系统如何管理内存

    1. 物理内存:物理内存有四个层次,分别是寄存器、高速缓存、主存、磁盘。操作系统会对物理内存进行管理,有一个部分称为内存管理器(memory manager) ,它的主要工作是有效的管理内存,记录哪些内存是正在使用的,在进程需要时分配内存以及在进程完成时回收内存。
    2. 虚拟内存:操作系统为每一个进程分配一个独立的地址空间,但是虚拟内存。虚拟内存与物理内存存在映射关系,通过页表寻址完成虚拟地址和物理地址的转换。
    3. 从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成: brk和mmap
    4. 为什么要用虚拟内存:因为早期的内存分配方法存在以下问题:
      1. 进程地址空间不隔离。会导致数据被随意修改。
      2. 内存使用效率低。
      3. 程序运行的地址不确定。操作系统随机为进程分配内存空间,所以程序运行的地址是不确定的。
    5. 使用虚拟内存的好处**:
      1. 扩大地址空间。每个进程独占一个4G空间,虽然真实物理内存没那么多。
      2. 内存保护:防止不同进程对物理内存的争夺和践踏,可以对特定内存地址提供写保护,防止恶意篡改。
      3. 可以实现内存共享,方便进程通信。
      4. 可以避免内存碎片,虽然物理内存可能不连续,但映射到虚拟内存上可以连续。
    6. 使用虚拟内存的缺点:
      1. 虚拟内存需要额外构建数据结构,占用空间。
      2. 虚拟地址到物理地址的转换,增加了执行时间。
      3. 页面换入换出耗时。
      4. 一页如果只有一部分数据,浪费内存。
  7. linux系统态与用户态

    1. 内核态与用户态:内核态(系统态)与用户态是操作系统的两种运行级别。内核态拥有最高权限,可以访问所有系统指令;用户态则只能访问一部分指令。
    2. 什么时候进入内核态:共有三种方式:a、系统调用。b、异常。c、设备中断。其中,系统调用是主动的,另外两种是被动的。
    3. 为什么区分内核态与用户态:在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。比如:清内存、设置时钟等。所以区分内核态与用户态主要是出于安全的考虑。
  8. LRU算法及其实现方式

    1. LRU算法:LRU算法用于缓存淘汰。思路是将缓存中最近最少使用的对象删除掉
    2. 实现方式:利用链表和hashmap。
      1. 当需要插入新的数据项的时候,如果新数据项在链表中存在(一般称为命中),则把该节点移到链表头部,如果不存在,则新建一个节点,放到链表头部,若缓存满了,则把链表最后一个节点删除即可。
      2. 在访问数据的时候,如果数据项在链表中存在,则把该节点移到链表头部,否则返回-1。这样一来在链表尾部的节点就是最近最久未访问的数据项。
  9. linux的栈是通过缺页来分配内存的,不是所有栈地址空间都分配了内存。因此,8M是最大消耗,实际的内存消耗只会略大于实际需要的内存(内部损耗,每个在4k以内)。

  10. 页表

    1. 页表是虚拟内存的概念。操作系统虚拟内存到物理内存的映射表,就被称为页表。减小虚拟内存页对应物理内存页的映射表大小.
    2. 在系统启动时,操作系统将整个物理内存以 4K 为单位,划分为各个页。之后进行内存分配时,都以页为单位,那么虚拟内存页对应物理内存页的映射表就大大减小了,4G 内存,只需要 8M 的映射表即可,一些进程没有使用到的虚拟内存,也并不需要保存映射关系,而且Linux 还为大内存设计了多级页表,可以进一页减少了内存消耗。
    3. 虚拟地址到物理地映射,三级页表转换方法:
      1. 逻辑地址转线性地址:段起始地址+段内偏移地址=线性地址
      2. 线性地址转物理地址:
  11. 操作系统中的缺页中断

    1. 缺页异常:malloc和mmap函数在分配内存时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常,引发缺页中断。
    2. 缺页中断:缺页异常后将产生一个缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存
    3. 缺页中断与一般中断一样,需要经历四个步骤:保护CPU现场、分析中断原因、转入缺页中断处理程序、恢复CPU现场,继续执行。
    4. 缺页中断与一般中断区别:
      1. 在指令执行期间产生和处理缺页中断信号
      2. 一条指令在执行期间,可能产生多次缺页中断
      3. 缺页中断返回的是执行产生中断的一条指令,而一般中断返回的是执行下一条指令。
  12. 并发和并行

    1. 并发:对于单个CPU,在一个时刻只有一个进程在运行,但是线程的切换时间则减少到纳秒数量级,多个任务不停来回快速切换。
    2. 并行:对于多个CPU,多个进程同时运行。
  13. 进程、线程、协程

    1. 进程:程序是指令、数据及其组织形式的描述,而进程则是程序的运行实例,包括程序计数器、寄存器和变量的当前值。
    2. 线程:微进程,一个进程里更小粒度的执行单元。一个进程里包含多个线程并发执行任务
    3. 协程:协程是微线程,在子程序内部执行,可在子程序内部中断,转而执行别的子程序,在适当的时候再返回来接着执行。
    4. 线程与进程的区别:
      1. 一个线程从属于一个进程;一个进程可以包含多个线程。
      2. 一个线程挂掉,对应的进程挂掉;一个进程挂掉,不会影响其他进程。
      3. 线程是系统资源调度的最小单位;线程CPU调度的最小单位。
      4. 进程系统开销显著大于线程开销;线程需要的系统资源更少。
      5. 进程在执行时拥有独立的内存单元,多个线程共享进程的内存,如代码段、数据段、扩展段;但每个线程拥有自己的栈段和寄存器组。
      6. 进程切换时需要刷新TLB并获取新的地址空间,然后切换硬件上下文和内核栈,线程切换时只需要切换硬件上下文和内核栈。
      7. 通信方式不一样。进程适应于多核、多机分布;线程适用于多核
    5. 线程与协程的区别:
      1. 协程执行效率极高。协程直接操作栈基本没有内核切换的开销,所以上下文的切换非常快,切换开销比线程更小。
      2. 协程程不需要多线程的锁机制,因为多个协程从属于一个线程,不存在同时写变量冲突,效率比线程高。个线程可以有多个协程。
  14. Linux的fork的作用

    1. fork函数用来创建一个子进程。对于父进程,fork()函数返回新创建的子进程的PID。对于子进程,fork()函数调用成功会返回0。如果创建出错,fork()函数返回-1。
    2. fork()函数不需要参数,返回值是一个进程标识符PID。返回值有以下三种情况:
      1. 对于父进程,fork()函数返回新创建的子进程的PID。
      2. 对于子进程,fork()函数调用成功会返回0。
      3. 如果创建出错,fork()函数返回-1。
    3. fork()函数创建一个新进程后,会为这个新进程分配进程空间,将父进程的进程空间中的内容复制到子进程的进程空间中,包括父进程的数据段和堆栈段,并且和父进程共享代码段。这时候,子进程和父进程一模一样,都接受系统的调度。因为两个进程都停留在fork()函数中,最后fork()函数会返回两次,一次在父进程中返回,一次在子进程中返回,两次返回的值不一样,如上面的三种情况
  15. 孤儿进程,僵尸进程, 守护进程

    1. 孤儿进程:是指一个父进程退出后,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并且由init进程对它们完整状态收集工作
    2. 僵尸进程:是指一个进程使用fork函数创建子进程,如果子进程退出,而父进程并没有调用wait()或者waitpid()系统调用取得子进程的终止状态,那么子进程的进程描述符仍然保存在系统中,占用系统资源,这种进程称为僵尸进程
    3. 如何解决僵尸进程: 一般,为了防止产生僵尸进程,在fork子进程之后我们都要及时使用wait系统调用;同时,当子进程退出的时候,内核都会给父进程一个SIGCHLD信号,所以我们可以建立一个捕获SIGCHLD信号的信号处理函数,在函数体中调用wait(或waitpid),就可以清理退出的子进程以达到防止僵尸进程的目的。 使用kill命令:ps aux | grep Z 会列出进程表中所有僵尸进程的详细内容,kill -s SIGCHLD pid(父进程pid)
    4. 守护进程:守护进程是运行在后台的一种生存期长的特殊进程。它独立于控制终端,处理一些系统级别任务
  16. 进程通信的方式

    1. 进程间通信主要包括管道、系统IPC(包括消息队列、信号量、信号、共享内存)、套接字socket
      1. 管道:包括无名管道和命名管道,无名管道半双工,只能用于具有亲缘关系的进程直接的通信(父子进程或者兄弟进程),可以看作一种特殊的文件;命名管道可以允许无亲缘关系进程间的通信。
      2. 消息队列:消息的链接表,放在内核中。消息队列独立于发送与接收进程,进程终止时,消息队列及其内容并不会被删除;消息队列可以实现消息的随机查询,可以按照消息的类型读取。
      3. 信号量semaphore:是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步。
      4. 信号:用于通知接收进程某个事件的发生。
      5. 内存共享:使多个进程访问同一块内存空间。
      6. 套接字socket:用于不同主机直接的通信。
  17. 进程同步的方式

    1. 信号量semaphore:是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步。P操作(递减操作)可以用于阻塞一个进程,V操作(增加操作)可以用于解除阻塞一个进程。
    2. 管道:一个进程通过调用管程的一个过程进入管程。在任何时候,只能有一个进程在管程中执行,调用管程的任何其他进程都被阻塞,以等待管程可用。
    3. 消息队列:消息的链接表,放在内核中。消息队列独立于发送与接收进程,进程终止时,消息队列及其内容并不会被删除;消息队列可以实现消息的随机查询,可以按照消息的类型读取
  18. 进程有五种状态:创建、就绪、执行、阻塞、终止。一个进程创建后,被放入队列处于就绪状态,等待操作系统调度执行,执行过程中可能切换到阻塞状态(并发),任务完成后,进程销毁终止。

    1. 创建状态:一个应用程序从系统上启动,首先就是进入创建状态,需要获取系统资源创建进程管理块(PCB:Process Control Block)完成资源分配。
    2. 就绪状态:在创建状态完成之后,进程已经准备好,处于就绪状态,但是还未获得处理器资源,无法运行。
    3. 运行状态:获取处理器资源,被系统调度,当具有时间片开始进入运行状态。如果进程的时间片用完了就进入就绪状态。
    4. 阻塞状态:在运行状态期间,如果进行了阻塞的操作,如耗时的I/O操作,此时进程暂时无法操作就进入到了阻塞状态,在这些操作完成后就进入就绪状态。等待再次获取处理器资源,被系统调度,当具有时间片就进入运行状态。
    5. 终止状态:进程结束或者被系统终止,进入终止状态
  19. mmap的原理和使用场景

    1. mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read, write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享
    2. 使用场景: 对同一块区域频繁读写操作; 可用于实现用户空间和内核空间的高效交互 可提供进程间共享内存及相互通信 可实现高效的大规模数据传输。
  20. 线程间的通信方式包括临界区、互斥量、信号量、条件变量、读写锁:

    1. 临界区:每个线程中访问临界资源的那段代码称为临界区(Critical Section)(临界资源是一次仅允许一个线程使用的共享资源)。每次只准许一个线程进入临界区,进入后不允许其他线程进入。不论是硬件临界资源,还是软件临界资源,多个线程必须互斥地对它进行访问。
    2. 互斥量:采用互斥对象机制,只有拥有互斥对象的线程才可以访问。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
    3. 信号量:计数器,允许多个线程同时访问同一个资源。
    4. 条件变量:通过条件变量通知操作的方式来保持多线程同步。
    5. 读写锁:读写锁与互斥量类似。但互斥量要么是锁住状态,要么就是不加锁状态。读写锁一次只允许一个线程写,但允许一次多个线程读,这样效率就比互斥锁要高。
    6. 递归锁:(std::recursive_mutex)递归锁是一种特殊类型的锁,它允许同一个线程多次获取同一个锁。每次获取锁时,递归锁的计数器会增加,当释放锁时,计数器减少。只有当计数器为0时,锁才会被真正释放。
      1. 递归锁支持递归锁定,避免了死锁的发生。同一个线程可以多次获取同一个锁,但必须确保释放次数与获取次数相同。
      2. 递归锁的性能略低于普通锁,因为需要维护递归计数器。
      3. 递归锁可以和C++11中的std::unique_lock一起使用,以便在异常安全的情况下管理锁的生命周期。
  21. 说说什么是死锁,产生的条件,如何解决

    1. 死锁: 是指多个进程在执行过程中,因争夺资源而造成了互相等待。此时系统产生了死锁。比如两只羊过独木桥,若两只羊互不相让,争着过桥,就产生死锁
    2. 产生的条件:死锁发生有四个必要条件:
      1. 互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问,只能等待,直到进程使用完成后释放该资源;
      2. 请求保持条件:进程获得一定资源后,又对其他资源发出请求,但该资源被其他进程占有,此时请求阻塞,而且该进程不会释放自己已经占有的资源;
      3. 不可剥夺条件:进程已获得的资源,只能自己释放,不可剥夺;
      4. 环路等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
    3. 如何解决:
      1. 资源一次性分配,从而解决请求保持的问题
      2. 可剥夺资源:当进程新的资源未得到满足时,释放已有的资源;
      3. 资源有序分配:资源按序号递增,进程请求按递增请求,释放则相反。
  22. 信号量

    1. 概念:信号量本质上是一个计数器,用于多进程对共享数据对象的读取,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。
    2. 原理:由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),具体的行为如下:
      1. P(sv)操作:如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行(信号量的值为正,进程获得该资源的使用权,进程将信号量减1,表示它使用了一个资源单位)。
      2. V(sv)操作:如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1(若此时信号量的值为0,则进程进入挂起状态,直到信号量的值大于0,若进程被唤醒则返回至第一步)。
  23. 进程、线程的中断切换的过程,上下文切换指的是内核(操作系统的核心)在CPU上对进程或者线程进行切换。

    1. 进程上下文切换
      1. 保护被中断进程的处理器现场信息
      2. 修改被中断进程的进程控制块有关信息,如进程状态等
      3. 把被中断进程的进程控制块加入有关队列
      4. 选择下一个占有处理器运行的进程
      5. 根据被选中进程设置操作系统用到的地址转换和存储保护信息
      6. 切换页目录以使用新的地址空间
      7. 切换内核栈和硬件上下文(包括分配的内存,数据段,堆栈段等)
      8. 根据被选中进程恢复处理器现场
    2. 线程上下文切换
      1. 保护被中断线程的处理器现场信息
      2. 修改被中断线程的线程控制块有关信息,如线程状态等
      3. 把被中断线程的线程控制块加入有关队列
      4. 选择下一个占有处理器运行的线程
      5. 根据被选中线程设置操作系统用到的存储保护信息
      6. 切换内核栈和硬件上下文(切换堆栈,以及各寄存器)
      7. 根据被选中线程恢复处理器现场
  24. 线程状态相互之间转换

    1. 创建状态:
      一个应用程序从系统上启动,首先就是进入创建状态,获取系统资源。
    2. 就绪状态 在创建状态完成之后,线程已经准备好,处于就绪状态,但是还未获得处理器资源,无法运行。
    3. 运行状态 获取处理器资源,被系统调度,当具有时间片开始进入运行状态。如果线程的时间片用完了就进入就绪状态。
    4. 阻塞状态 在运行状态期间,如果进行了阻塞的操作,如耗时的I/O操作,此时线程暂时无法操作就进入到了阻塞状态,在这些操作完成后就进入就绪状态。等待再次获取处理器资源,被系统调度,当具有时间片就进入运行状态。
    5. 终止状态
      线程结束或者被系统终止,进入终止状态
  25. 条件变量

    1. 条件变量类([condition_variable]是一个同步原语,它可以在同一时间阻塞一个线程或者多个线程,直到其他线程改变了共享变量(条件)并通知。

    2. 为什么需要条件变量

      1. 减少轮询从而提高效率。没有条件变量,CPU会浪费时间反复轮询某一个条件。条件变量出现使得线程可以在不满足条件进行休眠,将资源让给有需要的其他线程;
      2. 线程之间的协调。两个线程之间执行可以变得有序,也就是一个线程执行完成后,另外一个线程才会执行;
      3. 复杂同步条件。有时候一个线程不仅需要知道资源可达,还需要知道一些额外信息,比如说生产消费者中,缓冲队列是否满之类的操作;
    3. 条件变量是如何完成同步的

      1. 一个线程完成对数据的修改,另一个线程进行等待
      2. 数据修改完成,通知另一个线程已经完成修改
      3. 等待的线程收到通知,继续进行线程的执行
    4. 虚假唤醒的意思时,当一个正在等待条件变量的线程由于条件变量被触发而唤醒时,却发现它等待的条件(共享数据)没有满足(也就是没有共享数据)。

    5. 针对虚假唤醒的情况,解决办法就是在每次使用共享数据之前先判断一下是否为空,为空则继续等待

    6. pthread_cond_wait,pthread_cond_signal

      1. pthread_cond_wait用于阻塞当前线程,等待别的线程使用pthread_cond_signal()或 pthread_cond_broadcast来唤醒它。 
      2. pthread_cond_wait() 必须与pthread_mutex配套使用。pthread_cond_wait()函数一进入wait状态就会自动release mutex。当其他线程通过pthread_cond_signal()或pthread_cond_broadcast,把该线程唤醒,使pthread_cond_wait()通过(返回)时,该线程又自动获得该mutex。
      3. pthread_cond_signal函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行.如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。
      4. 使用pthread_cond_signal一般不会有“惊群现象”产生,他最多只给一个线程发信号。假如有多个线程正在阻塞等待着这个条件变量的话,那么是根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。但无论如何一个pthread_cond_signal调用最多发信一次。
      5. 但是pthread_cond_signal在多处理器上可能同时唤醒多个线程,当你只能让一个线程处理某个任务时,其它被唤醒的线程就需要继续wait,而且规范要求pthread_cond_signal至少唤醒一个pthread_cond_wait上的线程,其实有些实现为了简单在单处理器上也会唤醒多个线程. 另外,某些应用,如线程池,pthread_cond_broadcast唤醒全部线程,但我们通常只需要一部分线程去做执行任务,所以其它的线程需要继续wait.所以强烈推荐对pthread_cond_wait()使用while循环来做条件判断.
    7. 信号量不需要配合mutex使用的原因:信号量是利用多线程改变计数器数量动态实现同步的,同时P / V操作是原子操作,因此不需要搭配互斥锁使用

    8. 理解 pthread_cond_wait,它实际做的事情可以分为 4 部分: 将当前线程加入等待队列;

      1. pthread_mutex_unlock(&mutex);
      2. wait 等待唤醒;
      3. pthread_mutex_lock(&mutex);
      4. 所以线程在进入 wait 前已经解开了互斥锁,当被唤醒时会重新上锁。之所以要这么做,是因为我们必须要保证解锁和 wait 是原子操作,不然解了锁还没进入 wait 的阶段条件变量可能会发生改变。
    9. 条件变量使用方法 条件变量.png

  26. Linux零拷贝的原理

    1. 什么是零拷贝: 所谓「零拷贝」描述的是计算机操作系统当中,CPU不执行将数据从一个内存区域,拷贝到另外一个内存区域的任务。通过网络传输文件时,这样通常可以节省 CPU 周期和内存带宽。
    2. 零拷贝的好处:
      1. 节省了 CPU 周期,空出的 CPU 可以完成更多其他的任务
      2. 减少了内存区域之间数据拷贝,节省内存带宽
      3. 减少用户态和内核态之间数据拷贝,提升数据传输效率
      4. 应用零拷贝技术,减少用户态和内核态之间的上下文切换
    3. 零拷贝原理: 在传统 IO 中,用户态空间与内核态空间之间的复制是完全不必要的,因为用户态空间仅仅起到了一种数据转存媒介的作用,除此之外没有做任何事情。
      1. Linux 提供了 sendfile() 用来减少我们的数据拷贝和上下文切换次数。
      2. mmap 数据零拷贝原理
  27. Reactor模式

    1. Reactor模式:是指通过一个或多个输入同时传递给服务器的服务请求的事件驱动处理模式。服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程。即I/O复用统一监听事件,收到事件后分发给某线程
    2. Reactor模式中有两个关键组成:
      1. Reactor:Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应,就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人。
      2. Handlers:处理程序执行IO事件要完成的实际事件。Reactor通过适当的处理程序来响应IO事件,处理执行执行非阻塞操作。
  28. 自旋锁和互斥锁的使用场景

    1. 互斥锁用于临界区持锁时间比较长的操作,比如下面这些情况都可以考虑
      1. 临界区有IO操作
      2. 临界区代码复杂或者循环量大
      3. 临界区竞争非常激烈
      4. 单核处理器
    2. 自旋锁就主要用在临界区持锁时间非常短且CPU资源不紧张的情况下。
  29. 共享内存 Shared memory,使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。共享内存的通信方式不需要内核的参与。

    1. 通过 shmget(Shared Memory GET)来分配一个共享内存。
    2. 要让一个进程获取对一块共享内存的访问,这个进程必须先调用 shmat(SHared Memory Attach),绑定到共享内存。