深挖操作系统知识点
一个进程可以创建多少线程
进程的虚拟内存空间的大小
每创建一个线程,操作系统需要为其分配一个栈空间,如果线程数量越多,所需的栈空间就要越大,那么虚拟内存就会占用的越多。
在 32 位 Linux 系统里,一个进程的虚拟空间是 4G,内核分走了1G,留给用户用的只有 3G。
那么假设创建一个线程需要占用 10M 虚拟内存,总共有 3G 虚拟内存可以使用。于是我们可以算出,最多可以创建差不多 300 个(3G/10M)左右的线程。
系统的参数限制
- 栈空间的大小
linux内核中有给每一个线程分配的栈空间的大小,是一个固定的值,可以通过ulimit -a看到stack size:

可以看到图中显示的线程数量是8.192M,大概10M左右。 - 系统支持的最大线程数
/proc/sys/kernel/threads-max内部限制了创建线程的大小,默认是14553,大致是一万多的线程数量。这个参数也会限制最大线程数 - tid号的个数
系统全局的tid号有一个最大限制,所以当创建的数量超过之后会创建失败。
保证线程安全
保证线程安全的主要手段有互斥量、读写锁、原子操作、条件变量、线程本地存储
互斥量
在使用每一个临界区资源的时候采用加锁的方式访问,每次访问均要加上锁,加锁成功之和才能访问临界区资源
读写锁
当共享资源是符合读写场景的时候可以采用读写锁,原理和信号量十分相似。
原子操作
c++中可以用atomic类型声明变量,表示的是该变量是具有原子性的,对该变量进行操作要不完全操作完,要不就不操作,所以具备互斥性
条件变量
使用条件变量来使得多个线程按照一定的顺序执行,可以让线程之间有一个约束,访问资源的顺序确定下来就不会出现线程安全问题
使用线程本地变量保存
#include <iostream>
#include <thread>
thread_local int g_counter = 0; // 线程本地变量
void incrementCounter()
{
++g_counter;
std::cout << std::this_thread::get_id() << ": " << g_counter << std::endl;
}
int main()
{
std::thread t1(incrementCounter);
std::thread t2(incrementCounter);
t1.join();
t2.join();
return 0;
}
要注意的是这个g_counter变量虽然是全局变量,但是用thread_local修饰之后,表示是线程的私有量。由于g_counter 是线程本地变量,所以每个线程都有自己的 g_counter,互不干扰,也就是说每一个线程都有自己的g_counter变量,互不影响。
为了提升CPU使用率,线程数量是怎么确定的
在实际运用的时候我们往往需要考虑cpu的使用率,尽量使得cpu在处在忙碌的状态,所以要考虑任务的类型
- cpu密集型任务(CPU密集型任务是指在执行过程中需要大量CPU资源的任务):复杂的数学计算、图形处理、加密解密、编译任务等
- io密集型任务(IO密集型任务是指那些主要瓶颈在于输入输出操作,而非计算操作的计算机任务):磁盘io,网络io,设备交互。
io密集型任务
IO密集型任务通常适合使用多线程来提高程序的性能和效率。通过多线程的方式可以让程序在等待IO的同时执行其他任务,充分利用CPU资源,从而提高整体系统的吞吐率和响应速度。
线程数量一般取大一点,习惯性取10,大概是cpu数量的两倍多一些
cpu密集型
对于CPU密集型任务,是否适合使用多线程取决于具体情况。在单核处理器上,多线程可能不会带来性能上的提升,因为多个线程会竞争CPU资源。而在多核处理器上,使用多线程可以充分利用多个核心,提高并行计算能力,从而实现性能的提升。
一般取cpu核数 + 1
多线程和多进程的使用场景
多进程的使用场景:
-
并行计算:对于需要大量计算的任务,如科学计算、图像处理等,可以通过多进程实现并行计算,充分利用多核处理器的优势。
-
独立性要求:如果每个任务需要独立的内存空间或者隔离的环境,多进程比多线程更合适,因为每个进程拥有独立的内存空间,互不干扰。
-
可靠性要求:多进程比多线程更健壮,因为一个进程的崩溃不会影响其他进程,可以通过父进程监控和重启子进程来增强整体的可靠性。
多线程的使用场景:
-
共享内存:如果任务之间需要共享大量数据,使用多线程可以减少数据复制的开销,因为所有线程共享同一个地址空间。
-
响应性要求:对于需要快速响应用户请求的应用,如Web服务器或GUI应用程序,多线程可以使得同时处理多个请求或事件成为可能。
-
并发性:多线程适合处理大量I/O操作(如文件读写、网络通信等),通过I/O多路复用和非阻塞I/O可以有效提高系统的并发性能。
综上所述,选择多进程还是多线程取决于项目的具体需求和优化目标。通常情况下,多进程适合CPU密集型的任务或需要独立性和可靠性的场景,而多线程适合I/O密集型的任务或需要更高的响应速度和资源节约的场景。
线程池的相关问题
https://cloud.tencent.com/developer/article/2355219
进程切换、线程切换和协程切换分别涉及哪些资源


