智能指针
进程间通信
共享内存
- 让两个进程的虚拟地址空间映射到同一组物理内存页
|
1 2 3 |
进程 A 虚拟地址 ─┐ ├── 同一块物理内存 进程 B 虚拟地址 ─┘ |
LinuxPOSIX:shm_open+ftruncate+mmapSystem V:shmget+shmat- 匿名共享映射:
mmap(..., MAP_SHARED, ...) - 普通文件映射:
mmap
windowsCreateFileMappingOpenFileMappingMapViewOfFile
无名管道
- 无名管道是一段由内核维护的字节流缓冲区
|
1 |
进程 A ──write──> 内核管道缓冲区 ──read──> 进程 B |
linux- 通常先创建管道,再调用
fork() - 父子进程会继承文件描述符,因此天然适合父子进程通信
- 通常先创建管道,再调用
|
1 2 3 4 5 |
int fd[2]; pipe(fd); // fd[0]:读端 // fd[1]:写端 |
|
1 2 3 4 5 6 |
父进程创建 pipe ↓ fork ↙ ↘ 父进程 子进程 写 fd[1] 读 fd[0] |
windows- 通过句柄继承把管道句柄传递给子进程
|
1 |
CreatePipe(...) |
有名管道
- 有名管道拥有一个可供独立进程找到的名字,因此通信双方不需要有父子关系
Linux FIFOLinux FIFO本质上仍然是字节流
|
1 2 3 4 |
mkfifo("/tmp/my_fifo", 0666); int fd = open("/tmp/my_fifo", O_WRONLY); write(fd, data, size); |
|
1 2 |
int fd = open("/tmp/my_fifo", O_RDONLY); read(fd, buffer, size); |
Windows Named PipeWindows有名管道功能更强,可以:- 单向或双向通信
- 字节模式或消息模式
- 支持多个客户端实例
- 配合重叠
I/O和IOCP - 在适当配置下支持网络访问
|
1 2 3 |
CreateNamedPipe( R"(\\.\pipe\MyPipe)", ...); |
|
1 2 3 |
CreateFile( R"(\\.\pipe\MyPipe)", ...); |
消息队列
- 消息队列由内核维护。发送方提交完整消息,接收方按消息取出
|
1 2 3 |
进程 A ──消息1──┐ 进程 B ──消息2──┼──> 内核消息队列 ──> 进程 C 进程 D ──消息3──┘ |
- 与管道的重要区别:
- 管道:连续字节流,没有天然消息边界
- 消息队列:一条一条的消息,有消息边界
linux System V消息队列- 消息可以带类型:
|
1 2 3 4 |
msgget(); msgsnd(); msgrcv(); msgctl(); |
|
1 2 3 4 |
struct Message { long type; char data[128]; }; |
POSIX消息队列POSIX消息队列支持消息优先级
|
1 2 3 4 5 |
mq_open(); mq_send(); mq_receive(); mq_close(); mq_unlink(); |
windowsWindows没有与POSIX/System V完全对应的通用内核消息队列API
- 工程中常见组合:
- 消息队列:传递控制消息和共享内存索引
- 共享内存:保存真正的大块数据
socket
- 通用的双向通信接口
rpc
- 远程过程调用,它不是一种与
Socket完全平级的底层通信方式 RPC的目标是让跨进程调用看起来接近普通函数调用- 示例
|
1 |
User user = get_user(1001); |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
客户端调用 get_user(1001) ↓ 序列化:函数名、参数 ↓ Named Pipe / Socket / 共享内存 ↓ 服务端反序列化 ↓ 执行真正的 get_user ↓ 序列化返回值 ↓ 客户端反序列化得到 User |
com
- 跨进程时,可以用代理、存根服务器通信
DCOM
DCOM将COM调用扩展到不同机器
进程间同步
事件
- 某件事情发生了,等待者可以继续执行
|
1 2 3 4 5 |
HANDLE event = CreateEvent( nullptr, FALSE, // 自动重置 FALSE, // 初始无信号 nullptr); |
|
1 |
WaitForSingleObject(event, INFINITE); |
|
1 |
SetEvent(event); |
Windows事件有两种- 自动重置事件:唤醒一个等待者后,自动恢复为无信号状态
- 手动重置事件:保持有信号状态,可以唤醒所有等待者,直到调用
|
1 |
ResetEvent(event); |
linuxLinux没有一个与Windows Event完全相同的通用对象- 常用替代方式:
eventfd- 条件变量
- 信号量
- 管道
futexsignalfd
互斥量
- 同一时刻,只允许一个执行者进入临界区
|
1 2 |
进程 A 获得锁 → 修改共享数据 → 释放锁 进程 B 等待 ─────────────────→ 获得锁 |
windows- 命名
Mutex可以在多个进程之间打开
- 命名
|
1 2 3 4 |
CreateMutex(); OpenMutex(); WaitForSingleObject(); ReleaseMutex(); |
|
1 |
CreateMutex(nullptr, FALSE, L"Global\\MyMutex"); |
linux- 普通
pthread_mutex_t默认只在线程间使用 - 要跨进程,需要把互斥量放在共享内存中,并设置:
- 还可以使用
robust mutex,使持锁进程崩溃后,其他进程能够检测
- 普通
|
1 2 3 |
pthread_mutexattr_setpshared( &attr, PTHREAD_PROCESS_SHARED); |
|
1 2 3 |
pthread_mutexattr_setrobust( &attr, PTHREAD_MUTEX_ROBUST); |
信号量
- 信号量本质上是一个计数器
|
1 2 3 |
计数 > 0:等待者可以获得一个资源,计数减一 计数 = 0:等待者阻塞 释放资源:计数加一并可能唤醒等待者 |
- 如果初始化为
3,表示最多允许3个执行者同时进入
|
1 2 3 4 5 |
资源数量 = 3 进程 A:占用一个 进程 B:占用一个 进程 C:占用一个 进程 D:等待 |
Windows
|
1 2 3 4 |
CreateSemaphore(); OpenSemaphore(); WaitForSingleObject(); ReleaseSemaphore(); |
Linux POSIX- 第二个参数为
1表示进程间共享,但信号量对象本身必须位于共享内存
- 第二个参数为
|
1 2 3 |
// 匿名进程共享信号量 sem_init(&sem, 1, initial_value); |
|
1 2 3 4 5 6 7 |
// 命名信号量 sem_open(); sem_wait(); sem_post(); sem_close(); sem_unlink(); |
条件变量
- 等待某个受互斥量保护的条件成立
|
1 2 3 4 5 6 |
while (queue.empty()) { pthread_cond_wait(&condition, &mutex); } queue.push(data); pthread_cond_signal(&condition); |
- 跨进程使用时,条件变量和互斥量都必须放在共享内存中,并设置:
|
1 2 3 |
pthread_condattr_setpshared( &attr, PTHREAD_PROCESS_SHARED); |
文件锁
- 多个进程通过文件交换数据时,可以使用文件锁
linux
|
1 2 |
flock(); fcntl(); |
windows
|
1 2 3 |
LockFile(); LockFileEx(); UnlockFile(); |
COM的使用
介绍
COM定义了标准线路格式和协议,以便在不同硬件平台上运行的对象之间进行交互COM与实现语言无关,可以使用不同的编程语言来创建COM库
概念
- 接口
- 成员函数的集合,面向对象中接口类
IID- 每个接口都有自己的唯一接口标识符
IID,IID是一个全局唯一标识符GUID,IID只会对应一个接口
- 每个接口都有自己的唯一接口标识符
COM类COM类可以创建实现了多个 接口
GLSID- 每个
com类都有一个CLSID,在windows上com类 的实现在dll或者exe上
- 每个
- 映射关系
COM维护系统上安装的COM对象的所有Clsid的注册数据库- 注册数据库是每个
CLSID之间的映射,以及承载相应类的DLL或EXE的位置。 - 每当调用方想要创建
COM类的实例时,COM都将查询此数据库 - 调用方只需了解用于请求类的新实例的
CLSID
CoCreateInstance
- 根据
CLSID找到COM类的“类工厂” - 让类工厂创建对象
- 再返回调用者指定的接口指针
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
客户端线程 ↓ 确认 COM 已初始化 ↓ 根据 CLSID 查找 COM 类注册信息 ↓ 定位 DLL、EXE 或其他激活信息 ↓ 获得 IClassFactory ↓ 调用 IClassFactory::CreateInstance ↓ 创建 COM 对象 ↓ 获得请求的 IID 对应接口 ↓ 把接口指针返回给客户端 |
临界区
概述
- 临界区不是某块特殊内存,而是一段访问共享资源、需要互斥执行的代码
Windows CRITICAL_SECTION
|
1 2 3 4 5 6 7 8 9 |
InitializeCriticalSection ↓ EnterCriticalSection ↓ 访问共享资源 ↓ LeaveCriticalSection ↓ DeleteCriticalSection |
进入临界区时发生什么
- 如果没有其他线程持有:
- 立即获得临界区,继续执行
- 如果已经被其他线程持有:
|
1 2 3 4 5 |
当前线程等待 ↓ 持有线程调用 LeaveCriticalSection ↓ 等待线程获得临界区 |
- 工程中如果不依赖
Windows专用API,通常优先使用标准C++
|
1 2 3 4 5 6 7 8 9 |
#include <mutex> std::mutex g_mutex; int g_counter = 0; void update_counter() { std::lock_guard<std::mutex> lock{g_mutex}; ++g_counter; } |
|
1 2 3 4 |
void update_counter() { std::scoped_lock lock{g_mutex}; ++g_counter; } |
从DLL中找函数的地址
- 在
Windows中,从DLL查找函数地址的标准流程是:
|
1 |
LoadLibrary → GetProcAddress → 调用函数 → FreeLibrary |
GetProcAddress做了什么
|
1 2 3 4 5 6 7 |
MyDll.dll 映射到当前进程地址空间 ↓ 找到 PE 导出表 ↓ 按名称查找 "add" ↓ 返回函数入口地址 |
查看DLL中的函数
- 在
Windows下,要“查看DLL中有哪些函数”,本质上是查看DLL的PE导出表 - 枚举全部导出函数需要自己解析
PE导出表,或者使用dumpbin、Dependencies等工具
编码
反转链表
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
#include <iostream> struct ListNode { int value; ListNode* next; }; ListNode* reverse_list(ListNode* head) { ListNode* prev = nullptr; ListNode* current = head; while (current != nullptr) { // 1. 保存下一个节点,避免修改指针后丢失后续链表 ListNode* next = current->next; // 2. 反转当前节点的指向 current->next = prev; // 3. 两个指针向后移动 prev = current; current = next; } // current 已经指向 nullptr,prev 是新的头节点 return prev; } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
ListNode* reverse_list_recursive(ListNode* head) { if (head == nullptr || head->next == nullptr) { return head; } ListNode* new_head = reverse_list_recursive(head->next); head->next->next = head; head->next = nullptr; return new_head; } |
单链表排序
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
#include <iostream> struct ListNode { int value; ListNode* next; }; // 合并两个有序链表 ListNode* merge_sorted_lists(ListNode* lhs, ListNode* rhs) { // 临时头节点,避免单独处理结果链表的第一个节点 ListNode dummy{0, nullptr}; ListNode* tail = &dummy; while (lhs != nullptr && rhs != nullptr) { if (lhs->value <= rhs->value) { tail->next = lhs; lhs = lhs->next; } else { tail->next = rhs; rhs = rhs->next; } tail = tail->next; } // 其中一个链表已经用完,接上另一个链表的剩余部分 tail->next = lhs != nullptr ? lhs : rhs; return dummy.next; } // 使用归并排序对单链表排序 ListNode* sort_list(ListNode* head) { // 空链表或只有一个节点时,天然有序 if (head == nullptr || head->next == nullptr) { return head; } // 快慢指针寻找链表中点 ListNode* slow = head; ListNode* fast = head->next; while (fast != nullptr && fast->next != nullptr) { slow = slow->next; fast = fast->next->next; } // 从 slow 后面断开,得到两个独立链表 ListNode* right_head = slow->next; slow->next = nullptr; ListNode* left = sort_list(head); ListNode* right = sort_list(right_head); return merge_sorted_lists(left, right); } void print_list(const ListNode* head) { while (head != nullptr) { std::cout << head->value; if (head->next != nullptr) { std::cout << " -> "; } head = head->next; } std::cout << '\n'; } int main() { ListNode node5{2, nullptr}; ListNode node4{4, &node5}; ListNode node3{1, &node4}; ListNode node2{3, &node3}; ListNode node1{5, &node2}; ListNode* head = &node1; std::cout << "排序前:"; print_list(head); head = sort_list(head); std::cout << "排序后:"; print_list(head); } |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ 2023_02_1502/20
- ♥ 2022_02_24_0203/01
- ♥ 2020_11_0511/23
- ♥ 2019_11_0511/07
- ♥ 2022_03_0103/01
- ♥ 2020_11_0902/16
热评文章
- 2020_05_11_01 0
- 2022_03_07 0
- 2025_03_11 0
- 2020_04_29 0
- 2022_02_26 0
- 2025_03_18 0