Liunx系统编程(纯理论分享二)

七、线程

1. 简介

是进程内的一个执行单元,是操作系统调度的基本单位(CPU 调度的最小单位)。线程共享所属进程的全部资源(如内存空间、文件句柄),但拥有自己的程序计数器、栈和寄存器状态。
线程是一段代码(函数),线程是并发执行的。

2. 线程和进程的区别

(1)资源分配
进程:是资源分配的基本单位,拥有独立的内存空间、文件描述符、寄存器等系统资源;
线程:是系统调度的基本单位,共享所属进程的资源,仅拥有少量独立资源(如程序计数器、栈)。
(2)独立性
进程:相互独立,一个进程崩溃通常不会影响其他进程;
线程:属于同一进程的线程共享资源,一个线程崩溃可能导致整个进程崩溃。
(3)切换开销
进程切换:需要保存和恢复整个进程的上下文,开销较大;
线程切换:只需保存和恢复线程的少量上下文,开销较小。
(4)通信方式
进程间通信:需要通过管道、消息队列、共享内存等特殊机制;
线程间通信:可直接通过共享内存(全局变量等)进行,更简单高效。
(5)并发能力
进程:可以实现多任务并发,但资源消耗大;
线程:轻量级,能创建更多数量实现并发,适合 I/O 密集型任务。
简单来说,进程是程序的一次执行过程,而线程是进程中的执行单元。一个进程可以包含多个线程,它们像轻量级的 "子进程" 一样协同工作,共享资源同时又能并行执行不同任务。
例:(可以把进程看做户;宅基地以户为单位分配;os按照进程来分配资源。
把线程看做口,按人头来收税。)

3. 线程的创建

主线程 main()函数
分支线程 pthread_create
注意在运行可执行文件时,添加pthread库 -l pthread
线程在创建后是并发执行的。
一个线程有且仅有一个线程函数;一个线程函数能被多个线程使用。
并发执行的线程,执行先后的顺序不确定。有可能线程1先执行,一段时间后又变成线程2执行。
(sched_yield() 让出CPU,让并发的其他线程先执行。)

八、线程同步

1. 简介

线程同步是为了解决临界数据脏问题(指多个执行单元(线程或进程)同时访问和修改临界数据,即被多个执行单元共享的资源,如全局变量、共享内存、文件等时,因操作顺序不确定而导致数据状态异常、结果不可预测的现象。)
本质是让多个线程异步访问某个区域。
线程的同步有哪些:原子锁、互斥锁、读写锁、信号量、条件变量、自旋锁。

2. 原子锁

已经被废弃了,少数的Liunx操作系统仍然能使用。
原子锁就是利用原子操作,让某个操作不可分割;原子锁只有简单的操作,不能进行复杂的业务逻辑。

3. 互斥锁

作用:最常用的线程同步机制,保证同一时间内只有一个线程能访问资源。
原理:线程访问临界区前需先获取锁(pthread_mutex_lock),操作完成后释放锁(pthread_mutex_unlock)。未获取到锁的线程会阻塞等待。
注:有个弊端,可能某个线程总是会抢占资源,有可能某个线程永远抢不到。
关键函数:pthread_mutex_init(初始化)、pthread_mutex_lock(加锁)、pthread_mutex_unlock(解锁)、pthread_mutex_destroy(销毁)。

4. 读写锁

作用:区分“读操作”和“写操作",优化读多写少的场景。一把读锁、一把写锁,读读相容、读写相斥、写写相斥。
原理:多个线程可同时获取读锁(共享),但写锁是独占的;写锁获取时,所有读锁需释放,且后续读锁需等待写锁释放。
关键函数:pthread_rwlock_initpthread_rwlock_rdlock(加读锁)、pthread_rwlock_wrlock(加写锁)、pthread_rwlock_unlock(释放锁)。

5. 条件变量