总结起来就是两个问题,一个是虚拟地址的切换,牵涉页表的替换和缓存不中,一个是cpu调度相关的资源(寄存器和程序计数器),资源的记载和切换。
https://blog.csdn.net/weixin_44844089/article/details/115672685
进程切换
1.必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行,这种行为被称为进程切换,任务切换或上下文切换。
2. 实质上就是把进程存放在处理器的寄存器中的中间数据找个地方存起来,从而把处理器的寄存器腾出来让其他进程使用。
3. 进程切换的步骤
1)切换新的页表
当前进程的上下文需要被新的进程上下文所替换,所以当前上下文的物理地址也会被占用,此时就需要切换新的页表,使用新的虚拟地址空间。
2)切换内核栈
就是使用新的栈来存放进程运行时资源了。然后新进程有新的PCB控制块。
4. 虚拟地址切换空间比较慢的原因
现在我们已经知道了进程都有自己的虚拟地址空间,把虚拟地址转换为物理地址需要查找页表,页表查找是一个很慢的过程,因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个cache就是TLB(translation Lookaside Buffer,我们不需要关心这个名字只需要知道TLB本质上就是一个cache,是用来加速页表查找的)。由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么当进程切换后页表也要进行切换,页表切换后TLB就失效了,cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换则不会导致TLB失效,因为线程线程无需切换地址空间,因此我们通常说线程切换要比较进程切换块,原因就在这里。
线程切换的步骤
但是由于一个进程中的多个线程是共享一个进程的虚拟空间的,所以线程的切换不需要设计虚拟地址的变化,这就是为什么进程切换比线程切换的开销大。
所以线程的切换就只包括线程上下文的切换,就是替换线程放在处理器寄存器中的相关数据,但是同样需要从用户态转向内核态。
协程切换的步骤
实现协程的过程实际上是实现一个用户态的调度器
- 协程切换完全在
用户空间进行; - 协程切换相比线程切换做的事情更少,比如栈的信息不用替换;
用户态和内核态的切换方式有哪些
https://blog.csdn.net/JMW1407/article/details/107901155
应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用
什么是内核态
很多程序开始时运行于用户态,但在执行的过程中,一些操作需要在内核权限下才能执行,这就涉及到一个从用户态切换到内核态的过程。比如C函数库中的内存分配函数malloc(),它具体是使用sbrk()系统调用来分配内存,当malloc调用sbrk()的时候就涉及一次从用户态到内核态的切换。
用户态到内核态切换方式
- 系统调用(主动切换的):其实系统调用本身就是中断,但是软件中断(通过
INT n指令产生),跟硬中断不同。 - 异常(被动切换的): 当CPU正在执行运行在用户态的程序时,突然发生某些预先不可知的异常事件,这个时候就会触发从当前用户态执行的进程转向内核态执行相关的异常事件,典型的如缺页异常。
- 外设中断(硬中断)(被动切换的):当外围设备完成用户的请求操作后,会像CPU发出中断信号,此时,CPU就会暂停执行下一条即将要执行的指令,转而去执行中断信号对应的处理程序,如果先前执行的指令是在用户态下,则自然就发生从用户态到内核态的转换。
fork()与vfork()
https://blog.csdn.net/jianchi88/article/details/6985326
- fork()产生的子进程会拷贝父进程的资源产生新的虚拟地址空间(注意是写时复制的),而vfork()是共享数据段的。
- fork()产生的父子进程运行顺序是不确定的,而vfork()是子进程先运行的(子进程调用execl或者exit即子进程运行结束之和才能执行父进程),所以当有资源以来父进程的时候会死锁。
- 为什么会有vfork,因为以前的fork 很傻, 它创建一个子进程时,将会创建一个新的地址空间,并且
拷贝父进程的资源,而往往在子进程中会执行exec 调用,这样,前面的拷贝工作就是白费力气了,这种情况下,聪明的人就想出了vfork,它产生的子进程刚开始暂时与父进程共享地址空间(其实就是线程的概念了),因为这时候子进程在父进程的地址空间中运行,所以子进程不能进行写操作,并且在儿子 霸占”着老子的房子时候,要委屈老子一下了,让他在外面歇着(阻塞),一旦儿子执行了exec 或者exit 后,相当于儿子买了自己的房子了,这时候就相当于分家了。

浙公网安备 33010602011771号