最基础的定义是:
进程是计算机分配资源的基本单位
线程是cpu调度的基本单位
进程
###进程的创建
进程的出现是为了让一台计算机能同时处理多道程序,因此为了更方便的管理每一个进程,进程在创建的时候会由内核生成一个PCB结构体用于管理每一个进程,而且会分配一个进程号。在linux环境下使用top或者ps命令就可以查看到进程的进程号等相关信息。
那么在进程建立的时候,首先会创建PCB结构体并放进PCB表中,然后将数据读进4G内存空间(也是为了让多道程序能没有互相干扰的运行,所以需要给每一个进程分配一个4G的内存空间,因此也出现了内存管理技术)。要读入的数据分为4(5)段:
- 代码段、**数据段(已初始化、未初始化)**长度固定位于最低的地址段。
- 堆在其次由低向高生长。
-
栈从最高地址开始(3G从开始,因为最高地址空间的1G为内核空间)由高向低生长。
参考自1
子进程的创建
子进程创建的时候也会复制父进程的PCB,但是会修改为新的进程号等,并且在PCB中会表明父子进程的关系。所以会有某文件被一个进程打开后,该进程分出子进程,然后父子进程都会打开这个文件。
但是在读入内存空间时,它由父进程复制过来堆栈和数据段,代码段是与父进程共享的(如果有声名为进程共享的数据也是会共享的)。
而且复制时候是“lazy-copy”,被暂且标记为只读,当要写入时候再真实复制。
参考自2
进程的调度
这里写的是进程的调度并没有错。线程是cpu调度的基本单位,是在cpu视角下对线程分配的方式。而在这里cpu是一种资源,所以进程的调度其实是资源的分配,是操作系统实现的将cpu资源分配给进程的过程。
操作系统书中会讲到进程的五种状态以及他们之间的转换,进程的状态在PCB中都友记载,进程的调度就是对进程的PCB进行调度。因此PCB表有两种方式存储,一种直接是表调配相应的顺序区分状态,另一种是另加索引的方式。
参考自3
cpu在进行进程切换的时候需要保存寄存器状态、pc指针、PCB调度等操作,会很浪费资源。因此需要尽量减少调度。具体的调度方式就有很多种了,在王道操作系统中专门一节内容会讲到:
- 先来先服务(理解为使用队列实现)
- 短作业优先(理解为使用优先队列(队列加堆)实现)
- 优先级调度、高响应比优先、时间片轮转、多级反馈队列等
还有很多性能参数:cpu利用率,系统吞吐量,周转时间,等待时间,响应时间。
并发
用时间片轮转来举例的话,处理器在同一时间只会处理一个任务,但是这一秒处理任务A,下一秒处理任务B,在宏观上看,AB两个任务的是同时进行的,所以说并发需要软件实现。可以理解为假的“并”,就像分手厨房的单人模式,屏幕里是多个人在干活,其实只有你一个人在玩,笑。
这个是在单核时期,多个用户提交的任务能一起实现,不会使得某用户“饿死”而实现的办法。
并行
随着多处理器的出现,不再是同一时间只能处理一个任务了,而是有几核就能够同时处理几个任务,截取某一个时刻会看到是真是的同时有多个任务在一起被处理,所以说并行需要硬件的支持。理解为真的“并”,就像有了女朋友(基友)之后玩分手厨房,笑。
进程的通信
进程之间的通信既包括互相传递数据也包括了加锁解锁和同步等操作。书上说进程之间的通信方式有共享内存、信号量、管道等方式。但是因为进程的内存空间是独立的,所以需要将用户空间的一块内存与内核空间的一段内存相映射,然后多个进程都映射到同一块内核的内存,这样就能有共享的内存了。这也是上面的三种方法的实现原理。具体的内存映射的方式有mmap()或者是管道的buffer。
这是一个c++使用进程共享的锁的例子4,这里使用到了另一种进程创建方式fork(),也就是创建子进程,这里就是在fork前将锁声明为进程共享,然后fork之后就可以使用它来进行进程间的同步。
一个进程间信号量同步的例子5
linux信号的讲解6
线程
线程可以类比为一种轻量级进程。分为了内核线程和用户线程。
线程的创建
线程是在进程下进行创建的,线程与进程共享资源,但是栈是自己独立的,并有相应的范围,因此体现在进程的内存空间里是会有多个栈。
线程在创建的时候也会有相应的TCB结构,只不过相较于进程的PCB来说会轻量很多。而且内核线程的TCB是在内核里维护的,而用户线程的TCB则由用户维护。
线程的调度
如果是内核线程的话,线程的创建回收以及调度都是由内核来完成,要进入内核态。但是内核线程的切换效率也会比进程切换小绿要高。
如果是用户线程的话,线程的切换由用户实现(由运行在用户态的多线程库实现),因此在内核看来是没有切换的,不会进内核态,效率也会更加高一些。然而也有一定的弊端,一旦其中某个用户线程阻塞在io,会产生系统调用进入内核态进行线程切换,而这次切换是将该内核线程(对应了多个用户线程的这个内核线程)挂起,从而导致其他的用户线程也会被暂停。
解决办法就是用户线程库创建线程池,建立多对多的模型,这样的话io阻塞也只会阻塞一条内核线程,池里还有其他内核线程活跃。
线程的通信
多线程之间共享了堆和数据段的内存空间,最简单的办法就是使用全局变量来实现。
linux c 线程间同步的具体讲解7
后记
关于线程进程的自己的一点理解:
-
常说的CPU指的是放在主板上的那一张芯片,他的内部会集成多个核心,会被称为六核处理器(真的是六个)。而硬件厂商会使用超线程技术HT,在六个核心的基础上实现“六核十二线程”,因此,在系统层面看来是有十二个核心,可以当作十二核的处理器来使用。
-
在每一个CPU中存在三级缓存,L3缓存是share-memory
- 当操作系统将cpu分配给进程A之后,cpu会将该进程下的若干线程分配给若干个核心去处理,而这多个核心共享的内存段加载到L3缓存中后可以共享着使用。
- 而如果是多个进程每个进程一个线程的情况在cpu的多个核上同时处理的话,几个进程间不好产生共享的内存,因此在L3与主存间进行调换会更加频繁。
- 即使是在线程切换时,如果切换的是同一个进程下的两个线程,内存命中率也会更加高。所以在多个进程同时处理的时候,同一个进程的多个线程会偏向于由固定的几个处理器来处理。当出现空闲的核心时,再从其他核心待处理的任务里分配任务来处理。
- 关于内存映射的一些点
- 使用read或write进行文件的读写时,首先第一步,会进入内核态,并将相应的数据块从辅存(硬盘)拷贝到主存(内存),整个流程是cpu调配开始,由DMA实现数据传输,完成后产生中断交给cpu处理。然后第二步,由cpu将文件内容从内核空间拷贝到用户空间。这里是两次拷贝。
- 使用mmap进行文件映射的时候,第一步也是DMA拷贝,而第二步是将用户空间的一块内存映射到内核空间里的该数据上,因此只用了一次拷贝,而且还节省了cpu时间。在内核空间中存的位置叫做页缓存8。还有一个关于mmap的深度讲解9.
-
关于pthread库实现的线程,有说它内部是由类似于fork的clone系统调用来实现的,因此是内核线程。有说根据linux版本不同而不一样的。这里涉及到POSIX和内核的知识,就之后再找答案了。
-
后要继续学习linux内核的相关知识,进程线程的调度,还有POSIX是个什么。
参考资料
- 非常棒的一篇linux内存空间的讲解 传送门
- 关于并发并行的讲解 传送门
- 关于多线程多进程效率的讨论 传送门
- 操作系统在多cpu上调度的讲解 传送门
- 爱丁堡大学关于操作系统线程的ppt课件(英文预警) 传送门
-
https://baijiahao.baidu.com/s?id=1658841246179232657&wfr=spider&for=pc 进程的内存布局讲解 ↩︎
-
https://blog.csdn.net/shreck66/article/details/47039937/ 关于fork的讨论 ↩︎
-
https://blog.csdn.net/github_36487770/article/details/60144610 一个很不错的进程线程的讲解 ↩︎
-
https://www.cnblogs.com/sssblog/p/10457114.html ↩︎
-
https://blog.csdn.net/yishizuofei/article/details/78318967 ↩︎
-
https://www.cnblogs.com/yongfengnice/p/11953839.html ↩︎
-
https://www.cnblogs.com/wsw-seu/p/8036218.html https://www.cnblogs.com/lenmom/p/7998969.html ↩︎
-
https://blog.csdn.net/gdj0001/article/details/80136364 页缓存和文件io的深入讲解(很深) ↩︎
-
https://blog.csdn.net/qq_33611327/article/details/81738195 ↩︎