转载请注明作者和出处:https://blog.csdn.net/qq_28810395
运行平台: Windows 10
LeetCode官网:Fhttps://leetcode-cn.co
一、进程与线程的基本概念
1. 什么是进程,线程,彼此有什么区别⭐⭐⭐⭐⭐
进程是计算机资源(CPU,内存)分配的基本单位。
线程是计算机CPU调度与分配的基本单位,也就是程序执行的最小单位。
-
运行一个程序时,系统首先创建一个进程,同时分配地址空间和其他资源,随后将进程加入就绪队列,直到分配到CPU时间就可以正式运行了。
-
线程是进程的一个执行流,有一个初学者可能误解的概念,进程就像一个容器一样,包括程序运行的程序段、数据段等信息,但是进程其实是不能用来运行代码的,真正运行代码的是进程里的线程。
-
我们最熟悉的main()函数,我们既可以认为这是一个进程,也可以认为是一个线程。我们都知道,在C/C++中main函数是程序入口,所以准确来说main函数是程序的主线程。然而很神奇的地方在于,当系统在执行main函数的时候,main函数又是一个独立的进程,我们可以在main函数里创建子进程,也可以创建子线程。
-
在main函数里创建的多个子线程中,每个线程有自己的堆栈和局部变量,但多个线程也可共享同个进程下的所有共享资源(全局变量),因此我们经常可以创建多个线程实现并发操作,实现更加复杂的功能。
示例:我们看一个实际例子来加强理解。#include “stdio.h” int g_cnt = 0; //全局变量 int * thread(void * arg) { int m_cnt = 0; m_cnt = 5; g_cnt++; return 0; } int main(void) { int err = 0; pthread_t tid; int m_cnt = 0; //局部变量 err=pthread_create(&tid, NULL, thread, NULL); //创建子线程 if (0 != err) //检验是否创建成功 { printf("can't creat thread: %s\n", strerror(err)); } while(g_cnt == 0) { usleep(300); //延迟300毫秒,让子线程运行一会儿 } printf("g_cnt = %d, m_cnt = %d\n", g_cnt, m_cnt); return 0; }
结果:
程序中main函数是一个主线程,开始执行程序,同时main函数又是一个进程,我们可以创建子线程thread(),子线程有自己的堆栈和局部变量,同时又与主线程共享全局变量,这也就是为何输出结果显示子线程改变了全局变量
g_cnt
,但没有改变main函数里的同名局部变量m_cnt
。关键注意点
:我们可以看到main函数有一个while循环,一开始 g_cnt == 0,程序进入while循环后就不能做其他事情,但是子线程thread不受影响,仍然可以独立于main函数,自己做自己的事情。
2.多进程、多线程的优缺点⭐⭐⭐⭐
解析:为了理解多进程、多线程各自的优缺点之前,我们需要先了解进程和线程最大的区别和联系,一个进程由PCB(进程控制块)、数据段、代码段组成,进程本身不可以运行程序,而是像一个容器一样,先创建出一个主线程,分配给主线程一定的系统资源,这时候就可以在主线程开始实现各种功能。当我们需要实现更复杂的功能时,可以在主线程里创建多个子线程,跟人多好干活的道理一样,多个线程在同一个进程里,利用这个进程所拥有的系统资源合作完成某些功能。
理解了这些知识点,再来理解各自优缺点就很容易了。
- 鲁棒性:多进程更健壮,一个进程死了不影响其他进程,子进程死了也不会影响到主进程,毕竟系统会给每个进程分配独立的系统资源。多线程比较脆弱,一个线程崩溃很可能影响到整个程序,因为多个线程是在一个进程里一起合作干活的。
- 性能:进程性能大于线程,每个进程独立地址空间和资源,而多个线程是一起共享了同个进程里的空间和资源,结果就很明显了,线程的性能上限一定比不上进程。
- 系统花销:正因为进程性能大于线程。所以这也引发了另一重要知识点,创建多进程的系统花销远大于创建多线程。
- 数据传输: 多进程通讯因为需要跨越进程边界,不适合大量数据的传送,更适合小数据或者密集数据的传送。而多线程无需跨越进程边界,适合各线程间大量数据的传送,甚至还有很重要的一点,多线程可以共享同一进程里的共享内存和变量哦。
- 逻辑控制复杂度:多进程逻辑控制比多线程复杂,需要与主进程做好交互。根据上面几点,我们不难知道多进程是“要用来做大事”的,而多线程是“各自做件小事,合作完成大事”。所以要做大事自然就需要更复杂的逻辑控制,不像做小事那么目标明显。虽然多线程逻辑控制比较简单,但是却需要复杂的线程同步和加锁控制等机制。
- 进/线程数量:最后的一点,可能比较少见,我们可以通过增加CPU的数量来增加进程的数量,但增加不了线程的数量,即增加CPU无法提高线程数量,线程数量由进程的空间资源和线程本身栈大小确定,详情见1.1.6小节。
3.什么时候用进程,什么时候用线程⭐⭐⭐
解析:还是同一个思想,进程是“要用来做大事”的,而线程是“各自做件小事,合作完成大事”,结合上节新鲜出炉的优缺点我们就很好理解什么时候用进程或者线程了。
-
创建和销毁较频繁使用线程,因为创建进程花销大嘛。
-
需要大量数据传送使用线程,因为多线程切换速度快,不需要跨越进程边界。
-
并行操作使用线程。线程是为了实现并行操作的一个手段,也就是刚才说的需要多个并行操作“合作完成大事”,当然是使用线程啦。
-
最后可以总结为:安全稳定选进程;快速频繁选线程;
4. 多进程、多线程同步(通讯)的方法⭐⭐⭐
我们使用系统编程时,就会遇到多进程/多线程编程,所以需要了解多个进程,多个线程之间常见的通讯机制,这也是嵌入式面试中高频问题之一。
- 进程之间通讯:
(1)管道/无名管道;(2)信号;(3)共享内存;(4)信息量;(5)消息队列;(6)Socket
管道( pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系
有名管道 (named pipeline) :有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
高级管道(pipeline): 将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式。
信号量( semaphore ): 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
消息队列( message queue ): 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
信号 ( signal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
共享内存( shared memory) : 共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
套接字(socket ) : 套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
注意
:临界区是一种概念,指的是访问公共资源的程序片段,不是一种通讯方式
- 线程之间通讯
(1)信息量;(2)读写锁;(3)条件变量;(4)互斥锁;(5)信号
互斥锁:提供了以排他方式防止数据结构被并发修改的方法。
读写锁:允许多个线程同时读共享数据,而对写操作是互斥的。
条件变量:可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
信号量机制(Semaphore):包括无名线程信号量和命名线程信号量。
信号机制(Signal):类似进程间的信号处理。
- 题问:互斥锁与信号量的区别?
互斥锁用于线程互斥,信号量用于线程同步,这是互斥锁与信号量的根本区别,也就是互斥与同步之间的区别。同时互斥锁的作用域仅仅在于线程,而信号量可用于线程与进程。
5. 进程的空间模型⭐⭐⭐
解析:32位的系统中,系统运行一个程序,就会创建一个进程。系统为其分配4G的虚拟地址空间,其中3G是用户空间,1G是核心空间,内核空间是受保护的,用户不能对该空间进行读写操作,否则可能出现段错误。
- 其中栈空间的地址是从高到底,新的数据地址比旧的要小,相反,堆空间是向上的。
- 内核区:用户代码不可见的区域,页表就存放在这个区域中。
- 用户区:
a、代码段:只可读,不可写,程序代码段。
b、数据段:保存全局变量,静态变量的区域。
c、堆区:就是动态内存,通过malloc,new申请内存,有一个堆指针,可以通过brk系统调用调整堆指针。
d、文件映射区域:通过mmap系统调用,如动态库,共享内存等映射物理空间的内存区域。可以单独释放,不会产生内存碎片。
e、栈区:用于维护函数调用的上下文空间,用ulimit -s 查看。一般默认为8M。
注意
:64位操作系统下的虚拟内存空间大小:地址空间大小不是232,也不是264
,而一般是248 。因为不需要那么大的寻址空间,过大会造成浪费,所以一般为48位表示虚拟空间地址,40位标识物理地址。
6. 一个进程可以创建多少个线程,和什么有关⭐⭐
一个进程可以创建的线程个数与虚拟内存和分配给线程的调用栈大小决定
解析:我们知道一个进程会有4G的虚拟内存,3G用户空间,1G是内核空间,也就是3G能允许用来创建线程,而一般一个线程大小为8-10M,以10M来算,那就可以创建300个线程。
用户可以使用ulimit -s
指令来查看线程的空间大小
7.进程线程的状态转换图,什么时候阻塞,什么时候就绪⭐⭐
解析:在开始之前,我们首先要了解一个进程从开始到死亡可能出现什么状态。
- 创建态(New): 一个进程开始被创建,还没到就绪态时候的状态;
- 就绪态(Ready): 一个进程获取到CPU分配的空间与地址(除CPU时间片),如果获得时间片则转入运行态;
- 运行态(Running): 当一个进程得到CPU调度正在处理机上运行时的状态;
- 睡眠/挂起态: 由于某些资源暂时获得不到从而进入“睡眠态”,进行将被挂起;
- 阻塞/暂停态(Blocked): 由于当前进程被中断或者其他事件导致而暂停运行时的状态;
- 结束/僵尸态(Exit): 一个进程正在从系统中消失时候的状态,这是因为进程结果或其他因流产所导致。
- 死亡态: 进程生命周期结束,将所占用的资源归还系统。
我们从父进程调用fork ()创建子进程开始讲起,此时子进程处于创建态,此时系统为进程分配地址和资源后将进程加入就绪队列,进入就绪态。就绪态的进程得到CPU时间片调度正式运行,进入执行态。执行态有四种常见结果:
-
当时间片耗光或者被其他进程抢占,则重新进入就绪态,等待下一次CPU时间片;
-
由于某些资源暂时不可得到而进入“睡眠态”(如欲读取的文件为空或者欲获得的某个锁还处于不可获得状态),等待资源可得后再唤醒,唤醒后进入就绪态;
-
收到SIGSTOP/SIGTSTP信号进入暂停态,直到收到SIGCONT信号重新进入就绪态;
-
进程执行结束,通过内核调用do_exit(进入僵尸态,等待系统回收资源。当父进程调用wait(/waitpidO后接收结束子进程,该进程进入死亡态。
插播: 在书上看到一段话,描述进程的一生,感觉写的挺好的 首先,随着fork的成功执行,一个新的子进程诞生,此时他还只是父进程的一个克隆,从父进程那里得到数据段和堆栈段的拷贝。然后随着exec,新的进程脱胎换骨,独立成家,看是独自执行一个全新的程序,并完全代替原有的父进程。 人有生老病死,进程也一样,他可以是自然死亡,即运行到main函数的最后一"}",从容的离我们而去;也可以自杀,自杀有两种方式,第一种是调用exit函数,一种是在main函数内使用return,无论哪一种方式,他都可留下遗书,放在返回值里保存下来,;他甚至还可能被杀,被其他进程通过另外一些方式结束他的生命。 进程死掉时候之后,会留下一具僵尸,wait和waitpid充当了搬尸工,把僵尸退去火化,使其最终归于无形…… 这就是进程的一生……
8. 父进程、子进程的关系以及区别⭐⭐⭐⭐⭐
解析:首先我们看看子进程能在父进程继承什么,以及子进程独有的数据:
- 子进程继承父进程:
- 用户号UIDs和用户组号GIDs
- 环境
- 堆栈
- 共享内存
- 打开文件的描述符
- 执行时关闭标志
- 信号控制谁当
- 进程组号
- 当前工作目录
- 根目录
- 文件方式创建屏蔽字
- 资源限制
- 控制中断
- 子进程独有的:
- 进程号PID
- 不同的父进程号
- 自己的文件描述与目录流拷贝
- 子进程不继承父进程的进程正文(text),数据和其他锁定内存(memory locks)
- 不继承异步输入与输出
父进程调用fork()以后,克隆出一个子进程,子进程和父进程拥有相同内容的代码段、数据段和用户堆栈。但其实父进程只复制了自己的PCB块,而代码段,数据段和用户堆栈内存空间是与子进程共享的。只有当子进程在运行中出现写操作时,才会产生中断,并为子进程分配内存空间。
在面试前,我们需要记清楚、分清楚几个主要的父子进程共有的资源和子进程独有的资源。
子进程从父进程继承的主要有:用户号和用户组号,堆栈;共享内存﹔目录(当前目录、根目录)﹔打开文件的描述符;但父进程和子进程拥有独立的地址空间和PID参数、不同的父进程号、自己的文件描述符。
9. 什么是进程上下文,中断下下文⭐⭐
-
进程上下文:
- 进程上文: 其是指进程由用户态切换到内核态是需要保存用户态时cpu寄存器中的值,进程状态以及堆栈上的内容,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态,继续执行。
- 进程下文: 其是指切换到内核态后执行的程序,即进程运行在内核空间的部分。
-
中断上下文:
- 中断上文: 硬件通过中断触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。中断上文可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被中断的进程环境)。
- 中断下文: 执行在内核空间的中断服务程序。
当工作在用户态的进程想访问某些内核才能访问的资源时,必须通过系统调用或者中断切换到内核态,由内核代替其执行。进程上下文和中断上下文就是完成这两种状态切换所进行的操作总称。我将其理解为保存用户空间状态是上文,切换后在内核态执行的程序是下文。
二、参考信息
[1]https://blog.nowcoder.net/n/704684feeffc4979b9d04b66f13c1555