面试八股-通用
系统
基础
什么是协程
协程是一种运行在用户态的并发机制,类似于一种轻量级的用户态线程,协程一般运行在内核态线程之上(例如,可以按照n:m模型调度,即n个用户态协程运行在m个内核态线程上)。
协程分为有栈和无栈协程,有栈协程是指协程有自己栈空间,类似线程一样,无栈协程是指协程没有自己的栈空间,所有协程共享一个栈空间。golang的goroutine是典型的有栈协程,C++的coroutine是无栈协程。
进程和线程的区别
- 进程是资源分配的基本单位,线程是调度的基本单位
- 进程间相互隔离,不共享资源,一个挂了不影响其他的,线程共享进程资源,一个线程挂了整个进程全挂
- 线程运行在进程内部,一个进程包含多个线程
- 进程存在父子关系,线程不存在
- 进程上下文切换开销比线程大
- 进程间通信方式和线程不同
死锁的四个条件
- 互斥,资源具有排他性,一个资源只能被一个进程/线程占用
- 不可剥夺,资源只能由持有方主动释放,不能强行剥夺
- 持有且等待,一个进程/线程持有资源的同时又请求其他资源
- 循环等待,资源等待链够形成环形
进程/线程调度
抢占式调度,操作系统可以强制暂停正在运行的进程/线程,将CPU让出,现代通用操作系统基本都是抢占式调度(例如Windows、Linux等),通常基于时间片实现,抢占式调度由软中断和硬中断配合实现。
非抢占式调度,CPU资源只能由进程主动让出
调度算法
- 先来先服务,对长作业有利,但是对短作业不利
- 最短作业优先,会发生饥饿现象
- 高响应比优先,综合考虑执行时间和等待时间,响应比=(等待时间+执行时间)/执行时间,照顾到了长进程
- 最高优先级调度,每次选择优先级最高的进程调度执行,优先级可以是静态的和动态的
- 多级反馈队列调度
- 系统设置多个就绪队列,每个队列有不同的优先级和时间片长度。高优先级队列时间片短,低优先级队列时间片长。
- 新到达的进程通常先进入最高优先级队列,获得较高的响应速度。
- 如果进程在高优先级队列的时间片内没有完成,会被降到下一级队列。反之,如果低优先级队列的进程长时间未被调度,也可以提升其优先级,防止饥饿。
- 总是优先调度高优先级队列中的进程,只有高优先级队列空闲时才调度低优先级队列。
- 时间片轮转
页面置换策略
页面置换策略,也就是缓存淘汰策略
- 先进先出
- 最近最久未使用(LRU),通常用一个双向链表维护缓存页面信息,每当由页面被访问时,将其移动到链表头部,这样,链表尾部的页面就是最近最久未使用的页面,每次淘汰时选择链表尾部的页面。
- 最不常用(LFU),为每个页面维护一个访问计数器,每次淘汰访问次数最小的页面。
- 时钟页面置换,对每个内存页加个访问位,被访问到置位1,新页面为0,页面置换时扫描,将访问位为0时换出,访问位为1的重置为0。
- 最佳页面置换,最理想的页面置换,是一个概念,不是某个具体的算法。
系统调用的执行过程
- 应用程序在用户态通过库函数(如read、write、open等)发起系统调用请求。
- 系统调用会触发一次软中断(如x86上的int 0x80或syscall指令),CPU从用户态切换到内核态,进入操作系统内核。
- 系统调用id和参数通过寄存器或栈传递给内核,内核根据系统调用id找到对应的内核服务例程。
- 内核根据请求执行相应的操作(如文件读写、进程管理、内存分配等),访问硬件或内核资源。
- 内核将执行结果返回给用户程序,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 aux,x列出没有控制终端的进程,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 xxx或traceroute -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解决了什么问题
- 解决了select文件描述符上限是1024这个问题
- 对于select和poll,内核通过轮询来检查文件描述符的状态,文件描述符越多,效率越低
- 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火焰图,分析热点函数
代码审查
根据程序架构,找到可能性较大的瓶颈点,分析执行耗时点,内存分配和释放问题