作用:让线程在特定条件满足前先阻塞起来,避免无效轮询,提高效率;收到信号后,解除阻塞。(可以是群发也可以是单独发)
如果是单独发,就是只有一个线程会收到信号并解除阻塞,一般是轮流着来;
如果是群发,每个线程都会收到信号,同时解除阻塞。
(为了弥补互斥量“不公平”的情况,条件变量必须和互斥量一起使用)
原理:线程通过 pthread_cond_wait 等待条件,同时释放关联的互斥锁;其他线程满足条件后,通过 pthread_cond_signal(单独发) 或 pthread_cond_broadcast(群发) 唤醒等待线程。
关键函数:pthread_cond_initpthread_cond_waitpthread_cond_signal(唤单独发)、pthread_cond_broadcast(群发)。

6. 自旋锁

作用:与互斥锁类似,保证同一时间只有一个线程能访问临界资源,但线程在获取锁失败时不会睡眠,而是忙等(自旋),直到锁可用。
与互斥锁的区别:
互斥锁 通知机制
如果情况有变化,可以加锁、就加锁,否则就睡眠等着;加锁的速度相对较慢,资源开销少。
自旋锁 轮询机制
在内核中不断循环尝试加锁,一旦可以加锁,就马上加上;加锁的速度非常快,资源开销多。
原理:线程通过 pthread_spin_lock 尝试获取锁,若锁已被占用,线程会持续循环检查锁的状态(自旋),不放弃 CPU 使用权。
获取到锁的线程执行临界区操作,完成后通过 pthread_spin_unlock 释放锁,此时自旋等待的线程可竞争获取锁。
关键函数:pthread_spin_init(初始化)、pthread_spin_lock(加锁,自旋等待)、pthread_spin_trylock(尝试加锁,失败立即返回)、pthread_spin_unlock(解锁)、pthread_spin_destroy(销毁)。

7. 死锁

死锁,是指两个或多个进程(或线程)在执行过程中,因互相等待对方持有的资源而陷入无限期阻塞的状态。在这种状态下,每个进程都持有一部分的资源,同时又等待其他进程释放自己所需要的资源,最终导致所有进程都无法继续推进,程序陷入了停滞。
死锁的核心条件:
(1)互斥条件
资源具有排他性,即一个资源在同一时间只能被一个进程使用(如独占锁、文件独占打开等)。
(2)请求与保持条件
进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
(3)不可剥夺条件
进程已持有的资源不能被强制剥夺,只能由进程主动释放(如锁不能被其他进程强制解锁)。
(4)循环等待条件
多个进程形成环形等待链,每个进程都在等待下一个进程持有的资源(如进程 A 等进程 B 的资源,进程 B 等进程 A 的资源)。

8. 粒度

在 Linux 系统编程中,粒度通常指的是资源锁定的范围或程度,尤其在并发编程(多线程 / 多进程)中,常用来描述同步机制(如互斥锁、信号量)对资源保护的精细程度。
粒度的核心是:锁定资源的大小:主要分两种
(1)粗粒度
  • 定义:指同步机制保护的资源范围较大,可能包含多个独立的子资源或整个数据结构。
  • 特点
    • 实现简单,减少了锁的数量和切换开销。
    • 但会导致并发度降低,多个线程即使访问不同子资源,也可能因争夺同一把锁而阻塞。
  • 示例:对整个链表加一把锁,无论线程操作链表的哪个节点(头部、尾部、中间),都需要先获取这把锁。
(2)细粒度
  • 定义:指同步机制保护的资源范围较小,仅针对独立的子资源或数据结构的一部分。
  • 特点
    • 并发度高,不同线程可同时操作不同子资源(只要它们不冲突)。
    • 但实现复杂,可能需要更多锁,且锁的管理和切换开销增加,甚至可能引入死锁风险。
  • 示例:对链表的每个节点单独加锁,线程操作某个节点时只需获取该节点的锁,不影响其他节点的操作。
在 Linux 内核中,早期的内存管理可能使用粗粒度锁保护整个内存区域,而现代内核为了支持多处理器高并发,常采用细粒度锁(如按内存页或区域分锁)。
粒度的设计是并发编程中的关键权衡,需要在并发效率实现复杂度之间找到平衡。

9. 线程池

