进程、线程、多进程、多线程

一、进程和线程的区别
(1)进程:资源分配的最小单位;线程:程序执行的最小单位(CPU调度和分派的基本单位)。
(2)进程有独立的地址空间,每个进程都有自己的数据段、代码段和堆栈段;线程没有单独的地址空间,它包含独立的栈和CPU寄存器(同一进程内的线程共享进程的地址空间)。
(3)一个进程崩溃后,在保护模式下不会对其它进程产生影响;一个线程死掉就等于整个进程死掉。
(4)进程之间的通信只能通过进程通信的方式进行;线程之间的通信比较方便,同一进程下的线程共享数据(如全局变量,静态变量),通过这些数据来通信不仅快捷而且方便,但要考虑同步与互斥。
(5)每个独立的进程都有自己的一个程序入口,顺序执行序列和程序的出口;线程不能独立执行,必须依附在程序之中,由应用程序提供多个线程的并发控制。
(6)启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段;运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间。并且线程之间切换所需的时间远远小于进程间切换所需要的时间。

进程和线程都是一个时间段的描述,是cpu工作时间段的描述,  不过是颗粒大小不同。
(1)CPU+RAM+各种资源(显卡、光驱、键盘、GPS等外设)构成我们的电脑,但是电脑的运行实际就是CPU和相关寄存器以及RAM之间的事情
(2)CPU运行太快了,寄存器仅仅能追上它的脚步,RAM和挂在各总线上的设备完全不能望其项背。当多个任务要执行的时候,轮流着来?或者谁优先级高谁来?不管怎么样的策略,在CPU看来就是轮流着来
(3)执行一段程序代码,当得到CPU的时候,相关的资源必须也已经就位(显卡,GPS等),然后CPU开始执行。这里除了CPU以外所有的就构成了这个程序的执行环境(也就是所定义的程序上下文)。当这个程序执行完,或者分配给他的CPU执行时间用完了,那它就要被切换出去,等待下一次CPU的调用。在被切换出去的最后一步工作就是保存程序上下文,因为这是它下次被CPU调用的运行环境,必须保存。
(4)在CPU看来所有的任务都是一个一个轮流执行的,具体的轮流方法:先加载程序A的上下文,然后开始执行A,保存程序A的上下文,调入下一个要执行的程序B的程序上下文,然后开始执行B,保存程序B的上下文......
进程时间段 = CPU加载上下文时间 + CPU执行时间 + CPU保存上下文时间
(5)进程的颗粒度太大,每次都要有上下文的调入,保存,调出。要实现一个程序,实际分成a,b,c等多个块组合而成。具体的执行如下,
程序A得到CPU => CPU加载上下文 => 执行程序A的a小段 => 执行A的b小段 => 执行A的c小段 => CPU保存A的上下文
a,b,c的执行共享了A的上下文,CPU在执行的时候没有进程上下文切换。a,b,c就是线程,也就是说线程是共享了进程的上下文环境的更为细小的CPU时间段。

二、线程的概念,线程的基本状态及状态之间的关系
线程,有时称为轻量级进程,是CPU使用的基本单元,它由线程ID、程序计数器、寄存器集合、堆栈组成。它与属于同一进程的其他线程共享其代码段、数据段和其他操作系统资源(如打开文件和信号)。
线程有五种状态:新建状态、就绪状态、运行状态、阻塞状态、死亡状态。

(1)新建状态(new):新创建了一个线程对象
(2)就绪状态(runnable):线程对象创建后,其他线程调用了该对象的start()方法,该状态的线程位于"可运行线程池"中,变得可运行,只等待获取CPU的使用权
(3)运行状态(running):就绪状态的线程获取了CPU,执行程序代码
(4)阻塞状态(blocked):阻塞状态是因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行态。
(5)死亡状态(Dead):线程执行完了或者因异常退出,该线程结束生命周期。

三、多进程与多线程的区别

对比维度

多进程

多线程

总结

数据共享、同步

数据共享复杂,需要用IPC;数据是分开的,同步简单

因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂

各有优势

内存、CPU

占用内存多,切换复杂,CPU利用率低

占用内存少,切换简单,CPU利用率高

线程占优

创建销毁、切换

创建销毁、切换复杂,速度慢

创建销毁、切换简单,速度很快

线程占优

编程、调试

编程简单,调试简单

编程复杂,调试复杂

进程占优

可靠性

进程间不会互相影响

一个线程挂掉将导致整个进程挂掉

进程占优

分布式

适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单

适应于多核分布式

进程占优
(1)需要频繁创建销毁的优先用线程
(2)需要进行大量计算的优先使用线程
(3)可能要扩展到多机分布的用进程,多核分布的用线程
(4)强相关的处理用线程,弱相关的处理用进程
一般server需要完成如下任务:消息收发、消息处理。消息收发和消息处理就是弱相关的任务,可以分进程设计。
消息处理又可能分为消息解码、业务处理,这两个任务相对来说相关联性就很强,可以分线程设计。

四、线程安全
如果多线程的程序运行结果是可预期的,而且与单线程的程序运行结果是一样的,那么说明是"线程安全"的。、
    线程安全的条件:要确保函数线程安全,主要考虑的是线程之间的共享变量。属于同一进程的不同线程会共享进程的内存空间中的全局区和堆,而私有的线程空间则主要包括栈和寄存器。因此,对于同一进程的不同线程来说,每个线程的局部变量都是私有的,而全局变量、局部静态变量、分配于堆的变量都是共享的。在对这些共享变量进行访问时,如果要保证线程安全,则必须通过加锁的方式。


五、多线程同步与互斥有几种实现方法?都是什么?

临界区、互斥量、事件、信号量

(1)临界区:同一进程内,实现互斥
在任意时间只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后,其他试图访问临界区的线程都被挂起。
临界区释放后,其他线程可以继续抢占。
(2)互斥量:可以跨进程,实现互斥
互斥对象只有一个,只有拥有互斥对象的线程才具有访问资源的权限。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。互斥量与临界区的作用非常相似,但互斥量可以命名,也就是说它可以跨进程使用,所以创建互斥量需要的资源更多。
(3)信号量:可以跨进程,主要实现同步
它允许多个线程同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。
将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就减1,只要当前可用资源计数>0,就可以发出信号量信号。
当可用计数=0时,则说明当前占用资源的线程数已经达到了所允许的最大数目,不允许其他线程进入。
(4)事件:可以跨进程,实现同步
通过通知操作的方式来保持线程的同步,还可以实现对多个线程的优先级比较的操作。
总结:
临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
互斥量:为协调共同对一个共享资源的单独访问而设计的
信号量:为控制一个具有有限数量用户资源而设计。
事件:用来通知线程有一些事件已发生,从而启动后续任务的开始。

六、多线程同步和互斥有何异同,在什么情况下分别使用他们,举例说明。
同步:是指多个线程(或进程)为了合作完成任务,必须严格按照规定的某种先后次序来运行。
互斥:是指系统中的某些共享资源,一次只允许一个线程访问。当一个线程正在访问该临界资源时,其它线程必须等待。[具有唯一性、排它性、无序的]
同步其实已经实现了互斥,所以同步是一种更为复杂的互斥
互斥是一种特殊的同步
同步的例子:为了求出1到n的平均值,需要三个线程协调它们的工作次序来完成,这就是同步。

互斥的例子:int a = 200; int b = 100。线程A执行操作:将*a的值减少50,*b的值增加50。线程B执行:打印出(a 跟 b 指向的内存的值的和)
    如果串行运行:A: *a -= 50; *b += 50; B: printf("%d\n", *a + *b);
    如果并发执行,则有可能会出现一下调度:*a -= 50; printf("%d\n", *a + *b); *b += 50;

