Skip to content

面试八股-通用

系统

基础

什么是协程

协程是一种运行在用户态的并发机制,类似于一种轻量级的用户态线程,协程一般运行在内核态线程之上(例如,可以按照n:m模型调度,即n个用户态协程运行在m个内核态线程上)。

协程分为有栈和无栈协程,有栈协程是指协程有自己栈空间,类似线程一样,无栈协程是指协程没有自己的栈空间,所有协程共享一个栈空间。golang的goroutine是典型的有栈协程,C++的coroutine是无栈协程。

进程和线程的区别

  • 进程是资源分配的基本单位,线程是调度的基本单位
  • 进程间相互隔离,不共享资源,一个挂了不影响其他的,线程共享进程资源,一个线程挂了整个进程全挂
  • 线程运行在进程内部,一个进程包含多个线程
  • 进程存在父子关系,线程不存在
  • 进程上下文切换开销比线程大
  • 进程间通信方式和线程不同

死锁的四个条件

  1. 互斥,资源具有排他性,一个资源只能被一个进程/线程占用
  2. 不可剥夺,资源只能由持有方主动释放,不能强行剥夺
  3. 持有且等待,一个进程/线程持有资源的同时又请求其他资源
  4. 循环等待,资源等待链够形成环形

进程/线程调度

抢占式调度,操作系统可以强制暂停正在运行的进程/线程,将CPU让出,现代通用操作系统基本都是抢占式调度(例如Windows、Linux等),通常基于时间片实现,抢占式调度由软中断和硬中断配合实现。

非抢占式调度,CPU资源只能由进程主动让出

调度算法

  1. 先来先服务,对长作业有利,但是对短作业不利
  2. 最短作业优先,会发生饥饿现象
  3. 高响应比优先,综合考虑执行时间和等待时间,响应比=(等待时间+执行时间)/执行时间,照顾到了长进程
  4. 最高优先级调度,每次选择优先级最高的进程调度执行,优先级可以是静态的和动态的
  5. 多级反馈队列调度
    1. 系统设置多个就绪队列,每个队列有不同的优先级和时间片长度。高优先级队列时间片短,低优先级队列时间片长。
    2. 新到达的进程通常先进入最高优先级队列,获得较高的响应速度。
    3. 如果进程在高优先级队列的时间片内没有完成,会被降到下一级队列。反之,如果低优先级队列的进程长时间未被调度,也可以提升其优先级,防止饥饿。
    4. 总是优先调度高优先级队列中的进程,只有高优先级队列空闲时才调度低优先级队列。
  6. 时间片轮转

页面置换策略

页面置换策略,也就是缓存淘汰策略

  1. 先进先出
  2. 最近最久未使用(LRU),通常用一个双向链表维护缓存页面信息,每当由页面被访问时,将其移动到链表头部,这样,链表尾部的页面就是最近最久未使用的页面,每次淘汰时选择链表尾部的页面。
  3. 最不常用(LFU),为每个页面维护一个访问计数器,每次淘汰访问次数最小的页面。
  4. 时钟页面置换,对每个内存页加个访问位,被访问到置位1,新页面为0,页面置换时扫描,将访问位为0时换出,访问位为1的重置为0。
  5. 最佳页面置换,最理想的页面置换,是一个概念,不是某个具体的算法。

系统调用的执行过程

  1. 应用程序在用户态通过库函数(如read、write、open等)发起系统调用请求。
  2. 系统调用会触发一次软中断(如x86上的int 0x80或syscall指令),CPU从用户态切换到内核态,进入操作系统内核。
  3. 系统调用id和参数通过寄存器或栈传递给内核,内核根据系统调用id找到对应的内核服务例程。
  4. 内核根据请求执行相应的操作(如文件读写、进程管理、内存分配等),访问硬件或内核资源。
  5. 内核将执行结果返回给用户程序,CPU从内核态切换回用户态,应用程序继续运行。

软中断是中断的软件实现,和硬中断相对,软中断可以让cpu从用户态切换到内核态,主要用于实现系统调用和内核任务调度等。

进程控制块(PCB, Process Control Block)和线程控制块(TCB, Thread Control Block)是操作系统用来管理进程和线程的核心数据结构。它们分别保存如下内容:

进程控制块(PCB)主要内容: - 进程标识信息:进程ID(PID)、父进程ID、用户ID等 - 进程状态:就绪、运行、阻塞等 - 程序计数器(PC):指向下一条将要执行的指令地址 - CPU寄存器内容:上下文切换时保存/恢复 - 其它内容

线程控制块(TCB)主要内容: - 线程标识信息:线程ID、所属进程ID - 线程状态:就绪、运行、阻塞等 - 程序计数器(PC):线程当前执行位置 - CPU寄存器内容:线程上下文 - 栈指针和栈空间信息:每个线程有独立的栈 - 线程调度信息:优先级、调度队列指针等 - 线程局部存储(TLS)信息 - 其他:如信号掩码、线程私有数据等

程序内存布局

进程的内存布局大概分为以下5种内存区域 - 代码区 - Data区,已初始化的全局变量区 - BSS区,未初始化的全局变量区,会在程序启动后自动清零。 - 堆区 - 栈区

data区存放的数据是初始化过的全局变量,即编译期可以确定值的变量

bss区存放的是未初始化的全局变量,程序启动时会将这块内存区域清0

程序的内存布局是编译期决定的,编译完成后,内存布局信息就会写入到编出来的二进制文件中

可以通过一些工具查看可执行文件(exe,dll,so等)的内存布局,windows平台下可以用vs自带的dumpbin或者PE explorer和PE explorer v2查看,linux下可以用readelf查看

windows和linux的二进制文件格式不一样,windows是pe格式,linux是elf格式

实际的二进制文件中会有很多内存区域,上面提到的其实是理论上的5类内存区域

内存管理

虚拟内存和物理内存

虚拟内存是操作系统为每个进程提供的、看起来连续且独立的内存空间,又称逻辑内存空间。每个进程都认为自己拥有完整的、独立的内存,不会与其他进程冲突。虚拟内存的大小通常大于实际物理内存,依赖于磁盘空间(如交换分区或页面文件)进行扩展。

物理内存指的是计算机实际安装的内存条(RAM),是所有进程和操作系统共享的真实硬件资源。

虚拟内存和物理内存的映射由操作系统和硬件(MMU,内存管理单元)共同完成。操作系统将虚拟地址空间划分为页(Page),通过页表(Page Table)记录虚拟页与物理页的对应关系。当进程访问虚拟地址时,MMU 会根据页表将虚拟地址转换为物理地址,实现虚拟内存到物理内存的映射。如果访问的虚拟页不在物理内存中,会触发缺页中断,操作系统将所需数据从磁盘调入物理内存。

总结:

虚拟内存:进程看到的逻辑内存空间 物理内存:实际的硬件内存 映射方式:通过页表和MMU将虚拟地址映射到物理地址

内存碎片

内存碎片是指在内存分配和释放过程中,内存空间被分割成许多零散的小块,导致无法有效利用,无法分配大块连续内存

如何处理内存碎片

通过内存分配算法优化内存分配,尽量减少内存碎片产生,例如最佳适应算法(Best Fit),查找最小的满足请求的空闲块,可能导致内存碎片更小,但速度慢。

内存紧缩(Compaction),将内存中分散的小块空闲空间整理成大块连续空间(主要用于外部碎片,常见于早期操作系统)。

分页和分段机制,通过虚拟内存的分页(Page)和分段(Segment)技术,将物理内存分为固定大小的页或段,有效避免或减少外部碎片。

