一 权限管理¶
1.1 文件类型和访问权限¶
- d,rwx,r-x,r-x:文件类型,user,group,others
- 类型有:d文件夹,-普通文件,l软连接,b块设备文件(磁盘、光驱),p管道文件,c字符设备文件(屏幕等串口设备),s套接口文件
- 对于文件和目录:r(4,读/浏览),w(2,写/删除移动),x(1,执行/具有进入目录的权限),-表示不具有该权限
- chmod 权限 文件名:如chmod u+w /home/abc.text
二 进程¶
1.1 基本概念¶
- 课本概念:一个执行的程序
- 内核概念:操作系统资源分配的基本单位。
- 实质上:对某种结构体的描述,再加以组织。
1.2 PCB-task struct¶
Linux中进程控制块PCB-------task_struct结构体结构 - 童嫣 - 博客园 内容分类: - 标示符(PID/tgid): 描述本进程的唯一标示符,也叫任务ID,用来区别其他进程线程。 - 亲缘关系: - 状态: 任务状态,退出代码,退出信号等。通过 - 进程权限: - 进程调度: 相对于其他进程的优先级,调度器类和调度实体,调度策略,CPU使用等。 - 信号处理: - 程序计数器: 程序中即将被执行的下一条指令的地址。 - 内存指针: 包括程序代码和进程相关数据(mm_struct)的指针,还有和其他进程共享的内存块的指针 - 上下文数据: 进程执行时处理器的寄存器中的数据(休学例子,要加图CPU,寄存器)。 - 内核栈:包括了thread_info 和 pt_regs。和不同体系结构有关,上下文主要就是保存在这里面 - 文件系统、I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。 - 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
1.3 组织进程 TODO¶
可以在内核查看,TODO - 双向循环链表:进程的组织意识依靠双向循环链表完成的。
1.4 系统调用fork()创建进程¶
- fork两个返回值:0(child)和子进程pid(father)
- 写时拷贝(COW)技术父子:进程共享代码,任意一方写入时,单独分配内存空间
- 通常需要if进行分流。
1.5 进程状态 TODO¶
RSDTtXZ Z(zombie)僵死进程:子进程退出而父进程没有读到子进程退出的返回代码就会产生。 - 始终保持终止状态在进程表,并且一直等待父进程读取退出状态代码。 - PCB也要一直维护,创建又不回收会造成内存泄漏
1.6僵死进程和孤儿进程 TODO¶
1.7 进程优先级¶
三 环境变量 TODO¶
四 程序地址 空间¶
1.1 分页,虚拟空间地址¶
场景:fork一个进程子进程,子进程先输出地址和值,父进程输出地址和值,会发现地址竟然完全相同但值不同。 - 虚拟地址:巨大,分页机制,磁盘 - 物理地址:CPU
1.2 进程调度队列¶
- 两个模块:分别管理活跃进程和过期进程,结构完全一样
- nr_active:总共有多少个运行状态的进程
- bitmap(5):140个优先级,用5* 32个比特位是否为空。
- queue(140):双向队列。多个CPU需要考虑进程个数的负载均衡问题
- 140中:100~139为普通优先级,可以自己设置
- 0~99为实时优先级,可以暂时不考虑。
- 调度方法:
- 过期队列都是时间片消耗结对束的进程。
- 活动队列上进程处理完后,过期队列的进程进行时间片重新计算。
- 交换active和expired指向的内容,就相当于有一批新的互动进程!!(O1算法)
五 进程控制¶
5.1 进程创建fork(前面有讲)¶
5.2 进程终止 _ exit()¶
进程常见的退出方法有:
- 从main返回:return n相等于exit(n),main的返回值即exit的参数。
- 调用exit(int status):调用定义的清理函数,关闭流、冲刷缓存。再调用_exit
- 调用_exit(int status)
:
- 以及异常退出(ctrl+c,信号终止)
5.3 进程等待wait/waitpid¶
wait(int*status);
:成功返回pid,失败返回-1.waitpid(pid_t pid, int *status, int options)
:- pid=-1,等待任意子进程。pid>0,等待其进程id与pid相等的子程序。
- status:查看进程是否退出\查看进程退出码
- options:WNOHANG--子进程没结束则返回0不等待。 任意时候调用两个代码,且子进程存在且正常运行,进程可能阻塞,否则立刻返回。 如果不存在该子进程,立刻出错返回。
5.4 进程程序替换exec()¶
调用exec:进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。 - 并不创建新进程,所以调用exec前后该进程的id并未改变 - (只有出错返回值)调用成功加载新的程序,从启动代码开始执行不再返回。调用出错则-1.
5.5 思考函数和进程之间的相似性:¶
exec(fork)/exit就像call/return。 被调用的函数执行一定的操作,然后返回一个值。每个函数都有他的局部变量,不同的函数通过call/return系统进行通信。Linux鼓励将这种应用于程序之内的模式扩展到程序之间。
六 基础IO¶
- 复习C文件IO相关操作
- 认识文件相关系统调用接口
- 认识文件描述符,理解重定向
- 对比fd和FILE,理解系统调用和库函数的关系
- 理解文件系统中inode的概念
- 认识软硬链接,对比区别
- 认识动态静态库,学会结合gcc选项,制作动静态库
1.IO相关操作和系统调用¶
- open打开:返回文件描述符或-1(失败),文件名,(从头)r读、r+读写、(覆写)w、w+、(追加)a、a+。w+、a+自带创建。
- read读:fd,字符缓冲区,长度,返回读取长度
- write:字符串,长度,输入输出流stdin,stdout,stderr的fd,对应0、1、2。
- close:关闭fd对应文件
2.理解文件描述符¶
- filestruct、fd本质。0、1、2的关闭、fd的分配规则。
- 重定向:>,>>,<
- 系统调用:dup2
3.文件元数据与inode¶
- 模式、硬链接数、文件所有者、组、大小、最后修改时间、文件名(stat可查看更多信息)
- 磁盘:先理解磁盘的块设备存储,硬盘分区block,存储inode信息和块信息、结构信息、数据区
- inode:创建新文件的方式(找空闲i节点存储文件信息,存储数据到磁盘块abc、inode记录块abc分配情况,添加inode和块信息abc到目录文件)。所以磁盘找信息实际上是找inode。
- 硬链接(inode引用,增删计数)、软链接(名字引用)
4. 动态库和静态库¶
- 静态库(.a):编译链接阶段直接添加到可执行文件中,快但大。
- 动态库(.so):运行时才去链接,执行文件小,多程序共享
七 进程间通信(IPC)¶
省流¶
- 匿名管道、命名管道:最古老
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
1. 管道(本质为文件)¶
- 最古老,数据流,本质为文件。生命周期随进程,半双工。
- 匿名管道:pipe(fd{2})创建,0为读端,1为写端。fork后各自关掉不用的描述符
- 读写规则:无数据可读时,管道满时。一端文件描述符被关闭的情况。
- 父子通信,通过两个管道实现。
- 命名管道:FIFO文件管理,解决具有只有亲缘关系的管道通信问题,实际也是文件。
- ![[Pasted image 20250403110818.png]]
2.共享内存(最快)¶
- 最快的IPC形式,数据传递不再涉及到内核,不涉及系统调用。
3. 消息队列(生产者消费者模型)¶
- 实际上,可以说是异步的远程调用,面向数据。一发一收一调用。
- 案例对比:生产者消费者模型。
4.信号量(同步互斥)¶
概括:进程互斥、临界资源互斥资源、临界区
- 由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥
- 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。
- 在进程中涉及到互斥资源的程序段叫临界区
八 进程信号¶
我们在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的。
- 终端按键产生:键盘按键ctrl+c,触发硬件中断,发送信号SIGINT,执行终止程序的相关程序。
- 系统函数产生:如kill产生SIGSGEV
- 软件条件产生:SIGPIPE,SIGALARM
- 硬件条件产生:如除0导致运算单元异常发送SIGFPE,非法内存地址段导致MMU错误发送SIGSEGV。 唉记点常用的就差不多得了:
- SIGINT:终止进程
- SIGQUIT:终止进程并保存
- SIGSEGV:产生段错误(比如野指针异常),/kill。
- SIGPIPE:软件产生的信号,管道
-
SIGALARM:告诉内核seconds秒后发送,默认终止进程。
-
信号捕捉:signal(信号类型,函数指针)。捕捉到就调用回调函数。
九 多线程¶
- 了解线程概念,理解线程与进程区别与联系。
- 学会线程控制,线程创建,线程终止,线程等待。
- 线程分离与线程安全概念。
- 线程同步。
- 使用互斥量,条件变量,posix信号量,以及读写锁。
- 基于读写锁的读者写者问题。
9.1 线程是什么¶
- 一个程序的执行路线,“一个进程内部的控制序列”
- 在Linux中,CPU看到的还是PCB(task_struct),所以说是“轻量级线程”
- 多个线程共享同一地址空间。
9.2 线程优缺点¶
- 优点:创建代价小,切换时OS做的东西少,资源占用小,多处理器利用,等待慢速IO过程中,程序可执行其他任务或IO密集时,同时等待不同IO。
- 缺点:计算密集型的性能损失(线程数量太多,同步和调度开销),健壮性较低,缺乏访问控制,编程难度提高。
- 线程异常:单线程崩溃(如除0)会导致整个进程崩溃,触发信号机制。其他线程一起退出。
9.3 Linux 进程VS线程¶
- 线程自己的数据:线程ID,一组寄存器,栈,errno,信号屏蔽字,调度优先级。
- 共享以下进程资源与数据:文件描述符表、每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)、当前工作目录、用户id和组id。
- pthread_t是什么?该类型的ID,就是进程地址空间的一个地址 ![[Pasted image 20250404100152.png]]
9.4 线程等待¶
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。创建新的线程不会复用刚才退出的线程的地址空间
- pthread_join:挂起等待,直到对应id的线程结束
9.5 线程互斥¶
- 背景概念:临界资源(共享资源),临界区(访问临界资源的代码),互斥(保证只有一个执行流进入临界区),原子性(不会被任何调度机制打断)
- 互斥量:一般线程有局部变量和共享变量,共享变量在多个线程并发执行会带来些问题,因此引入了互斥量。一般解决该问题需要三个点:代码必须互斥,只允许一个线程进入临界区,如果线程不在临界区执行则不能阻止其他线程进入临界区。本质上就需要一把锁,叫互斥量。
--
不是原子操作:分为load,update,store。即存寄存器,减,取寄存器三个步骤
9.6 可重入/线程安全¶
- 线程安全:多线程并发执行同一段代码,不会出现不同的结果
- 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,称之为重入。
- 可重入,不可重入:一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题。
- 常见线程不安全:不保护共享变量的函数,函数状态被调用时发生变化,返回指向静态变量的指针,调用线程不安全函数的函数。
- 常见线程安全:只读无写入,原子操作,多线程切换接口无二义性
- 常见不可重入:malloc和free(全局链表管理堆),标准IO,可重入函数体使用静态数据结构。
- 常见可重入:无全局、静态变量,不用malloc和new,不返回静态、全局数据,使用本地数据。
- 可重入即线程安全的,不可重入有可能线程安全,全局变量不可重入也不线程安全。
- 可重入函数属于线程安全函数的一种
9.7 锁概念--死锁¶
- 死锁概念:多个进程线程占有不释放的资源,但又互相申请其他进程的资源导致永久等待状态
- 死锁四个必要条件:互斥,请求保持(当前),(使用完前)不剥夺,循环等待
- 避免死锁:破坏四条件,加锁顺序一致,资源一次性分配,银行家算法。
9.8 Linux线程同步¶
- 条件变量:当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。这种情况下需要使用条件变量。
- 同步概念与竞态条件:
- 同步:数据安全情况下,按照顺序访问临界资源,有效避免饥饿问题
- 竞态:时序问题,导致程序异常。
- 条件变量为什么需要互斥量:条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。
- 条件变量使用规范:解锁和等待必须是原子操作,代码暂时就不放了。
9.9 生产者消费者模型¶
- 解耦、支持并发、支持忙闲不均
- queue模拟阻塞队列
9.10 POSIX信号量semaphore¶
- 初始化0,销毁、等待(-1),发布信号量(+1)。
- 环形队列生产消费模型:数组+模运算模拟环形,计数器辅助计算满或空(或者预留一个空的位置)--可以用信号量代替计数器在多线程上模拟。
9.11线程池¶
- 维护着多个线程,等待着监督管理者分配可并发执行的任务。
- 避免了在处理短时间任务时创建与销毁线程的代价,够保证内核的充分利用,还能防止过分调度。
- 应用场景:大量线程,且任务时间短/性能要求苛刻/接受突发性的大量请求。
- 示例:创建固定数量线程池,
9.12线程安全单例模式¶
- 饿汉,懒汉,懒汉(双检查锁),懒汉(call once),懒汉(c11)
- 注意双检查锁+volatile防止过度优化
9.13 STL,智能指针是否线程安全?¶
STL并非,主要为了挖掘性能。 uniqueptr安全,shadredptr涉及到引用计数,但是用了原子操作CAS也安全。
9.14其他常见的锁¶
- 悲观锁:担心被其他线程修改,取数据前先加锁。
- 乐观锁:乐观认为不会被修改。但是在更新前会判断有没有被修改(通过版本号机制和CAS)
- 原子操作CAS:需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
- 自旋锁
- 非公平锁
- 公平锁
9.15读写问题¶
- 读写锁:读得多,加锁降低效率,解决多读少写问题。
- 写独占,读共享,读锁优先级高。
- 三种请求情况:无锁(可读写)、读锁(可读不写)、写锁(不读不写)
十 网络编程套接字¶
10.1 前置知识¶
- 源IP地址、目的iP地址、端口号、进程ID(pid)、TCP协议和UDP协议、网络字节序(大小端)
10.2 Socket编程接口¶
- socket(domain,type,protocol):TCP\UDP,服务端客户端
- bind(socket,address,len):TCP、服务端
- listen(socket,backlog):TCP、服务端
- accept(socket,address,len):TCP、服务端
- connect(sockfd,addr,len):TCP、服务端
10.3 Socket的address结构¶
- 地址类型、端口号、IP地址
10.4 TCP和UDP实现(略)¶
10.5 多个连接¶
- 多线程版本-> 线程池版本
10.7 TCP协议通讯流程¶
![[Pasted image 20250405010036.png]]
服务器初始化: - 调用socket创建FD - 调用bind,将当前的FD和ip/port绑定在一起;如果这个端口已经被其他端口占用了,就会bind失败 - 调用listen,声明当前这个FD作为一个服务器的FD,为后面accept做好准备 - 调用accept,并阻塞,等待客户端连接。
建立连接的过程(三次握手): - 调用socket,创建fd - 调用connect,向服务器发送连接请求 - connect会发出SYN段并阻塞等待服务器应答(第一次) - 服务器收到客户端的SYN,会应答一个SYN-ACK表示“同意建立连接”(第二次) - 客户端收到SYN-ACK后会从connect()返回,同时应答一个ACK段(第三次)
数据传输过程 - TCP协议提供全双工的通信服务(双方同时写数据)。 - 服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待; - 读写数据:这时客户端调用write()发送请求给服务器,服务器收到后read返回,对客户端的请求进行处理,服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求 - 如此循环
断开连接的过程: - 如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次); - 服务器收到FIN后,进入CLOSE_WAIT状态,会回应一个ACK,同时read会返回0(第二次) - read返回之后,服务端就知道客户端关闭了连接,那么也调用close(),服务器会再次向客户端发送FIN(第三次) - 客户端收到FIN后,进入TIME_WAIT状态,再返回一个ACK给无服务器(第四次)
在学习socket API时要注意应用程序和TCP协议层是如何交互的。
十一 网络基础¶
10.1 HTTP协议¶
- URL构成
- URLencode和decode
10.2 TCP,UDP构成¶
- UDP:源端口+目的端口+UDP报文长度+检验和+数据
- TCP:源端口+目的端口+序号+确认序号+报头长度+6位标志位+窗口大小+检验和+紧急指针+选项+数据(没有报文长度)
10.3 ACK确认应答机制(TCP)¶
TCP每个字节数据都有编号,ACK带有确认序列号,告诉发送者下次从哪里发
10.4超时重传机制¶
收不到ACK,倍数时间时间发一次,一定次数后强制关闭连接
10.5连接管理机制¶
前面省略.... TIME_WAIT:客户端要等待一个最大报文生存时间,才会进入CLOSE状态。所以主动结束服务端进程,并不会断开底层的SOCKET连接。
CLOSE_WAIT状态:大量的出现意味着服务端没有正确关闭,需要加上close。
10.6滑动窗口¶
- 需要开辟发送缓存区,多发多收,提高吞吐率,收一个向后移一个
- 丢包重传:
- 数据包抵达,ACK丢了,不要紧可以通过后面ACK确认
- 数据包直接丢了,三个来自服务端的重复应答(如1001),重传数据
10.7 流量控制¶
- 为什么流量控制?发送端发送太快导致接收端缓冲区被打满,继续发送会导致丢包。
- 接收端:超过承载,发送一个能接受的窗口大小值提醒发送端更新
- 发送端:如果接收端满了,窗口会置为0。此时发送端也不再发送数据,但是需要定期发送一个窗口探测数据段,便于接收端把窗口大小告诉发送端
10.8 拥塞控制¶
- 为什么需要拥塞控制?刚开始就发送大量数据,可能会引发问题。特别是当前网络状态非常拥堵。
- 慢启动:慢启动阈值前倍数增长,慢启动阈值后线性增长。每次窗口大小和流量控制发来的窗口取最小
- 超时重发:阈值减半,窗口变1,重新慢启动。
10.9 延迟应答¶
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小,即使窗口再放大一些, 也能处理过来; - 数量限制,N个包答一次 - 时间限制,超过最大延迟时间答一次。
10.10 捎带应答¶
ACK的时候顺便带个消息回去。
10.11 面向字节流¶
- 其实就是TCPscocket在内核建立了两个缓存区(收发),因此可以做到全双工
10.12 粘包问题¶
- 问题原因:TCP没有报文长度的字段,但是有序号
- 解决:明确边界,
- 定长的包:保证固定大小读就行
- 变长的包:可以在包头处定一个总长度字段,也可以在包和包之间使用明确分隔符(应用层协议,程序员自己确定)
- UDP是否存在“粘包问题”?不存在,有报文长度字段,要么不收,要么全收,不会只收半个。
10.13 UDP实现可靠传输¶
抄点TCP的机制。 - 引入序列号 - 引入ACK确认应答机制 - 引入超时重传
十一 高级IO¶
11.1 五种IO模型¶
- 阻塞IO:内核数据准备好前,系统调用会一直存在,所有套接字都是阻塞IO
- 非阻塞IO:轮询--反复尝试读写文件描述符,比较浪费CPU,特定场景用
- 信号驱动IO:内核将数据准备好的时候,发SIGIO通知应用程序IO
- IO多路转接:流程上类似阻塞IO,实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态
- 异步IO:内核在数据拷贝完成时, 通知应用程序(信号驱动是告诉应用程序何时可以开始拷贝数据) 任何IO都是:先等待(一般时间消耗最大),后拷贝
11.2 同步通信VS异步通信¶
同步:调用者者主动等待这个调用的结果,没有得到结果之前,该调用就不返回 异步:调用在发出之后,这个调用就直接返回了,所以没有返回结果。当处理结束后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.
11.3 IO多路复用之Select¶
- 参数,最大fd值+1 ,读fd集、写fd集、异常fd集、等待时间
- 集合结构:数组(位图),用位表示要监视的FD集合(如fd=5,对应00010000)
- 执行过程:加入fd,则将位图位置置为1.
- 原本只加入了fd=1、2、5,若fd=1,fd=2都发生可读事件,select返回时,set变成00000011,此时fd = 5就被清空了。
11.3 IO多路复用之poll¶
- 参数:poll函数监听结构fds,fds数组长度,超时时间
- 结构fds又分为fd,请求事件和返回事件
- 优点:poll改用一个指针替代了三个位图的方式,接口使用更方便,且没有最大数量限制。
- 缺点:和select依旧需要轮询,而每次调用poll都需要将大量pollfd拷贝到内核中。同时连接的大量客户端同一时刻可能很少处于就绪状态,随着监视的描述符数量增长,效率也会降低。
11.4 I/O多路复用之epoll¶
- 作用:是为处理大批量句柄而作了改进的poll
- epoll_create:创建句柄,用完必须close关闭
- epoll_ctl:epoll的事件注册函数,参数为epoll句柄,动作,fd,内核需要监听的事件、
- 其中这个动作op就包含:注册、修改和删除
- epoll_wait:收集在监控中已经发送的事件,参数为epfd,events数组,events大小,超时时间
11.5 epoll工作原理¶
- create创建红黑树和双链表根节点
- 这些事件都会挂载到红黑树中,重复添加的事件就可以通过红黑树高效识别
- 所有添加到epoll的事件都会与设备建立回调关系(事件发生回调方法)
- 发生的事件会被添加到rdlist双链表中
- epoll中,对于每一个事件,都会建立一个epitem结构体,记录红黑树和双向链表节点信息。句柄细腻和所属eventpoll对象,以及期待发生的事件类型
- epoll_wait检查是否有事件发生时,检查双链表中是否有opitem即可
- 不为空,则将事件复制到用户态,同时返回事件数量,时间复杂度O1 总结一下, epoll的使用过程就是三部曲:
- 调用epoll_create创建一个epoll句柄;
- 调用epoll_ctl, 将要监控的文件描述符进行注册;
- 调用epoll_wait, 等待文件描述符就绪
11.6 epoll的优点¶
- 接口使用方便:
- 数据拷贝轻量:
- 事件回调机制:
- 没有数量限制: