一、进程与线程的基本概念
1. 什么是进程,线程,彼此有什么区别
- 进程是计算资源(CPU,内存)分配的基本单位
- 线程是计算机CPU调度和分配的基本单位,也就是程序执行的最小单位
1、运行一个程序时,系统首先创建一个进程,同时分配地址空间和其他资源,随后将进程加入就绪队列,指导分配到CPU时间就可以正式运行了。
2、线程是进程的一个执行流。代码其实真正运行的是进程里面的线程。
3、main()函数本质上属于一个进程业属于一个线程,我们既可以在main函数里面创建子进程,也可以同时创建子线程
4、在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函数有一个while循环,一开始 g_cnt == 0,程序进入while循环后就不能做其他事情,但是子线程thread不受影响,仍然可以独立于main函数,自己做自己的事情。
二、多进程、多线程的优缺点
解析:为了理解多进程、多线程各自的优缺点之前,我们需要先了解进程和线程最大的区别和联系,一个进程由PCB(进程控制块)、数据段、代码段组成,进程本身不可以运行程序,而是像一个容器一样,先创建出一个主线程,分配给主线程一定的系统资源,这时候就可以在主线程开始实现各种功能。当我们需要实现更复杂的功能时,可以在主线程里创建多个子线程,跟人多好干活的道理一样,多个线程在同一个进程里,利用这个进程所拥有的系统资源合作完成某些功能。
理解了这些知识点,再来理解各自优缺点就很容易了
- 鲁棒性:多进程更健壮,一个进程死了不影响其他进程,子进程死了也不会影响到主进程,毕竟系统会给每个进程分配独立的系统资源。多线程比较脆弱,一个线程崩溃很可能影响到整个程序,因为多个线程是在一个进程里一起合作干活的。
- 性能:进程性能大于线程,每个进程独立地址空间和资源,而多个线程是一起共享了同个进程里的空间和资源,结果就很明显了,线程的性能上限一定比不上进程。
- 系统花销:正因为进程性能大于线程。所以这也引发了另一重要知识点,创建多进程的系统花销远大于创建多线程。
- 数据传输: 多进程通讯因为需要跨越进程边界,不适合大量数据的传送,更适合小数据或者密集数据的传送。而多线程无需跨越进程边界,适合各线程间大量数据的传送,甚至还有很重要的一点,多线程可以共享同一进程里的共享内存和变量哦。
- 逻辑控制复杂度:多进程逻辑控制比多线程复杂,需要与主进程做好交互。根据上面几点,我们不难知道多进程是“要用来做大事”的,而多线程是“各自做件小事,合作完成大事”。所以要做大事自然就需要更复杂的逻辑控制,不像做小事那么目标明显。虽然多线程逻辑控制比较简单,但是却需要复杂的线程同步和加锁控制等机制。
- 进/线程数量:最后的一点,可能比较少见,我们可以通过增加CPU的数量来增加进程的数量,但增加不了线程的数量,即增加CPU无法提高线程数量,线程数量由进程的空间资源和线程本身栈大小确定。
三、什么时候选进程什么时候选线程?
最后可以总结为:安全稳定选进程;快速频繁选线程;
四、多进程、多线程同步(通讯)的方法
我们使用系统编程时,就会遇到多进程/多线程编程,所以需要了解多个进程,多个线程之间常见的通讯机制,这也是嵌入式面试中高频问题之一。
进程之间通讯:
(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):类似进程间的信号处理。
五、进程中的空间模型
解析:32位的系统中,系统运行一个程序,就会创建一个进程。系统为其分配4G的虚拟地址空间,其中3G是用户空间,1G是核心空间,内核空间是受保护的,用户不能对该空间进行读写操作,否则可能出现段错误。
内核区:用户代码不可见的区域,页表就存放在这个区域中。
用户区:
- a、代码段:只可读,不可写,程序代码段。
- b、数据段:保存全局变量,静态变量的区域。
- c、堆区:就是动态内存,通过malloc,new申请内存,有一个堆指针,可以通过brk系统调用调整堆指针。
- d、文件映射区域:通过mmap系统调用,如动态库,共享内存等映射物理空间的内存区域。可以单独释放,不会产生内存碎片。
- e、栈区:用于维护函数调用的上下文空间,用ulimit -s 查看。一般默认为8M。
注意:64位操作系统下的虚拟内存空间大小:地址空间大小不是232,也不是264
,而一般是248 。因为不需要那么大的寻址空间,过大会造成浪费,所以一般为48位表示虚拟空间地址,40位标识物理地址。
六、一个进程可以创建多少个线程,和什么有关
一个进程可以创建的线程个数与虚拟内存和分配给线程的调用栈大小决定
解析:我们知道一个进程会有4G的虚拟内存,3G用户空间,1G是内核空间,也就是3G能允许用来创建线程,而一般一个线程大小为8-10M,以10M来算,那就可以创建300个线程。
用户可以使用ulimit -s 指令来查看线程的空间大小
七、进程线程的状态转换图,什么时候阻塞,什么时候就绪
在书上看到一段话,描述进程的一生,感觉写的挺好的
首先,随着fork的成功执行,一个新的子进程诞生,此时他还只是父进程的一个克隆,从父进程那里得到数据段和堆栈段的拷贝。然后随着exec,新的进程脱胎换骨,独立成家,看是独自执行一个全新的程序,并完全代替原有的父进程。
人有生老病死,进程也一样,他可以是自然死亡,即运行到main函数的最后一"}",从容的离我们而去;也可以自杀,自杀有两种方式,第一种是调用exit函数,一种是在main函数内使用return,无论哪一种方式,他都可留下遗书,放在返回值里保存下来,;他甚至还可能被杀,被其他进程通过另外一些方式结束他的生命。
进程死掉时候之后,会留下一具僵尸,wait和waitpid充当了搬尸工,把僵尸退去火化,使其最终归于无形……
这就是进程的一生……
- 创建态(New): 一个进程开始被创建,还没到就绪态时候的状态;
- 就绪态(Ready): 一个进程获取到CPU分配的空间与地址(除CPU时间片),如果获得时间片则转入运行态;
- 运行态(Running): 当一个进程得到CPU调度正在处理机上运行时的状态;
- 睡眠/挂起态: 由于某些资源暂时获得不到从而进入“睡眠态”,进行将被挂起;
- 阻塞/暂停态(Blocked): 由于当前进程被中断或者其他事件导致而暂停运行时的状态;
- 结束/僵尸态(Exit): 一个进程正在从系统中消失时候的状态,这是因为进程结果或其他因流产所导致。
- 死亡态: 进程生命周期结束,将所占用的资源归还系统