linux使用

  • ps,查看进程状态
  • ps命令参数分三种风格,带“-”的是unix风格,不带的是bsd风格,“--”是GNU风格,最坑的是不同风格的输出不一样 > 比如,ps -aux 和 ps aux其实完全不一样,-aux是想打印用户x的所有进程,如果用户x不存在,ps会帮你把命令改写成ps aux
  • 查看所有进程
    • ps -e,ps -ef,ps -eF,ps -ely
    • ps aux,ps ax
  • 查看进程树,ps -ejH,ps axjf
  • ps auxx列出没有控制终端的进程,u列出更多信息(用户、CPU、内存等)、a列出所有用户的进程
  • ps aux字段含义 USER 进程所有者的用户名 PID 进程ID(Process ID) START 进程激活时间 %CPU 进程的cpu占用率 %MEM 进程使用内存的百分比 VSZ 进程使用的虚拟内存大小,以K为单位 RSS 驻留空间的大小。显示当前常驻内存的程序的K字节数。 TTY 与进程关联的终端(tty) STAT 进程状态,包括下面的状态: D 不可中断的sleep,通常在IO R 正在运行,或在可运行队列中的进程 S 可中断的sleep状态,通常是在等待事件将其唤醒 T 暂停状态(ctrl+z),即进程挂起状态 t 由于调试器跟踪而暂停 Z 僵尸进程,进程已执行完毕,但父进程没有回收其退出状态,这时候进程会成为僵尸进程 和孤儿进程不同,孤儿进程是指父进程已结束,由init进程接管的进程 W 进入内存交换(从内核2.6开始无效) X 正在被销毁(非常短暂,几乎不可能被看到) 状态描述 < 高优先级 N 低优先级 L 有些页被锁进内存 s session leader(会话首进程) + 在前台进程组中 l 多线程,克隆线程 TIME 进程使用的总CPU时间 COMMAND 被执行的命令行 NI 进程的优先级值,较小的数字意味着占用较少的CPU时间 PRI 进程优先级。 PPID 父进程ID WCHAN 进程等待的内核事件名 >linux中的虚拟内存不是指swap,是指进程的逻辑地址空间,只有实际访问到的内存会被映射到物理内存,这部分物理内存叫常驻内存。
  • grep,文本搜索,-c:仅显示找到的行数,-n:显示行号,-i:忽略大小写,-v:反向搜索(搜索不包含指定字符串的行)
  • crontab,定时任务,格式,“分 时 日 月 星期 命令”
  • free,查看内存,默认按KB查看,-b,按字节展示,-k,按KB展示,-m,按MB展示,-g,按GB展示,-h,按适当的单位展示,-s N,隔N秒输出一次
  • 第一行是内存,第二行是swap
  • total是总内存,used是已使用的内存,free是未使用的内存,shared是共享使用的内存,buff/cache是buffer和cache使用的内存,available是应用程序可用的内存。
  • 和free相比,available算入了page cache >buffer是指buffer cache,buffer cache是存储在内存中的文件块(一个文件块大小不能超过内存页大小) > >cache是指page cache,一般翻译成页高速缓存,是用内存做磁盘缓存,包含对普通文件、块设备文件、内存映射文件的缓存。对块设备文件的缓存就是buffer cache。
  • free命令来自/proc/meminfo文件,直接查看这个文件也行
  • top,实时查看当前系统中的进程状态
  • 第一部分,系统总体状况
    • 第一行,当前时间,运行时间(up 2 day),当前用户数量,系统在之前1分钟、5分钟、15分钟的平均负载
    • 第二行(进程信息),系统中的进程总数,正在运行的进程数,睡眠的进程数,正在停止的进程数,僵尸进程数。
    • 第三行(cpu占用),用户模式,系统模式...
    • 第四、五行(内存信息,和free命令差不多)
  • 第二部分,各个进程的详细信息,以下是各字段含义 PID — 进程id USER — 进程所有者 PR — 进程优先级,值越小优先级越高 NI — nice值。静态优先级,值越小优先级越高 VIRT — 进程使用的虚拟内存总量,单位kb。 RES — 常驻内存大小,单位kb。 SHR — 共享内存大小,单位kb S —进程状态。 %CPU — cpu占用 %MEM — 内存占用 TIME+ — 进程使用的CPU时间总计,最小单位是秒 COMMAND — 命令
  • tar,压缩和打包命令
  • netstat,查看网络状态
  • 按域查看,-4,-6,-A <域>,--域,具体的域有inet(ipv4)、inet6(ipv6)等等
  • 按socket查看,-t,--tcp,-u,--udp,-x,--unix,-w,--raw等等
  • -a,查看所有socket(默认只显示建立连接的),-n,显示ip而不是域名,-p,显示pid(只有当前用户的进程号),-o,显示timers,-l,只显示监听,-e,显示用户和inode
  • -r,查看路由表,-i,查看网卡,-s,查看网络连接统计数据
  • -c,持续输出
  • ping,用于检测和目标主机的网络连接
  • 利用icmp协议的echo信息检查目的主机的可达性、延迟、报文丢失情况 >icmp协议(网络控制报文协议)是一种差错报告和信息查询协议,当数据传输出现错误时,利用icmp协议通知源主机数据传输出错,也可以利用icmp查询一些信息。 > >icmp报文放在ip报文的数据部分中,从报文结构上来看,icmp是ip的上层协议,但是icmp承担了ip层的任务,因此我们将icmp视为ip层协议
  • traceroute,用于查看到目的主机的路由路径
  • traceroute有三种实现,icmp、udp、tcp
  • udp,traceroute xxxtraceroute -T xxx,traceroute默认使用udp。向目的主机发送很多不同TTL的UDP报文,并访问一个大于30000的端口号,如果在传输到某个路由器时TTL的值减为0,则路由器回送一个超时的ICMP报文,如果报文到达了目的主机,则目的主机会回送一个端口不可达的ICMP报文。
  • icmp,traceroute -I xxx。利用icmp echo实现,向目的主机发送很多不同TTL的ICMP echo请求报文,如果在传输到某个路由器时TTL的值减为0,则路由器回送一个超时的ICMP报文,如果报文到达了目的主机,则目的主机会回送一个ICMP echo响应报文。 >TTL通常是指“报文最大生存时间”,但是在icmp里,TTL就是指路由器的跳数
  • tcp,traceroute -T xxx
  • 问题
    • 使用时看不到目的主机,使用udp协议时可能会出现这种情况,原因是目的主机没开放udp服务,回送不了“端口不可达”icmp报文
    • 使用时看不到中间路由器,可能是因为中间路由器不支持回送超时icmp报文
  • windows下的tracert使用icmp实现
  • kill,按进程id发信号
  • kill -l 可以查看所有信号的编号
  • kill默认发送的是TERM信号(编号15),会要求进程退出,进程在退出前可以做一些善后工作
  • kill -9 发送的是KILL信号,让进程强制退出,不给进程善后的机会
  • killall,按进程名字发信号
  • pkill,筛选进程并发信号
  • pgrep,筛选进程
  • linux的硬连接和软连接
  • 一个linux文件元信息分为两部分,一部分是文件名,另一部分是inode,inode存放了文件的属性、数据块指针等
  • 软连接,软连接类似于windows中的快捷方式,软连接不会对inode有真正的引用
  • 硬连接是对inode的真正引用,只有把硬连接全部删除,文件才会被删掉

linux的五种I/O模型

  • 阻塞式I/O
  • 普通的IO操作都是阻塞式I/O
  • 阻塞式I/O可能会被永久阻塞,比如网络连接一直没有数据过来
  • 非阻塞式I/O
  • 如果I/O操作因为不满足条件而无法完成,调用会立即出错返回
  • 误区1,阻塞式I/O和异步I/O混为一谈
    • 非阻塞是强调不会进入阻塞状态,调用一旦返回,这次调用就已经完成了。
    • 异步I/O强调的是I/O操作和调用方并行,异步调用发起后立即返回的不是最终结果,而是告诉调用者一个“已收到”信息,最终执行完成后通过某种手段通知调用方执行结果。
  • 误区2,非阻塞式I/O会造成进程或线程一直占用CPU
    • 占用CPU的不是非阻塞式I/O,而是程序员轮询非阻塞式I/O造成的(比如写个死循环不停地调用非阻塞式I/O,直到成功为止)
  • 信号驱动I/O
  • 内核用SIGIO这个信号通知进程有事件到达,是一种异步通知机制
  • 一般是UDP通信会使用这个I/O模型,原因是系统统一使用SIGIO这一个信号表示I/O事件,UDP只有两个事件会发出这个信号(数据到达和发生错误),比较容易判断和处理,而TCP有一堆事件会发出这个信号,而且信号发出过于频繁,没法采用这个I/O模型
  • I/O多路复用
  • 同时从多个文件描述符里读数据,可以在同一个线程内一个循环搞定,降低程序复杂度
  • 通过 I/O 复用函数向内核注册一组文件描述符,当某个文件描述符准备好数据时,I/O 复用函数把其中就绪的文件描述符返回给用户程序,用户程序只需要轮询I/O复用函数即可,I/O复用函数是阻塞式的,因此轮询并不会占用cpu资源
  • 异步I/O
  • 以上四种I/O模型,最多只能实现异步形式的通知,而最重要的数据传输都是同步的,即数据传输都要由用户线程或进程亲自完成。
  • 异步I/O数据传输由内核接管,用户进程只需要发起I/O请求和等待完成通知即可

linux I/O多路复用

  • linux的IO多路复用,多路是指多个文件描述符,复用是指单线程或单进程可以同时监听多个文件描述符
  • I/O多路复用是linux的五种I/O模型中的一种,是为了解决在同一个用户进程内同时读写多个文件描述符的问题
  • 三种,select/pselect、poll、epoll
  • 通过 I/O 复用函数向内核注册一组文件描述符,当某个文件描述符准备好数据时,I/O 复用函数把其中就绪的文件描述符返回给用户程序,用户程序只需要轮询I/O复用函数即可。I/O复用函数是阻塞式的,因此循环调用并不会占用cpu资源。
  • I/O复用函数实际上是把文件描述符的状态检查交给内核去接管,用户进程只需要读写数据。
  • I/O多路复用通过超时时间来控制文件描述符的阻塞时间
  • select
  • 传给内核的文件描述符集用一个位图来表示,同时要传入一个最大文件描述符+1这个一个值,告诉内核传入的描述符数,内核常量FD_SETSIZE表示这个值的最大值(1024)。
  • select返回准备好的描述符数,并且会根据准备好的描述符集覆写用户传入的位图。等于说是select返回以后,用户还得自己去遍历传入的位图,下一次调用select的时候再重新设置位图。
  • 按事件类型将文件描述符集分为可读、可写和异常三个文件描述符集,可以根据需要传入一个或多个文件描述符集。
  • select的变体pselect,调用的时候可以额外指定一个信号屏蔽字
  • poll
  • poll不像select一样为每个条件分别构造一个描述符集,而是采用一个数组,每个元素都是一个结构体,指定了文件描述符和事件类型。事件类型有两个,一个由用户写入,作为参数,一个由内核写入,作为返回。
  • poll突破了文件描述符1024上限的限制
  • epoll
  • epoll由linux 2.6版本引入
  • epoll有三个函数
    • epoll_create,创建epoll句柄(也是一个文件描述符)
    • epoll_ctl,注册文件描述符和事件
    • epoll_wait,等待事件发生
  • 相比select和poll,epoll解决了什么问题
    1. 解决了select文件描述符上限是1024这个问题
    2. 对于select和poll,内核通过轮询来检查文件描述符的状态,文件描述符越多,效率越低
    3. select和poll的每次调用是不相关的,每次调用都要重新构造参数,每次调用都会发生从用户态到内核态的内存拷贝
  • epoll的原理
    • epoll背后有一套数据结构在做支撑,因此不需要每次调用epoll_wait之前都重新注册文件描述符,减少了内存拷贝
    • epoll不是通过轮询来检查文件描述符的状态,而是给每个文件描述符注册一个回调函数,一旦有注册事件发生,就会调用回调函数把文件描述符加入一个就绪列表,epoll_wait函数只是把这个就绪列表返回给用户,效率大大提升。
    • epoll有两种工作模式LT(level-trigger, 水平触发,默认)和ET(edge-trigger, 边缘触发)
    • LT是如果一个文件描述符处于就绪状态,内核就会通知你操作,即使你每次都不做操作,内核还是每次都会通知你。LT同时支持阻塞式I/O和非阻塞式I/O
    • ET是内核只有在文件描述符发生变化的时候才会通知你,如果文件描述符就绪了,你没有做任何操作或者数据没读完,下次内核就不会继续通知你了,因为文件描述符的状态没有变。

linux进程

  • fork是一种copy-on-write机制
  • 孤儿进程,父进程结束的进程,由init进程接管,父进程变为init进程
  • 僵尸进程,进程结束执行后,其进程控制块(task_struct)一直没被父进程回收,处于这个状态的进程叫做僵尸进程。
  • 进程控制块通常是由父进程调用wait函数来回收,以获取子进程的返回码,运行状态等信息。
  • 解决办法,kill掉其父进程,僵尸进程会被init进程接管并回收。
  • 进程组
  • 每个进程都属于一个进程组
  • 每个进程组有一个组长进程,组长进程的进程id和进程组一致
  • fork的子进程默认加入父进程的进程组
  • 会话(session)
  • 会话和一个控制终端相关联,一个会话包含多个进程组,其中有一个是前台进程组,其他的是后台进程组
  • 会话首进程是控制进程,没有终端,会话首进程成为一个新进程组的组长
  • 用户在shell中启动的一个进程组称为一个作业
  • 只有前台作业能接受终端输入
  • 孤儿进程组
  • 如果一个组的某个进程的父进程是同一会话中的另一个组的进程,那么这个组就不是孤儿进程组,否则它是孤儿进程组
  • 换个说法,一个组的所有进程的父进程要么是组内的一个进程,要么是会话外的一个进程。
  • 对于孤儿进程组,操作系统会给它们中的停止进程发送SIGHUP(挂断)信号。
  • 守护进程
  • 守护进程是一种长期在系统后台运行的进程
  • 内核守护进程
    • ps命令输出的方括号中的进程是内核守护进程
    • 所有的内核进程由2号进程kthreadd创建,kthreadd也是一个内核进程。
    • 比如kswapd,负责内存换页
  • 用户守护进程
    • 由1号进程init创建,init是用户态进程(init进程是传统的linux初始化进程,systemd是较新的Linux初始化进程,pid也是1)
    • 比如inetd服务,负责侦听系统网络端口