(1)池化技术
为了在需要使用资源的时候能有资源可用,本质上是冗余,而且是可控的冗余;浪费一点资源,保障供应。
(2)五大池
连接池:数据库连接池、数据库连接
任务池:把工作封装成任务,一般是线程池(进程池、协程池)的一部分
内存池:一般是堆内存
线程池:管理线程
进程池:管理进程
协程池:管理协程(协程可以理解成轻量级的线程)
(3)线程池架构

九、计算机网络

1. 最小系统

(1)运算功能:CPU、GPU
(2)存储功能:内存、硬盘、存储芯片、U盘
(3)IO功能:主板
(4)时钟:先后顺序

2. 主机

网络中的计算机我们称之为主机,能够与其他计算机进行通信。

3. IP地址

本质是一个整数,是用来区分主机的。
ipv4:32位 ipv6:128位
windows:ipconfig Liunx:ifconfig
本地ip:127.0.0.1,在当前主机上的进程可以使用

4. C/S架构和B/S架构

C/S架构(客户端 / 服务器架构)
B/S架构(浏览器 / 服务器架构)
客户端:安装在用户设备上的应用程序,负责与用户交互
浏览器:用户无需安装软件,通过浏览器即可访问系统,负责数据展示和简单交互
服务器:高性能计算机(例如数据库服务器、Web服务器、应用服务器等等)响应客户端和浏览器的请求。

5. 计算机网络架构

6. socket

也翻译为网络套接字、网络设备描述符。
本质是整数,是网络设备在os应用层的映射,对socket进行操作就是对网络设备进行操作。
服务器的socket有两种操作:(1)接受连接(2)收发数据

十、TCP和UDP

1. TCP

面向连接(必须建立连接通道,例如打电话)的可靠传输协议,通过建立连接、确认机制等保证数据完整、有序地到达目的地,适用于对可靠性要求高的场景。
三次握手
(1)客户端向服务器发送连接请求 SYN。
(2)服务器收到客户端的连接请求并确认 ACK+SYN。
(3)客户端收到服务器的ACK+SYN,发送ACK确认。
四次挥手
(1)客户端在发送完数据后向服务器发送一个连接释放报文FIN,表明停止发送数据,但仍可以接收数据。
(2)服务器收到FIN报文,立即恢复一个ACK,通知高层应用进程,客户端→服务器方向已经释放,当服务器可能还会发送数据,进入半关闭状态;
客户端收到ACK确认信号后,会仍然等待服务器发送FIN报文。
(3)服务器发送完最后的数据,会向客户端发送FIN报文。
(4)客户端收到FIN报文,发出确认信号ACK,经过一定时长关闭;服务器收到信号就关闭。
TCP的十一种状态
LISTEN - 侦听来自远方TCP端口的连接请求;
SYN-SENT - 在发送连接请求后等待匹配的连接请求;
SYN-RECEIVED - 在收到和发送一个连接请求后等待对连接请求的确认;
ESTABLISHED - 代表一个打开的连接,数据可以传送给用户;
FIN-WAIT-1 - 等待远程TCP的连接中断请求,或先前的连接中断请求的确认;
FIN-WAIT-2 - 从远程TCP等待连接中断请求;
CLOSE-WAIT - 等待从本地用户发来的连接中断请求;
CLOSING - 等待远程TCP对连接中断的确认;
LAST-ACK - 等待原来发向远程TCP的连接中断请求的确认;
TIME-WAIT - 等待足够的时间以确保远程TCP接收到连接中断请求的确认;
CLOSED - 没有任何连接状态;

2. UDP

无连接(例如发快递,不一定能送到)的不可靠传输协议,直接发送数据而不保证数据是否送达,适用于对实时性要求高、可容忍少量数据丢失的场景。

3. 关键区

一句话总结:
TCP 是 “可靠但稍慢” 的协议,适合需要保证数据完整的场景;
UDP 是 “不可靠但快速” 的协议,适合需要低延迟的实时场景。

十一、IO多路复用

1. 简介

