- 进程管理
- 进程、线程、协程
- 进程通信方式
- 线程同步方式
- 死锁的条件
- 生产者-消费者
- 内存管理
- 分页、分段、段页
- 页面置换算法
- 虚拟地址空间
- 快表
- 伙伴系统
- SLAB分配器
- 页表目录
- 文件管理
- VFS虚拟文件系统
- page cache
- FAT
- ext2
- IO
- 中断机制
- DMA和通道
- IO多路复用技术
- 内核态和用户态
- 系统调用
- 中断
- 异常 参考视频
- 内核态用于执行一些特权指令,比如中断机制,原语,进程管理等,目的是保护系统程序。
- 内核态通常包括三个方面:系统调用,中断和异常
- 中断和异常都可以实现用户态到内核态的切换,是通过硬件实现的。
- 中断:IO完成,定时器时钟中断。
- 异常:越界,溢出,缺页,异常不能被屏蔽,一旦出现应立即处理。
- fwrite可以把数据写到用户空间缓冲区,但不会立即写到内核
- write内核函数
- fflush把用户缓冲区的数据立即写到内核中的页缓冲区
- 当我们写文件的时候,内核的文件系统模块把数据保存在页缓冲区中,不会立即写到存储设备。我们可以使用fsync把文件修改过的属性和数据立即写到存储设备,或者使用fsyncdata把文件修改过的数据立即写到存储设备。
- 应用程序可以使用glibc库封装的标准IO流函数访问文件。
- 标准IO流提供了缓冲区,目的是尽可能减少调用read和write的次数,提高性能。
- 参考博客
- fwrite---application data---clib buffer-----page cache----disk
- fwrite返回的时候,数据还在clib buffer中,调用fclose可以触发文件写入到disk中。
- clib buffer刷新到page cache内核缓冲区可以调用fflush函数(非系统调用)
- page cache内核缓冲区强制刷新到磁盘可以调用fsync函数(系统调用)
- write函数直接从application data拷贝到page cache内核缓冲区
- -普通文件
- d目录
- l符号连接
- s套接字
- b块文件
- c字符文件
- p管道
- 进程是系统进行资源分配的基本单位,是程序加载到内存后的执行过程。进程一般由数据段,代码段和进程控制块三部分组成。系统通过进程控制块感知进程的存在并对进程进行控制。由于进程之间虚拟地址空间相互独立,多进程比多线程更安全,一个进程基本上不会影响另外一个进程。
- 就绪
- 运行
- 阻塞
- 管道
- 共享内存
- 消息队列
- 信号(开销小)
- 信号量
- socket
- 子进程先结束,而父进程没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。
- 父进程先结束,而子进程仍然存活,此时子进程称为孤儿进程,将由系统的init进程负责回收相关资源。
- 线程是CPU调度的基本单位。一个进程可以包含多个线程,线程占有的系统资源很少,线程可以和同属于一个进程的其他线程共享进程所拥有的全部资源。多线程之间对内存共享,线程间通信可以直接基于共享内存来实现,比多进程之间通信更方便。多线程之间切换不需要切换虚拟内存空间、文件描述符等,所以线程的上下文切换也比多进程轻量。
- 互斥锁
- 读写锁(读时共享,写时互斥)
- 条件变量
- 信号量(互斥锁的升级版)
- 自旋锁(可以避免进程或线程上下文的开销)
- 文件描述符表(打开的文件)
- 进程用户ID和进程组ID
- 进程的内存地址空间
- .text代码段
- .data数据段
- .bss
- 堆区
- 全局变量
- 静态变量
- 每种信号的处理方式
- 进程的当前目录
- 线程栈
- 寄存器的值
- 线程ID
- 错误返回码errno变量
- 线程信号屏蔽字
- 线程优先级
- 一般不同任务间需要大量的通信,使用多线程的场景比多进程多。IO密集型。
- 但是多进程有更高的容错性,一个进程的崩溃不会导致整个系统的崩溃,在任务安全性较高的情况下,采用多进程。CPU密集型。
- 进程更安全,一个进程完全不会影响另外的进程。
- 进程间通信比线程间通信的性能差很多,线程切换开销更低。
- 进程是资源分配的基本单位。一般由代码段,数据段和进程控制块组成。进程控制块记录了进程的ID,打开的文件描述符表,对信号的处理方式等。
- 线程是进程的一个实例。一个进程中可以有多个线程。多个线程间共享进程的内存地址空间。所以线程间的同步就很方便,如条件变量,信号量,套接字。线程独占的资源很少,包括寄存器、栈、错误返回码、线程优先级等。
- 总的来说:进程拥有独立的地址空间,所以一个进程的崩溃不会影响另一个进程。安全性较好。同时进程的通信方式开销比较大,包括共享内存,消息队列,管道,信号量,信号,套接字。大量通信用线程。安全性较高用进程。IO密集用协程。
- 协程是轻量级线程。传统的进程和线程由操作系统去管理,而协程是用户态的。在golang中的GMP模型中,每个goroutine对应一个内核级线程,通过processor去进行调度,当本地队列不足时,到全局队列中获取goroutine。当其中一个goroutine阻塞时,goroutine将让出CPU。
- 阻塞的文件描述符为阻塞IO
- 非阻塞的文件描述符为非阻塞IO
- 同步IO向应用程序通知的是IO就绪事件。要求用户代码自行执行读写操作,将数据从内核缓冲区读入用户缓冲区。
- 异步IO向应用程序通知的是IO完成事件。由内核来执行IO读写操作。在linux环境下,aio.h头文件定义的函数提供了对异步IO的支持。
- reactor 同步IO模型通常用于实现reactor模式。要求主线程只负责监听文件描述符是否有事件发生,有的话就立即将该事件通知工作线程。
- proactor 异步IO模型通常用于实现proactor模式。也可以用同步IO模拟出proactor模式。proactor将所有IO操作都交给主线程和内核来处理,工作线程仅仅负责业务逻辑。
- 主线程往epoll内核事件表中注册socket上的就绪事件。
- 主线程调用epoll_wait等待socket上有数据可读。
- 当socket上有数据可读时,epoll_wait通知主线程。主线程将socket可读事件放入请求队列。
- 睡眠在请求队列上的某个工作线程被唤醒,它从socket读取数据,并处理客户请求,然后往epoll内核事件表中注册该socket上的写就绪事件。
- 主线程调用epoll_wait等待socket可写。
- 当socket可写时,epoll_wait通知主线程。主线程将socket可写事件放入请求队列。
- 睡眠在请求队列上的某个工作线程被唤醒,它往socket上写入服务器处理客户请求的结果。
-
半同步半异步模式:同步线程用于处理客户逻辑,异步线程用于处理IO事件。异步线程监听到客户请求后,就将其封装成请求对象并插入到请求队列中,请求队列将通知某个工作在同步模式下的工作线程来读取并处理该请求对象。半同步半反应堆模式采用的事件处理模式是reactor模式:它要求工作线程自己从socket上读取客户请求和往socket写入服务器应答。半同步半反应堆也可以模拟proactor模式,即由主线程来完成数据的读写。在这种情况下,主线程会将应用程序数据,任务类型等信息封装为一个任务对象,然后将其插入请求队列。工作线程从请求对象取得任务对象以后,可直接处理无需执行读写操作。 问题:主线程和工作线程共享请求队列需要加锁。工作线程较少时可能产生请求任务堆积。
-
领导者追随者模式
-
在IO模型中,同步和异步区分的是内核向应用程序通知的是何种事件,是就绪事件还是完成事件,以及该由谁来完成IO读写,是应用程序还是内核。
-
在并发模式中,同步指的是程序完全按照代码序列的顺序执行。异步指的是程序的执行需要由系统事件来驱动。常见的系统事件包括中断和信号。
- 虚拟地址是操作系统管理内存的一种方式。方便不同进程使用的虚拟地址彼此隔离。方便物理内存中不相邻的内存在虚拟地址上视为连续的来使用。虚拟地址和物理地址的映射是通过MMU页表进行的。虚拟内存对实际内存有保护作用。
-
内核态用于执行一些特权指令,比如中断机制,原语,进程管理等,目的是保护系统程序。
-
陷入内核态的三种情况:
- 系统调用
- 中断
- 异常
-
中断和异常都可以实现用户态到内核态的切换,是通过硬件实现的。
-
中断:IO完成,定时器时钟中断。
-
异常:越界,溢出,缺页,异常不能被屏蔽,一旦出现应立即处理。
- 抢占式:立马停止。
- 非抢占式:时间片用完或者等待资源时,再调用另一个进程。
- 先来先服务
- 短作业优先
- 优先级调度
- 时间片轮转
- 高响应比优先
- 管道是一种伪文件,实质为内核缓冲区大小为4K,内核借用环形队列实现。
- 管道是半双工的,数据只能单向流动,不可重复读取,只能用于有血缘关系的进程。
- 大端字节序:网络字节序(高位存低位)
- 小端字节序:主机字节序,现代PC机采用小端字节序(低位存低位,高位存高位)
比如0x1f3f5f7f 地址0x1000 0x1001 0x1002 0x1003
大端法:7f存在0x1003 5f存0x1002 3f存0x1001 1f存0x1000 低存高
小端法:7f存在0x1000 5f存0x1001 3f存0x1002 1f存0x1003 低存低
socket 创建socket文件描述符 bind 绑定IP和端口号 listen 设置监听连接数的上限
accept 阻塞等待数据到来 read/write 处理客户端的业务 close 关闭文件描述符
socket 创建套接字文件描述符 bind 绑定IP和端口号(也可以隐式绑定) connect 尝试连接服务器(建立TCP连接) write/read 处理服务器端的业务
- 同步阻塞IO
- 同步非阻塞IO
- IO多路复用
- 信号驱动IO
- 异步IO
- 协程是一种用户态的轻量级线程。
- 协程的开销远远小于线程的开销。
- 协程是一种比线程更加轻量级的存在,一个线程可以拥有多个协程。
- 无论是进程还是线程,都是由操作系统所管理的。而协程不是被OS所管理,而完全是由程序所控制(也就是在用户态执行)。
- 信号是一种不精确通信。
- 常用的信号有SIGKILL 9 无条件终止信号
- SIGSEGV 11 无效存储访问
- SIGPOLL 8 轮询事件信号。
- 信号有三种处理方式:忽略,捕获,默认。
- kill命令向进程发送信号
-
因为资源调度的方式不合理或者资源的稀缺性,导致进程间的相互等待。
-
死锁的四个必要条件:
- 互斥条件
- 请求和保持条件
- 环路等待条件
- 不可剥夺条件
- 死锁的预防只要破坏死锁产生的四个必要条件。通常采用预先静态分配方法,可以破坏请求和保持条件。
- 死锁的避免:采用银行家算法,只要系统处于安全状态,系统便可避免死锁。
- 死锁的解决:撤销进程,剥夺资源。
- 死锁检测:依赖关系图是否有环。
- fork函数用来创建子进程 一次调用,两次返回。在父进程中返回子进程的PID,在子进程中返回0
- 在程序中调用另一个可执行程序,但是进程ID不改变。
-
服务器通常需要处理三类事件:IO事件,信号及定时事件。
-
事件处理模式:reactor和proactor
-
同步IO模型通常用于实现reactor模式
-
异步IO则用于实现proactor模式
- 将磁盘空间映射到进程空间,使进程可以采用指针的方式操作这段内存,而不用调用read和write函数。提高了读写的效率,同时也可以实现进程间的通信。
- 底层将数据准备好后,内核会给进程发送一个异步通知信号SIGIO29通知进程,然后进程调用信号处理函数去读数据,没准备好,数据就忙自己的事情。
- open/read/write操作文件描述符三连
ret = read(fds[0], buf, sizeof(buf)); //返回读取到的字节数
write(fds[1], str, strlen(str));
int pid = fork()
if(pid > 0){} 父进程
else if(pid == 0){}子进程 -1表示失败
- 本质是内核缓冲区,使用环形队列实现,默认大小是4k。
- 半双工,单方向流动数据
- 一读一写
itn fds[2];
int ret = pipe(fds);
0表示成功 -1表示失败
-
fork出子进程后,希望子进程执行另外一个可执行程序。
-
调用exec函数以后,进程的用户空间数据(.text和.data)被新程序所替换,进程id不改变。
execlp("ls","随便什么都可以","-l","-a",NULL); //最后一定要传入NULL用于指示不定长参数的结尾
-
信息量比较小,开销小,不可靠,有时延。每个进程收到的所有信号,都是由内核负责发送的,内核处理。
-
产生信号:kill raise abort 段错误 除0 按键
-
信号机制:信号屏蔽字(阻塞信号集),未决信号集。放在PCB进程控制块中。
-
未决信号集bitmap
-
处理方式:忽略/捕获/默认
-
注册信号捕捉函数
-
SIGSEGV
-
SEGKILL
-
SIGALRM
-
SIGINT
void my_handler(int sig){
printf("hello world!\n");
abort(); //终止进程
}
int main(){
signal(SIGALRM, my_handler); //注册信号捕捉函数
alarm(1); //定时
for(int i = 0; ; i++){
printf("%d\n",i);
}
return 0;
}
定时器,只能支持到秒级,与进程状态无关,进程挂起时也在计时中。
每个进程都有且只有一个唯一个定时器,二次调用覆盖原来的定时器,返回原来定时的剩余时间。
alarm(5) //5秒后程序终止
alarm(0) //取消闹钟
setitimer微秒级定时
- 分配策略
- 首次适应:地址递增最先满足条件的内存
- 最佳适应:容量递增最先满足条件的内存
- 循环适应:首次适应的优化版,从上次分配的位置开始。
- 固定分区会产生内部碎片,动态分区会产生外部碎片。分页使用相等的页面大小,内存和进程都进行划分,由于页面较小,所以每个进程平均产生半个页的内部碎片。分页管理是从计算机的角度考虑设计的,页面的大小对用户是透明的。分段管理是从用户的角度去设计的,为了满足编程的方便,信息保护和共享等多方面的需要。
- 逻辑地址:页号P + 页内偏移量W
- 物理地址:b*L + W
- 页表项:页号P + 物理内存的块号b
- 页面大小必须相等
- 页表:由页表项组成。
- 地址转换机构:逻辑地址---快表TLB---页表---物理地址
- 两次访问主存,如果使用了快表就可以实现只访问一次主存拿数据和指令
- 多级页表:减少页表所占的连续内存空间。
- 逻辑地址:段号S + 段内偏移量W
- 段表:由段表项组成。
- 段表项:段号S + 段长L + 起始地址b
- 物理地址:b + W (需要判断b是否小于等于L)
- 逻辑地址:段号S + 页号P + 页内偏移量W
- 每个进程有一个段表,每个段有一个页表。
- 段页式存储既有分页也有分段的优点,采用分段来分配和管理用户地址空间,用分页来管理物理存储空间,但它的开销最大。
- 局部性原理:时间局部性和空间局部性。
- 部分页面装入内存
- 缺页中断机制
- 页面置换算法:
- 最佳置换算法
- 最近最少使用
- 先进先出
- 时钟算法
- 抖动:页面频繁调度
- Belady问题:FIFO中随着物理块的增加,缺页故障反而增加。
- 先来先服务:属于不可剥夺算法,对长作业有利,对短作业不利,有利于CPU繁忙,不利于IO繁忙。
- 短作业优先:对于长作业会产生饥饿,即长期不被调度。
- 优先级调度:从就绪队列中选择优先级最高的进程,分配CPU
- 高响应比优先调度:主要用于作业调度,克服了饥饿状态,兼顾了长作业。
- 时间片轮转:主要适用于分时系统,剥夺式算法
- 多级反馈队列
- 一个进程打开的文件描述符上限是1024
- ulimit -u 获取文件描述符个数
- im /etc/security/limits.conf 打开该文件修改进程的文件描述符上限
- 查看系统的文件描述符上限 cat /proc/sys/fs/file-max
- select的数组大小要修改只能重新编译内核
- 保护的临界数据处理的时间尽可能短,否则很浪费CPU
- 在多CPU的系统上,自旋锁才有价值。
- 执行自旋锁内部代码时不能主动让出CPU,否则会引起死锁等问题。
- 自旋锁内是不允许存在信号量操作的,反之,信号量保护的代码里面是可以有自旋锁操作的。
- buddy system
- 基于2的幂次开辟内存。
- 释放时自动合并成更大的空间。
- 是linux系统的一种内存分配机制。它是针对经常分配和释放的对象。slab分配器基于对象进行管理,每次从slab列表中分配一个同样大小的单元。释放时不直接返回给伙伴系统,而是保存在slab列表中。
- 32位机两级页表
- 64位机四级页表
- 子进程死了,父进程没有进行回收。waitpid回收指定进程。
- 父进程死了,子进程仍然存活。系统会让init进程领养孤儿进程。
- 内核区
- 用户区
- 环境变量
- 命令行参数
- 栈区
- 共享区【加载动态链接库或者建立内存映射】
- 堆区
- .bss段未初始化变量
- .data段初始化数据
- .text段代码段
- 0-4k的受保护区域NULL
- IO系统调用:read/write用户态没有缓冲区
- 标准IO库:fread/fwrite用户态有缓冲区
- 零拷贝:是指不用将数据从内核态到用户态的频繁拷贝,节省了CPU的周期和内存。
- sendfile发送文件时只需要一次系统调用
- 将数据从磁盘读取到内核缓冲区
- 在socket buffer中记录内核缓冲区的位置和偏移量
- 根据socket buffer中的记录将数据copy到网卡设备中
- 避免了内核态到用户态的频繁拷贝,减少系统调用的次数,降低了上下文切换的开销。
- mmap:将磁盘上的物理空间映射到共享区,避免了read和write的系统调用,可以直接通过指针操作文件。
- 页表目录项更新
- 寄存器中的数据
- 切换内核态栈
- 刷新TLB
- L1到L3的缓存间接失效,直接访问内存。
- 预编译
- 编译
- 汇编
- 链接
- 核心函数是load_elf_binary,它通过读存放在ELF文件中的信息为当前进程建立一个新的执行环境。
- elf是静态文件,程序执行时所需要的指令和数据必需在内存中才能够正常运行。load_elf_binary 就是将elf里的指令和数据加载到内存中。
- 进程的创建
- 创建一个独立的虚拟地址空间(先共享父进程的页框,即COW机制)
- 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系(在子进程需要加载新elf文件时)
- 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行
- 完整流程
- shell首先fork一个子进程,子进程通过execve系统调用启动加载器
- 读取elf文件头
- 调用load_elf_binary函数加载代码段和动态链接库,分配虚拟地址空间。
- 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行。
- 往磁盘上写入数据,调用write函数,触发系统调用sys_write,写入内核缓冲区,可以使用fsync强制刷新到磁盘。
- 创建
- 删除
- 读写
- 挂载
- 为了屏蔽底层文件系统和驱动程序等细节,提供友好的统一用户接口
- EXT4 块文件系统
- NTFS 日志文件系统
- FAT32 proc sysfs
- swap 虚拟内存的文件系统
- 日志文件系统可以在系统发生断电或者其它系统故障时保证整体数据的完整性
打开文件描述符表,简称为打开文件表。 当进程打开一个文件的时候,虚拟文件系统会创建文件的一个打开实例:file结构体,然后在进程的打开文件表中分配一个索引inode,这个索引称为文件描述符,最后把文件描述符和file结构体的映射添加到打开文件表中。 file结构体包括:
- inode对象指针
- 文件偏移量
- 访问模式
打开文件表是PCB进程控制块的一部分,进程控制块是个task_struct结构体包括:
- 进程号
- 打开文件描述符表
- 等等
文件系统应具有以下功能
- 完成文件存储系统的管理,即分配空间和回收空间。
- 实现文件名到物理地址的映射。
- 实现文件和目录的建立,读写管理。
- 向用户提供有关文件和目录操作的接口。
- 用户空间
- 内核空间
- 虚拟文件系统VFS
- 块设备文件系统、内存文件系统、闪存文件系统
- 页缓存 Page Cache
- 块设备层
- 块缓存
- IO调度器
- 块设备驱动程序
- 硬件
- 机械硬盘、固态硬盘
- 闪存
- NOOP 先来先服务
- CFQ 完全公平 该算法为每个进程分配一个时间窗口,在时间窗口内允许进程发出IO请求
- Deadline 每个IO请求有一个最后执行期限。
- 从进程虚拟地址空间映射到内存地址空间。
- 页面大小相同,内存和进程都进行划分。由于页面较小,每个进程平均产生半个页的内部碎片。
- 逻辑地址:页号 + 页内偏移
- 地址转换:逻辑地址---快表TLB---页表----物理地址
- 两次访问主存,如果使用了快表就可以实现只访问一次主存拿数据和指令
- 对用户友好,屏蔽底层细节。
- 管理和存储文件
- 文件系统的主要功能:安装,卸载,创建,删除,读写。
- 文件系统的结构分为三层:用户层,内核层,硬件层。
- 用户层的函数:fopen fclose fwrite fflush fread fseek
- 内核层函数:mount unmount reade write open lseek fsync fdatasync
- 内核中有个页缓存:page cache
- 一个存储设备上的文件系统,只有挂载到内存中目录树的某个目录下,进程才能访问这个文件系统。
- a.out是ELF文件格式
- linux每个程序都会运行在一个进程上下文中,这个进程上下文有自己的虚拟地址空间。这个进程是由shell进程fork出来的子进程。
- 分页内存管理
- 内存和进程划分为大小相等的页面,通常是4k,页面大小是对用户透明的,分页是从计算机的角度去考虑的,为了减少内存碎片,由于页面较小,每个进程平均产生半个内存碎片。
- 分段
- 分段是从用户的角度考虑,为了满足编程的方便,信息保护和共享等方面的要求。
- 段页
- 段号+页号+页内偏移
- 基于局部性原理,使用页面置换算法,是内存的可用空间更大。
- 进程隔离
- 内存的管理
- 不连续的空间
- 调用内核提供的系统调用read/write
- 调用glibc库封装的标准IO流函数fread/fwrite。用户空间有缓冲区,能减少系统调用的次数,提高性能。
- 创建基于文件的内存映射,把文件的一个区间映射到进程的虚拟地址空间,然后直接读内存。mmap避免系统调用,性能最高。
- 轮询
- 中断 CPU字节干预IO
- 直接存储器访问DMA 一个块数据 干预一次
- 通道 一组块数据 干预一次
- 目的:减少CPU对IO的干预,提高其处理数据的效率。
- 外设速度远远慢于CPU的速度,所以需要外设资源准备好以后,利用中断机制主动通知操作系统。
- 内部中断/异常:CPU执行指令期间检测到非法的条件(除零,地址访问越界)
- 硬中断/外中断/中断:IO中断,时钟中断,信号中断产生的时间不确定。
- 软中断:应用程序使用系统调用而引发的事件。
- 中断描述符表
- os中预先设置一些中断处理函数,当CPU接收到中断时,会根据中断号去查对应的处理函数,中断向量表就是记录中断号与中断函数映射关系的表。
- 中断机制是为了弥补CPU速度和外设速度数量级差异的机制,它的核心是中断向量表。
- 超级块 super block
- 索引节点 iNode
- 目录项 dentry
- 文件 file
- 描述文件系统的总体信息,挂载文件系统时在内存中创建超级块的副本。
- 一个文件系统只有挂载到内存中目录树的一个目录下,进程才能访问这个文件系统。
- 每次挂载文件系统,VFS会创建一个挂载描述符:mount结构体,并读取文件系统的超级块,在内存中创建超级块的副本。
- 每种文件系统的超级块的格式不同,需要向VFS注册文件系统类型,并实现mount方法用来读取和解析超级块。
- ext4----register----mount-----super block
- 每个文件对应一个索引节点,每个索引节点有个唯一的编号。当内核访问存储设备上的一个文件时,会在内存中创建索引节点的副本:结构体inode
- 一个块由多个扇区组成4k=8sector
- inode文件的元信息:创建者,创建日期,文件的大小。
- 查看系统中iNode的用量df -i
- df -h 输出文件系统分区情况
- du -h 文件、目录
- 文件系统把目录看作文件的一种类型,目录的数据是由目录项组成,每个目录项存储一个子目录或文件的名称以及对应的索引节点号**。
- 当进程打开一个文件的时候,VFS会创建文件的一个打开实例:file结构体,然后在进程的打开文件描述符表中分配一个索引,这个索引被称为文件描述符,最后把文件描述符和file结构体的映射添加到文件描述符表中。