linux进程间通信(IPC)

  • 信号,信号是一种软中断机制
  • 进程可以给一个信号注册一个处理函数,一旦接收到信号,就执行这个处理函数
  • 如何发信号
    • 用户按下某些终端按键,如ctrl+c
    • 用户使用某些特殊命令,如kill
    • 函数调用,如kill、raise、alarm
  • 作业控制信号
    • SIGINT,中断信号(ctrl+c),值为2,允许程序在退出前清理资源
    • SIGABRT,异常终止信号
    • SIGQUIT,退出(ctrl+\)
    • SIGTSTP,暂停(ctrl+z),可以忽视
    • SIGSTOP,暂停,不能忽视
    • SIGCONT,继续执行信号
    • SIGHUP信号,挂断会话
  • 其他信号
    • SIGTERM,请求退出
    • SIGKILL,强制退出,值为9,程序会立即被终止,无法进行任何清理或资源释放
    • SIGIO,IO事件
  • 管道
  • 只能在有公共祖先的进程间通信,比如父子进程
  • 调用pipe函数创建管道,返回两个文件描述符,对应管道的两端
  • 通常的做法是在父进程创建管道,然后fork子进程,这样子进程也拥有了这个管道。进程把自己不用的那端关掉。
  • 协同进程,现在有一个程序P,它从标准输入读,写到标准输出,如果有一个进程A,利用管道写P的标准输入,读P的标准输出,那么执行P的进程就是A的协同进程。
    • 具体实现,A fork 进程B,创建两个管道p1和p2,p1的方向是从A到B,p2的方向相反,然后在进程B中执行程序P,并将P的标准输入改为p1的读端,将P的标准输出改为p2的写端,这样就实现了A输出到P的标准输入,A从P的标准输入读,此时,B是A的协同进程。
  • FIFO,又称命名管道
  • 和管道不同,FIFO可以在两个不相关的进程之间使用
  • FIFO创建在文件系统中,有路径名
  • 三种XSI IPC(XSI是POSIX.1中的X/Open系统接口的简写,属于一种Unix规范)
  • 消息队列
    • 是内核中的一个链表,由消息队列标识符标识
    • 消息队列可以实现全双工通信
    • 消息队列最初的设计目的是提供全双工的高速通信,但随着后来很多新的IPC机制的提出,消息队列的性能优势已经不复存在,并且由于它存在的一些问题,新的程序里已经不推荐使用消息队列了。
    • 缺陷,没有引用计数
  • 信号量,是一个计数器,用来控制对临界资源的访问
    • 缺陷,信号量不是一个单个非负值,而是一个信号量值的集合;信号量的定义和初始化不是原子的;无端增加程序的复杂性
  • 共享内存
    • 共享内存允许多个进程共享一段内存空间,数据不需要来回拷贝,所以共享内存是一种最快的IPC
    • 共享内存段要先申请,然后连接到进程自己的内存地址空间
  • XSI IPC的统一缺陷
    • 它们在内核中是全局的,没有引用计数,不会自动回收,必须手动释放
    • XSI IPC的资源不是文件描述符的形式,而是各自专用的标识符
  • posix信号量
  • 解决了XSI信号量的不少问题,性能更高,没有信号量集的设定,不需要手动管理
  • 推荐用这个
  • 内存映射/存储映射(memory-mapped I/O mmap)
  • 把一个文件直接映射为一段内存地址,读写这段内存地址就相当于对这个文件I/O
  • 利用mmap实现
  • socket
  • tcp、udp、unix域
  • 记录锁
  • 对文件上锁,可以只对文件的一部分上锁
  • 三种操作:共享读、独占写、解锁
  • 记录锁只能用于多进程,不能用于多线程

linux线程同步

  • 线程基本操作
  • 创建,pthread_create
  • 终止,pthread_exit
  • 等待其他线程执行完毕,pthread_join
  • 请求取消另外一个线程,pthread_cancel,取消是一种非正常退出
  • 分离,pthread_detach,join操作不能等待分离线程
  • 线程其他操作
  • 获取线程id
  • 注册线程清理函数
  • 线程同步
  • 所有锁都可以设置阻塞时间
  • 互斥量mutex
  • 读写锁
  • 条件变量
    • 和互斥量配合使用
    • 使用步骤
    • 用户设定一个条件
    • 一个线程因等待条件成立而阻塞,pthread_cond_wait()
    • 另外一个线程使条件成立,pthread_cond_broadcast()pthread_cond_signal()
    • 条件变量分为两部分: 条件和变量。条件本身是由互斥量保护的,线程在改变条件状态前先要锁住互斥量。具体过程是这样的:线程进入wait前,由用户加锁mutex,线程进入wait后释放mutex,保证wait过程是原子操作;唤醒也一样,唤醒前先加锁,唤醒后解锁;
    • 条件变量类似于一个信号,线程本质上等待的是信号状态的改变
  • 自旋锁,不会让线程进入阻塞状态,而是进入忙等待状态,一直占用cpu资源
  • 屏障,各个线程在各自的某个位置设置一个屏障,先执行到屏障的线程停下来等待,等足够多的线程执行到屏障位置,大家再一起往下执行。pthread_join就是一种屏障,只不过只有一个线程在等待。