是一种高效处理多个 IO 操作的技术,允许程序同时监控多个文件描述符,当其中任何一个或多个文件描述符处于可读、可写或发生异常状态时,系统会通知程序进行相应处理。
它的核心优势在于:单进程 / 线程可以同时管理多个 IO 流,避免了传统阻塞式 IO 中因单个 IO 操作阻塞而导致整个程序无法响应其他 IO 的问题,也减少了多线程 / 多进程模型的资源开销(如上下文切换)。

2. select

描述符集合,和信号集类似,里面有多个fd,会阻塞式监视描述符集合。
如果没有动静,select函数阻塞不返回;如果描述符集合中,某个描述符有动静,select函数返回>0的整数,返回的同时清空描述符集合。
我们可以调用select函数监视某些描述符号(每次调用select都要更新描述符号集合),根据select函数的返回值来知道是否有动静。
然后逐个检查来知道是哪个描述符号有动静。阻塞式io操作即可。
缺点:文件描述符数量有限制(默认 1024),每次调用需复制整个集合到内核空间,效率随监控数量增加而下降。
编程模型

3. poll

poll也是监视描述符集合,但它是立即返回,也是需要循环检测哪个fd有动静,poll不会每次都清空。
编程模型

4.epoll

为什么要用epoll?
select和poll都是轮询模式,监视到描述符号们有动静后,需要挨个检查是哪个描述符号发生了变化,然后处理。随着描述符号数量的增长,整个服务器的性能增长变缓。
三万个fd以下使用select或者poll,三万个fd以上使用epoll。
服务器单元:一般网络服务器分为三个单元——IO单元、逻辑单元、存储单元。
IO单元:处理连接 请求、IO多路复用、异步io
逻辑单元:也叫业务单元,处理具体的业务逻辑,输入输出、运算
存储单元:处理数据的存储与查找,数据库、连接池
epoll原理
采用事件通知机制。
调用了监视之后,然后如果有动静,就触发事件。我们可以等待事件发生然后进行处理。不会随着描述符号数量的增加而造成性能的下降。
编程模型

十二、异步IO

1. 理论
io多路复用是在应用层检查fd是否有动静。有动静就去操作。
而异步则是应用层调用内核的接口,然后去忙自己的事情,内核完成工作后通知应用层或者应用层主动查询内核是否完成了应用层交托给它的工作。

2. 主动查询

编程模型

3. 阻塞式等待

编程模型

4. 等待内核通知(信号)

#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <stdlib.h> #include <aio.h> struct Student { int id; char name[20]; int age; double score; }; #define DATA_LENGTH sizeof(struct Student) #define FILENAME "b.txt" void f(union sigval u) { struct aiocb *p = (struct aiocb *)u.sival_ptr; if (0 == aio_error(p)) { int r = aio_return(p); if (r >= 0) { struct Student *p1 = (struct Student *)(p->aio_buf); printf("%p\n", p1); for (int i = 0; i < 5; i++) { printf("%d %s %d %g\n", p1[i].id, p1[i].name, p1[i].age, p1[i].score); } exit(0); } } } int main() { // 1 创建结构体 int fd = open(FILENAME, O_RDONLY); if (-1 == fd) printf("打开文件失败:%m\n"), exit(-1); printf("打开文件成功!\n"); struct aiocb cb = {0}; cb.aio_fildes = fd; cb.aio_buf = malloc(DATA_LENGTH * 5); printf("%p\n", cb.aio_buf); memset((void *)cb.aio_buf, 0, DATA_LENGTH * 5); cb.aio_nbytes = DATA_LENGTH * 5; cb.aio_lio_opcode = LIO_READ; cb.aio_sigevent.sigev_notify = SIGEV_THREAD; cb.aio_sigevent.sigev_value.sival_ptr = &cb; cb.aio_sigevent.sigev_notify_function = f; int r = aio_read(&cb); if (r == -1) printf("异步读失败:%m\n"), close(fd), exit(-1); printf("异步读成功!\n"); int n = 0; while (1) { printf("n:%d\n", n++); } }

5. lio_listio

编程模型
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值