【C++八股-第16期】进程与线程 ②
❤ 10朵花 秒更下一期
提纲:
👉 八股:
介绍一下管道的概念,及其实现原理
Linux中一个线程占用多少内存?
32位系统访问4GB以上内存存在什么限制?
概述进程、线程、协程概念及区别?
TLB是什么东西
cache line是什么
互斥量能不能在进程中使用?
1. 介绍一下管道的概念,及其实现原理
管道(Pipe) 是 UNIX 和 Linux 系统中常用的进程间通信(IPC)机制之一,它允许一个进程将数据发送到另一个进程,即在同一时刻
两个进程之间只能进行单项通信
,因此管道也被称为 半双工
管道。
半双工:
- 指同一时刻数据只能由一个进程流向另一个进程
管道的本质是一种特殊的文件
-
匿名管道(Unnamed Pipe)只能用于有亲缘关系的进程间通信,例如父子进程。
-
命名管道(Named Pipe 或 FIFO)可以在无亲缘关系的进程之间进行通信,通过路径名来标识。
管道的基本实现原理:
管道的实现原理基于内核缓冲区
和文件描述符
。
-
内核缓冲区:
- 当管道创建时,内核会分配一个缓冲区,用于在进程之间传递数据。
- 这个缓冲区是一个先进先出(FIFO)的队列,确保数据按照写入的顺序被读取。
-
文件描述符:
pipe
系统调用创建一对文件描述符,一个用于读(read end)fd[0]
,一个用于写(write end)fd[1]
,fd[1]
的输出是fd[0]
的输入,管道是建立在内核之中的- 这两个文件描述符指向内核缓冲区的不同端,写描述符用于写入数据到缓冲区,读描述符用于从缓冲区读取数据。
-
数据传输:
- 写进程通过写描述符将数据写入内核缓冲区。
- 读进程通过读描述符从内核缓冲区读取数据。
- 当缓冲区满时,写操作将阻塞,直到有空间可写;当缓冲区为空时,读操作将阻塞,直到有数据可读。
管道的创建和使用:
#include <unistd.h>
#include <iostream>
#include <cstring>
int main() {
int fd[2];
if (pipe(fd) == -1) {
std::cerr << "Pipe failed" << std::endl;
return 1;
}
pid_t pid = fork();
if (pid == -1) {
std::cerr << "Fork failed" << std::endl;
return 1;
}
if (pid == 0) { // 子进程
close(fd[1]); // 关闭写端
char buffer[100];
read(fd[0], buffer, sizeof(buffer)); // 从管道读取数据
std::cout << "Child received: " << buffer << std::endl;
close(fd[0]); // 关闭读端
} else { // 父进程
close(fd[0]); // 关闭读端
const char *msg = "Hello from parent";
write(fd[1], msg, strlen(msg) + 1); // 向管道写入数据
close(fd[1]); // 关闭写端
}
return 0;
}
2. Linux中一个线程占用多少内存?
在Linux系统中,每个线程的默认栈大小通常为8MB
。
这意味着每个线程在创建时会预留最多8MB的虚拟内存空间用于其栈。
但是,这并不意味着每个线程一开始就实际使用了8MB的物理内存。
内存分配机制
Linux通过一种称为“按需分配”
(lazy allocation)或“缺页分配”
(demand paging)的机制来管理内存。以下是这种机制的工作原理:
-
当创建一个新线程时,操作系统会为其栈预留一块虚拟内存区域。这块区域的默认大小通常为8MB。
-
实际上,这块8MB的虚拟内存区域并不会立即全部映射到物理内存。当线程第一次访问栈中的某个地址时,才会触发缺页异常,操作系统会分配相应的物理内存页(通常为4KB)。
-
因此,线程实际使用的物理内存仅略大于其实际需要的内存量。这种分配方式可以有效节约物理内存。
3. 32位系统访问4GB以上内存存在什么限制?
在不用任何额外技术的,正常情况下无法访问
在32位系统中,内存地址空间的限制源于地址总线的宽度。
32位系统
的地址总线长度为32位,这意味着可以表示的内存地址范围是2^32,即4GB。具体来说:
-
32位地址总线可以表示的
最大内存空间是4GB
(2^32字节)。 -
操作系统和硬件通常需要保留一部分地址空间用于设备和系统用途,因此实际可用的物理内存通常略少于4GB。
PAE(Physical Address Extension)技术
使用
PAE技术
可以实现32位系统访问 4GB以上内存
PAE
技术通过扩展内存地址总线
来实现这一点:
- PAE将内存地址从32位扩展到36位。
- 36位地址总线可以表示的最大内存空间是64GB(2^36字节)。
4. 概述进程、线程、协程概念及区别?
-
进程:
内核调度
- 进程是操作系统中运行的程序的实例(操作系统会创建一个进程来运行你的程序代码)。
- 进程包含了程序代码、数据、和所有必要的资源,如文件句柄、内存地址空间、执行上下文等。
- 操作系统通过进程调度来管理CPU时间分配给各个进程。
-
线程:
内核调度
- 线程是进程中的一个执行单元,一个进程可以包含多个线程。
- 它们共享进程的资源(如内存、文件句柄等)
- 但有独立的执行上下文(如程序计数器、寄存器等)。
-
协程:
程序自身调度
- 协程是比线程更轻量化的
用户态
上下文切换机制。 - 协程由程序自己调度管理,因为在用户态,所以切换速度比线程、进程快得多。
- 协程只能在同一个线程内运行,不是并行执行。
- 协程通过
yield
和resume
等机制显式地让出和恢复控制权。
- 协程是比线程更轻量化的
进程、线程、协程对比
从属关系 | 独立的运行实例 | 进程中的执行单元 | 线程中的执行单元 |
调度单位 | 系统资源调度的最小单位 | CPU调度的最小单位 | 程序控制的最小单位 |
系统开销 | 系统开销大,切换时需要刷新TLB ,获取新的地址空间 |
系统开销较小,只需切换硬件上下文和内核栈 | 系统开销更小,切换只涉及用户空间,无需内核干预 |
内存单元 | 独立的内存空间,包括代码段、数据段和堆 | 共享进程的内存空间,每个线程有自己的栈和寄存器组 | 共享线程的栈,无需多线程的锁机制 |
通信方式 | 消息队列、信号量、共享内存等 | volatile 、wait() 和notify() |
直接调用函数,无需多线程的锁机制 |
执行效率 | 相对较低,切换开销大 | 相对较高,切换开销小于进程 | 极高,切换开销极小 |
适用场景 | 多核、多机分布式应用 | 多核并行应用 | 高并发场景,单线程并发控制 |
其他特点 | 一个进程挂掉不会影响其他进程 | 一个线程挂掉会导致整个进程挂掉 | 不依赖内核,切换速度快,操作灵活 |
5. TLB是什么东西
TLB(Translation Lookaside Buffer,翻译后备缓冲)是现代计算机处理器中的一种高速缓存
,用于加速虚拟地址到物理地址的转换过程。
作用
-
提高地址转换效率: TLB存储最近使用的虚拟地址到物理地址的映射,从而避免每次访问内存都需要通过页表进行地址转换。大大减少虚拟地址到物理地址转换所需时间。
-
减少了内存访问的延迟: TLB命中时,可以直接获取物理地址,避免多次访问内存进行页表查找。
工作原理示例
假设CPU要访问一个虚拟地址0xABCDEF
:
-
TLB查找:
TLB命中
- 先在TLB中查找虚拟地址
0xABCDEF
对应的页号。 - 如果命中,则直接得到物理帧号,组合形成物理地址访问内存。
- 先在TLB中查找虚拟地址
-
页表查找:
TLB未命中
- 如果未命中,则需要访问页表获取物理帧号,最终使用得到的物理地址访问内存。
- 通过页表查找到对应的物理帧号后,将这个映射关系加载到TLB中,以备下次快速查找。
-
TLB刷新:
- 当发生上下文切换(即进程切换)时,TLB中的条目可能需要刷新,因为新的进程可能使用不同的虚拟地址空间。
- 通常通过硬件机制或操作系统指令刷新TLB,确保新进程的地址转换正确。
CPU
| 虚拟地址(0xABCDEF)
v
+------------------+
| TLB查找 |
+------------------+
| |
| 命中 | 未命中
| |
v v
+-------+ +------------------+
|物理地址| | 页表查找 |
+-------+ +------------------+
| |
| 得到物理地址
v
+----------+
| 物理地址 |
+----------+
6. cache line是什么
**缓存行(Cache Line)**是缓存(Cache)中的最小存储单元。
!大小: 缓存行的大小通常是固定的,常见的大小有32字节、64字节等。 !存储单位: 缓存中的数据以缓存行为单位存储和管理,缓存行包含若干个连续的字节。 !对齐: 缓存行通常是对齐的,即它的起始地址是缓存行大小的整数倍。
工作原理
-
加载数据:
- 当CPU访问某个内存地址时,如果该地址的数据不在缓存中(缓存未命中),缓存会加载包含该地址的整个缓存行到缓存中。
- 例如,如果缓存行大小为64字节,当CPU访问地址0x1000,缓存会将0x1000到0x103F范围内的数据加载到一个缓存行中。
-
数据访问:
- 如果CPU再次访问0x1000到0x103F范围内的任一地址,缓存可以直接提供数据(缓存命中),无需访问主存。
- 这样大大提高了访问速度,因为访问缓存比访问主存快得多。
缓存行的重要性
-
局部性原理:
- 时间局部性: 最近被访问的数据很可能再次被访问。
- 空间局部性: 与某个地址相邻的地址很可能在不久后被访问。
- 缓存行利用空间局部性,将连续的数据一次性加载到缓存中,提高了访问效率。
-
伪共享(False Sharing):
- 当多个处理器的线程在同一个缓存行的不同部分上操作时,会导致缓存一致性协议频繁更新缓存行,降低性能。
- 通过适当的内存对齐和避免共享缓存行的数据,可以减少伪共享的影响。
7. 互斥量能不能在进程中使用?
**互斥量(Mutex)**可以在进程中使用,用于解决不同进程或同一进程内多个线程之间的资源竞争问题。
-
进程间的互斥:
- 不同的进程之间可能会竞争共享资源,例如文件、共享内存等。在这种情况下,需要互斥量来同步访问,防止数据竞争和数据不一致问题。
- 进程间的互斥量通常使用系统提供的IPC(进程间通信)机制,如System V信号量或POSIX信号量。
-
进程内的互斥:
-
一个进程中可以包含多个线程,这些线程之间也可能会竞争共享资源,如全局变量、堆数据等。为了防止多个线程同时修改共享数据,导致数据不一致,需要使用互斥量进行同步。
-
进程内的互斥量可以使用标准的互斥锁,如
std::mutex
(C++11)或pthread_mutex_t
(POSIX线程库)。