linux内核

  • linux内核主要负责
  • 内存管理
  • 进程和线程管理
  • 设备驱动
  • 内核态和用户态
  • 内核态是内核程序运行的空间
  • 用户态是用户程序运行的空间
  • 系统调用,内核态对用户态提供的访问接口
  • 内存管理
  • 存储层次,CPU、缓存、内存、外存
  • 段页式内存管理
    • 把内存划分为大的段,在段内部分页
    • 一个地址的高位是段地址,地位是页地址
    • 段表相当于是页表的索引
  • 快表(TLB)和页表,块表是页表的缓存
  • numa,现代多核cpu通常是numa结构,每个核心都有自己的cache,可访问内存区域等,构成了每个核心的内存访问子系统

Linux内核的两种内存分配函数kmalloc 和 vmalloc,但只适用于内核模块和驱动模块,不能再用户态程序中使用:

kmalloc

  • 分配物理上连续的内存区域,虚拟地址和物理地址一一对应。
  • 分配速度快,适合分配小块内存(通常小于几百 KB)。
  • 常用于需要 DMA(直接内存访问)等对物理连续性有要求的场景,如驱动开发、内核模块中缓存、队列等。

vmalloc

  • 分配虚拟地址连续但物理地址不一定连续的内存区域。
  • 适合分配大块内存(大于 kmalloc 能分配的最大块),如几百 KB 甚至 MB 级别。
  • 适用于对物理连续性无要求,但需要较大内存的内核模块。

网络

基础

  • 网络体系结构
  • OSI七层:物理层,数据链路层,网络层,传输层,会话层,表示层,应用层
    • 会话层想要解决两个应用程序之间的会话管理问题
    • 表示层想要解决的是两个应用程序之间通信的语法语义问题,数据转换问题(编码解码,压缩和解压缩)
  • 五层:物理层,数据链路层,网络层,传输层,应用层
  • TCP/IP四层:网络接口层,网络层,传输层,应用层
  • 数据链路层协议,CSMA/CD、PPP、PPPoE
  • 网络层协议,IP、ARP、RARP、OSPF、RIP、BGP
  • 路由协议,OSPF、RIP、BGP,内部网关协议IGP是一类协议
  • OSPF,是一种内部网关协议,开放最短路径优先协议,基于IP
  • RIP,是一种内部网关协议,距离矢量,基于UDP
  • BGP,边界网关协议,距离矢量,基于TCP
  • 传输层协议,TCP、UDP
  • 哪些基于UDP,RIP、DNS、DHCP
  • http状态码
  • 1开头,信息性状态码,接受的请求正在处理
  • 2开头,成功
  • 3开头,重定向
  • 4开头,客户端错误
  • 5开头,服务端错误
  • HTTP 1.0 为短连接,HTTP 1.1 支持长连接

TCP报文结构

TCP报文结构(20字节定长首部 + 可选首部 + 数据部分)
下面是20字节定长首部的结构示意图:

  0      4       8      12      16      20      24      28      32
  +-------+-------+-------+-------+-------+-------+-------+-------+
  |        源端口(16位)         |      目的端口(16位)         |
  +-------------------------------+-------------------------------+
  |                        序号(32位)                          |
  +---------------------------------------------------------------+
  |                     确认号(32位)                           |
  +---------------------------------------------------------------+
  | 数据偏移 | 保留 | 控制位 |        窗口(16位)                |
  |  (4位)  | (6位)| (6位)  |                                    |
  +-------------------------------+-------------------------------+
  |         校验和(16位)        |    紧急指针(16位)            |
  +-------------------------------+-------------------------------+
  |           选项(可变)        |           填充(可变)         |
  +---------------------------------------------------------------+
  |                        数据部分(可变)                       |
  +---------------------------------------------------------------+

说明: - 序号,4字节,报文序号 - 确认号,4字节,期望下次收到报文的序号 - 数据偏移:4位,TCP首部长度(以4字节为单位),因此TCP首部最长60字节 - 保留:6位,保留未用 - 控制位:6位(URG, ACK, PSH, RST, SYN, FIN) - 紧急字段URG,当URG=1时,代表此报文段中有紧急数据,应尽快传送。 - 确认字段ACK,当ACK=1时,表示确认,且确认号有效;当ACK=0时,确认号字段无效。 - 推送字段PSH,当PSH=1时,则报文段会被尽快地交付给目的方,不会对这样的报文段使用缓存策略。 - 复位字段RST,当RST为1时,表明TCP连接中出现了严重的差错,必须释放连接,然后再重新建立连接。 - 同步字段SYN,当SYN=1时,表示发起一个连接请求。 - 终止字段FIN,用来释放连接。当FIN=1时,表明此报文段的发送端的数据已发送完成,并要求释放连接。 - 窗口:2字节,接收窗口大小 - 校验和:2字节,首部和数据的校验 - 紧急指针:2字节,紧急数据的偏移量 - 选项和填充:可选字段,首部长度大于20字节时存在 - 数据部分:实际传输的数据内容

TCP三次握手四次挥手

================ TCP三次握手交互 ==================

Client                                          Server

            |                                |  CLOSED
CLOSED      |                                |  LISTEN
            | ---- SYN(seq=x) -------------> |  