七、进程间通信
进程间通信主要包括:管道和命名管道(FIFO文件)、信号量、消息队列、共享内存区、套接字(socket)
(1)管道
管道是进程之间的一个单向数据流:一个进程写入管道的所有数据都由内核定向到另一个进程,另一个进程由此就可以从管道中读取数据。
$ ls | more    // 第一个进程(ls)的标准输出被重定向到管道中,第二个进程(more)从这个管道中读取输入 
管道被看作是打开的文件,但在已安装的文件系统中没有响应的映像。
缺点:只能用于具有亲缘关系的进程间通信。无法打开已经存在的管道,这就使得任意两个进程不可能共享同一个管道,除非管道由一个共同的祖先进程创建。
(2)命名管道(FIFO文件)
FIFO不同于管道之处在于它提供了一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程与FIFO的创建进程之间)。
FIFO是一种双向通信管道,可能以读/写模式打开一个FIFO。
FIFO索引节点出现在系统目录树上,而不是pipefs特殊文件系统中。
FIFO往往都是多个写进程,一个读进程。
(3)信号量
本质上信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。一般说来,为了获取共享资源,进程需要执行下列操作:
(a)测试控制该资源的信号量
(b)若此信号量的值为正,则允许进程使用该资源,进程将信号量的值减1;
(c)若此信号量的值为0,则该资源目前不可用,进程进入睡眠状态,直到信号量值大于0,进程被唤醒,转入步骤(a);
(d)当进程不再使用一个信号量控制的资源时,信号量值加1,如果此时有进程正在睡眠等待此信号量,则唤醒此进程。
(4)消息队列
进程彼此之间可以通过IPC消息进行通信。进程产生的每条消息都被发送到一个IPC消息队列中,这个消息一直存放在队列中直到另一个进程将其读走为止。
只要进程从IPC消息队列中读出一条消息,内核就把这个消息删除,因此只能有一个进程接收一条给定的消息。
消息是由固定大小的首部和可变长度的正文组成的,可以使用一个整数值(消息类型)标识消息,这就允许进程有选择地从消息队列中获取消息。
(消息队列是使用一个链表实现的,因此消息可以按照非先进先出的次序获得。新消息通常都放在链表的末尾。)
(5)共享内存区
共享内存就是运行两个或多个进程通过把公共数据结构放入一个共享内存区来访问它们。如果进程要访问这种存放在共享内存区的数据结构,就必须在自己的地址空间中增加一个新内存区,将它映射到与这个共享内存区相关的页框。
优点:进程间的数据不用传送,而是直接访问内存,加快了程序的效率
缺点:共享内存没有提供同步机制
(6)套接字
通过套接字可以在计算机内通信,也可以在计算机之间通信。
套接字的特性由三个属性确定:域、类型和协议。
域:指定套接字通信中使用的网络介质。最常见的套接字域是AF_INET,它指的是IPv4因特网域。AF_UNIX,指的是UNIX域。
类型:确定套接字的类型,进一步确定通信特征
协议:指定协议类型(如:TCP, UDP, IP, ICMP等)
总结:
管道:速度慢,容量有限,只有父子进程能通讯。
FIFO:任何进程都能通讯,但速度慢。
信号量:不能传递复杂消息,只能用于同步。
消息队列:容易受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题。
共享内存区:能够很容易控制容量,速度快,但要保持同步。

八、linux内核中使用的同步技术(9种)
技术                     说明                                                     适用范围
每CPU变量            在CPU之间复制数据结构                        所有CPU
原子操作               对一个计数器原子地"读-修改-写"的指令  所有CPU
内存屏障               避免指令重新排序                                 本地CPU或所有CPU
自旋锁                   加锁时忙等                                           所有CPU
信号量                   加锁时阻塞等待(睡眠)                           所有CPU
顺序锁                   基于访问计数器的锁                              所有CPU
本地中断的禁止      禁止单个CPU上的中断处理                    本地CPU
本地软中断的禁止   禁止单个CPU上的可延迟函数处理          本地CPU
读-拷贝-更新(RCU)  通过指针而不是锁来访问共享数据结构   所有CPU
(1)每CPU变量:主要是数据结构的数组,系统的每个CPU对应数组的一个元素。一个CPU不应该访问与其他CPU对应的数组元素,
但它可以随意读或修改它自己的元素而不用担心出现竞争条件,因为它是唯一有资格这么做的CPU。
使用情况:系统CPU上的数据在逻辑上是独立的。
在单处理器和多处理器系统中,内核抢占都可能使每CPU变量产生竞争条件。因此,内核控制路径应在禁用抢占的情况下访问每CPU变量。
(2)原子操作:借助于汇编语言指令中对"读--修改--写"具有原子性的汇编指令来实现
(3)内存屏蔽:在原语之后的操作开始执行之前,原语之前的操作已经完成。
(4)自旋锁:用来在多处理器环境中使用。如果内核控制路径发现自旋锁"开着",就获取锁并继续自己的执行。相反,如果内核控制路径发现锁由运行在另一个CPU上的内核控制路径"锁着",就在周围"旋转",反复执行一条紧凑的循环指令,直到锁被释放。
自旋锁所保护的每个临界区都是禁止内核抢占的,单处理器系统上,自旋锁不起锁的作用,仅仅是禁止或启用内核抢占
(5)信号量:当内核控制路径试图获取内核信号量所保护的忙资源时,相应的进程被挂起。只有在资源被释放时,进程才再次变为可运行的。
只有可以睡眠的函数才能获取内核信号量;中断处理程序和可延迟函数都不能使用内核信号量。
(6)顺序锁:与自旋锁相似,只是顺序锁中写者比读者有较高的优先级,即使在读者正在读的时候也允许写者继续允许。
(7)本地中断的禁止:保证即使硬件设备产生了一个IRQ信号时,内核控制路径也会继续允许,从而使中断处理例程访问的数据结构受到保护。
禁止本地中断并不保护运行在另一个CPU上的中断处理程序对数据结构的并发访问,因此在多处理器系统中,禁止本地中断经常与自旋锁结合使用
(8)本地软中断的禁止:由于软中断是在硬件中断处理程序结束时开始运行的,所以最简单的方式是禁止那个CPU上的中断。
(9)读-拷贝-更新(RCU):主要用于保护被多个CPU读的数据结构,允许多个读者和写者同时运行,且RCU是不用锁的。
使用限制:RCU只保护被动态分配并通过指针引用的数据结构;在被RCU保护的临界区中,任何内核控制路径都不能睡眠
原理:当写者要更新数据时,它通过引用指针来复制整个数据结构的副本,然后对这个副本进行修改。修改完毕后,写者改变指向原数据结构的指针,使它指向被修改后的副本(指针的修改时原子的)。

九、什么是死锁,如何避免死锁
死锁:指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
死锁的发生必须具备以下四个必要条件:
(1)互斥条件
(2)请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程阻塞,但又对自己已获得的其他资源保持不放。
(3)不可抢占条件:进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
(4)循环等待条件:在发生死锁时,必然存在一个进程---资源的环形链。

十、其他
(1)以下多线程对int型变量x的操作,哪几个需要进行同步:
(A)x=y;(B)x++;(C)++x; (D)x=1;
答:A,B,C,显然y的写入与x读要同步。
(2)多线程中栈与堆是公有的还是私有的
答:栈私有,堆公有。栈一般存放局部变量
(3)一个全局变量tally,两个线程并发执行(代码段都是ThreadProc),问两个线程都结束后,tally取值范围。
inttally = 0;//glable
voidThreadProc()
{
    for(inti = 1; i <= 50; i++)
        tally += 1;
}

答:两个线程串行时,结果最大为100。当某个线程运行结束,而另一个线程刚取出还为计算时,结果最小为50。