SYN-SENT    |                                |  
            | <--- SYN(seq=y),ACK(ack=x+1) - |
            |                                |  SYN-RCVD
            | ---- ACK(ack=y+1) ------------>|  
ESTABLISHED |                                |  ESTABLISHED

================ TCP四次挥手交互 ==================

Client                                         Server

ESTABLISHED |                               |  ESTABLISHED
            | ---- FIN(seq=u) ------------->|  
FIN-WAIT-1  |                               |  
            | <--- ACK(ack=u+1) ------------|  
FIN-WAIT-2  |                               |  CLOSE-WAIT
            | <--- FIN(seq=v) --------------|  
            |                               |  LAST-ACK
            | ---- ACK(ack=v+1) ----------->|  
TIME-WAIT   |                               |  CLOSED
CLOSED      |                               |

三次握手

  • 客户端状态变迁:CLOSED,SYN-SENT,ESTABLISHED
  • 服务端状态变迁:CLOSED,LISTEN,SYN-RCVD,ESTABLISHED
  • 过程
  • 服务端从CLOSED变为LISTEN
  • 客户端发送 SYN,seq=x,从CLOSED变为SYN-SENT,x是客户端设定的初始序号
  • 服务端发送 SYN,ACK,seq=y,ack=x+1,从LISTEN变为SYN-RCVD
  • 客户端发送 ACK,seq=x+1,ack=y+1,从SYN-SENT变为ESTABLISHED
  • 服务端进入ESTABLISHED

为什么不能两次握手

  • 两次握手的话,服务端只有两个状态,监听和建立连接,服务端很有可能因为一个延迟收到的连接请求单方面进入连接状态,浪费资源。比如客户端向服务端发起连接,客户端重试了一次,第二次成功建立了链接,等连接释放后,服务端才接收到第一次连接请求,当方面进入了连接状态,而客户端并没有进入连接,白白浪费了客户端资源。
  • 而客户端没有这个问题,因为客户端是主动发起连接的一方,只要客户端不想进入连接,不管他接收到什么数据,都不会影响客户端。
  • 加了第三次握手以后,重试的连接请求只能导致客户端进入SYN-RCVD状态,不会导致进入连接状态

四次挥手

  • 一方主动断开
  • 主动断开方A状态变化,ESTABLISHED,FIN-WAIT-1,FIN-WAIT-2,TIME-WAIT,CLOSED
  • 被动断开方B状态变化,ESTABLISHED,CLOSE-WAIT,LAST-ACK,CLOSED
  • A发送 FIN,seq=x,A从ESTABLISHED变为FIN-WAIT-1
  • B发送 ACK,ack=x+1,B从ESTABLISHED变为CLOSED-WAIT
  • A从FIN-WAIT-1变为FIN-WAIT-2
  • B发送 FIN,ACK,seq=y,ack=x+1,B从CLOSED-WAIT变为LAST-ACK
  • A发送ACK,seq=x+1,ack=y+1,A从FIN-WAIT-2变为TIME-WAIT(等待2MSL),最后变为CLOSED
  • B从LAST-ACK变为CLOSED
  • 两方同时断开
  • 双方同时向对方发送FIN,同时回复ACK,分别进行两次挥手,同时释放连接

为什么要四次挥手

让被动断开的一方把剩余数据传输完毕

主动断开的一方为什么要等待2MSL

MSL是报文在网络中的最大生存时间,2MSL正好是报文一次往返的时间,这是为了确保对方收到了最后的ACK,避免对方ACK丢失重发FIN时主动关闭方收不到,从而无法关闭连接。

同时也是为了防止新连接收到网络上遗留的上次连接的旧数据包。

TCP如何保证可靠传输

  • 序列号和确认号
    每个TCP报文段都有一个序列号,用于标识数据的顺序;接收方通过确认号来告诉发送方自己已经收到的数据。
  • 选择确认(SACK),场景:接收方收到了一些数据,但另一些数据没有收到(或未按序到达),接收方希望发送方有选择地重传,而不是重传整个窗口的数据。
  • 超时重传
    发送方在规定的时间内没有收到确认应答,会自动重传未被确认的数据包
  • 有序传输
    接收方会根据序号将乱序到达的数据重新排序后交给应用层
  • 校验和
    TCP报文段包含一个校验和字段,用于检测数据在传输过程中是否发生了错误。

TCP流量控制

流量控制是指让发送方发送不要太快,让接收方来得及接收,主要利用滑动窗口来实现流量控制。

什么是滑动窗口

  • 发送窗口和接收窗口
  • 发送窗口中是允许发送的字节,发送窗口之前是已发送并得到确认的字节,发送窗口之后是不允许发送的字节
  • 接收窗口中是允许接收的字节,接收窗口之前是已经收到并确认过的字节,接收窗口之后是不允许接收的字节
  • 发送窗口是发送方的发送缓存的一部分;接收窗口是接收方的接收缓存的一部分
  • 窗口中的每个字节都有序号
    发送窗口的序号是发送方的序号,接收窗口的序号是接收方的序号,相互独立,各是各的
  • 发送方和接收方
  • 每一方都有自己的发送窗口和接收窗口;因此A的发送窗口和B的接收窗口是一对,反之是另外一对

如何利用滑动窗口控制流量

B会在每个TCP报文里带上自己当前的接收窗口大小,将自己的接收窗口大小发送给A,A会根据接收窗口的大小确定自己发送窗口的大小,因此发送窗口一定不会大于接收窗口

当接收方感觉发送速率过快时,减小接收窗口,并将新的窗口值发送给发送方。

零窗口时(接收窗口降为0时),发送方启动一个计时器,每隔一段时间发送一个探测报文,去获得当前最新的窗口值;防止接收方发出的窗口值更新报文滞留在网络中。

TCP拥塞控制

拥塞控制的目的是防止大量的报文进入网络中,把负载控制在网络可承受的范围内。

拥塞窗口,发送方根据网络状态自己计算出的一个窗口;可以理解为发送窗口=min(拥塞窗口cwnd,接收窗口rwnd)

有四种拥塞控制算法,分别是慢开始,拥塞避免,快重传和快恢复,这四种算法通过调整拥塞窗口的大小来控制网络的拥塞。

  • 慢开始
  • 发送方逐渐增加拥塞窗口,而不是一下子增大拥塞窗口。
  • 发送方将拥塞窗口cwnd设置为1,即1个报文段的大小(MSS),每收到一个确认,就把拥塞窗口增大1个报文段的大小(MSS),直到达到一个门限值ssthresh。注意,拥塞窗口实际是按乘法增加的(每次翻倍),因为拥塞窗口增加后,发送方能发出更多的报文,能收到更多的确认,拥塞窗口会增加速度会越来越快。
  • 拥塞避免
  • 慢开始和拥塞避免这两个过程之间有一个门限值ssthresh,拥塞窗口过了这个门限值以后切换为拥塞避免算法。
  • 拥塞避免算法把拥塞窗口的乘法增大改为加法增大,不再按收到的ack回复增大,而是每隔一个往返时间RTT就增大一个报文段的大小(MSS)
  • 发生超时后,把门限值设为当前拥塞窗口的一半,cwnd重置为1,从慢开始算法重新开始
  • 快重传
  • 快重传是为了避免发送方误以为网络发生了拥塞从而降低传输效率;,
  • 当接收方收到一个失序的数据包(即收到了后续的数据,但中间某个数据包丢失)时,会对最后一个按序收到的数据包的确认号进行重复确认,发送方一旦收到连续三个重复确认,就知道有数据丢失了,马上重传数据,而不进入超时等待;
  • 快恢复
  • 发送方现在知道了只是丢失了个别数据,因此重传的时候不执行慢开始,把发送窗口增大到门限值后直接进入拥塞避免算法;

unix套接字

  • 概念
  • 进程,是一个通信实体
  • ip:port,是一种通信地址资源
  • unix套接字,是对一个通信端点的抽象
  • 套接字类型(可以看作是对传输特性的描述)
    • SOCK_DGRAM,固定长度、无连接、不可靠的报文传输
    • SOCK_STREAM,面向连接、有序、可靠的字节流传输
    • SOCK_RAW,IP协议的数据报接口
    • SOCK_SEQPACKET,固定长度、面向连接、有序、可靠的报文传输
  • 域类型
    • AF_INET,IPv4域
    • AF_INET6,IPv6域
    • AF_UNIX,unix域
    • AF_UNSPEC,未指定
  • 套接字和域的组合决定了协议,一个组合可以对应多个协议,在IPv4域中,SOCK_STREAM默认对应TCP协议,SOCK_DGRAM默认对应UDP协议
  • 操作
  • socket,创建一个套接字
  • bind,绑定一个ip:port
  • 服务端
    • listen,监听请求,监听请求的套接字称为原始套接字
    • accept,获得一个连接请求并建立连接,每个连接对应一个新套接字,它们的地址和原始套接字相同
  • 客户端
    • connect,和服务端建立连接
  • close,关闭套接字
  • 一个ip:port可以建立多个tcp连接吗?
  • 连接是通过套接字建立的,一般情况下,一个ip:port只能绑定到一个套接字上,如果ip:port被占用,又试图去用另外一个套接字用这个地址通信,就会出现端口冲突。
  • 对于服务端,原始套接字负责监听,每建立一个连接,就创建一个新套接字负责数据传输,原始套接字和新套接字共用一个地址。因此,对于服务端,一个ip:port可以通过原始套接字建立多个tcp连接。或者说,只能有一个套接字监听一个ip:port,但是可以和多个客户端建立连接。
  • 对于客户端,没有原始套接字的概念,一个套接字只能建立一个tcp连接,因而,对于客户端,一个ip:port只能建立一个tcp连接

系统设计

restful api

  • 把接口视为资源
  • 用HTTP方法表示操作,GET、POST、PUT、DELETE,读取、新增、修改、删除
  • 用URL表示资源,而不是通过参数表示资源
  • 接口命名(命名中只包含资源,不能包含操作)

设计模式

  • 单例模式,静态全局变量,私有构造函数,静态方法获取全局变量
  • 懒汉式,获取时初始化单例(存在线程安全问题)
  • 饿汉式,直接初始化单例
  • 工厂模式
  • 简单工厂,根据参数返回特性对象
  • 抽象工厂,根据参数返回特定工厂
  • 模板模式

性能分析

监控和告警

看哪些指标下降,包括机器和服务两种监控

机器监控主要看机器资源(cpu idle、memory idle,网络带宽情况),服务监控主要看服务状态(稳定性,耗时,流量)

日志分析

分析全链路线上日志,按照关注的日志项,使用grep、tail、wc等命令分析请求执行情况

压力测试+性能分析工具

gperf + brpc火焰图,分析热点函数

代码审查

根据程序架构,找到可能性较大的瓶颈点,分析执行耗时点,内存分配和释放问题