本次笔记内容: 7.8 为什么使用线程 7.9 什么是线程 7.10 线程的实现 7.11 上下文切换 7.12 进程控制——创建进程 7.13 进程控制——加载和执行进程 7.14 进程控制——等待和终止进程
使用线程案例:
编写一个MP3播放软件,核心功能模块有三个: 从MP3音频文件当中读取数据; 对数据进行解压缩; 把解压后的音频数据播放出来。https://img-blog.csdnimg.cn/20190922130343892.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjgxNTYwOQ==,size_16,color_FFFFFF,t_70
如上图,单线程有可能由于I/O阻塞,声音不连贯。
如上图,将三个模块分为三个进程,可能会造成开销、通讯等额外问题。
由此,提出线程,是一种满足如下特性的实体:
实体之间可以并发地执行;实体之间共享相同的地址空间。如上图,线程也包括TCB、PC(程序运输器)、SP(堆栈)、寄存器信息等等(因为有不同控制流)。但是内存空间中,有独立部分,也有共享部分。
如上图,线程等于进程减共享资源。线程的优点也是线程的缺点,由于共享资源,安全性得不到保障。
很强调性能、执行代码相对统一(天气预报、水利、空气动力)的计算,使用线程;浏览器打开n个网页,采取多个进程机制实现,这样的话不会因一个网页崩溃而浏览器崩溃。比如Chrome。
如上图,线程需要单独的寄存器、堆栈,但是数据等资源是共享的。
主要有三种线程实现方式:
用户线程,在用户空间实现(POSIX Pthreads,Mach C-threads,Solaris threads);内核线程,在内核中实现(Windows,Solaris,Linux);轻量级进程:在内核中实现,支持用户线程(Solaris(LightWeight Process))。如上图,在用户空间实现线程,操作系统看不到线程的实现,但是可以看到进程的状态。线程的实现不依靠操作系统调度。
在用户空间实现线程机制,它不依赖于操作系统的内核,由一组用户级的线程库函数来完成线程的管理,包括进程的创建、终止、同步、调度。
由于用户线程的维护由相应进程来完成(通过线程库函数),不需要操作系统内核了解用户线程的存在,可用于不支持线程技术的多进程操作系统;每个进程都需要它自己私有的线程控制块(TCB)列表,用来跟踪记录它的各个线程的状态信息(PC、栈指针、寄存器),TCB由线程库函数来维护;用户线程的切换也是由线程库函数来完成,无需用户态/核心态切换,所以速度特别快;运行每个进程拥有自定义的线程调度算法。用户线程缺点:
如果一个线程发起系统调用而阻塞,则整个进程在等待;当一个线程开始运行后,除非它主动地交出CPU的使用权,否则它所在的进程当中的其他线程将无法运行;由于时间片分配给进程,故与其他进程比,在多线程执行时,每个线程得到的时间片较少,执行会较慢。如上图,内核线程,就是操作系统能看见的线程。线程的TCB被放在内核中。Windows就是如此设计的,因此CPU的调度单位也是线程,而非进程。
在支持内核线程的操作系统中,由内核来维护进程和线程的上下文信息(PCB和TCB); 线程的创建、终止和切换都是通过系统调用/内核函数的方式进行,由内核来完成,因此系统开销较大; 在一个进程当中,如果某个内核线程发起系统调用而被阻塞,并不会影响到其他内核线程的运行; 时间片分配给线程,多线程的进程获得更多CPU时间; Windows NT和Windows 2000/XP支持内核线程。
它是内核支持额用户线程,一个进程可有一个或多个轻量级进程,每个量级进程由一个单独的内核线程来支持。(Solaris/Linux)
如上图所示轻量级进程。
停止当前运行进程(从运行状态改变成其他状态),并且调度其他进程(转变成运行状态)。
必须在切换之前存储许多部分的进程上下文;必须能够在之后恢复他们,所以进程不能显示它曾经被暂停过;必须快速(上下文切换是非常频繁的)需要存储的上下文有: 寄存器(PC,SP,…),CPU状态等;一些时候可能会费时,所以应尽量避免切换。如上图,展示了进程P1切换到P2再恢复运行状态的过程。
实际操作过程中,与硬件紧密相连,因此使用汇编代码进行编写。
如上图,PCB放在不同的队列(链表实现)中。
进程创建是一种系统调用,在不同的操作系统当中,进程创建API不同。
Windows进程创建API:CreateProcess(filename)Unix进程创建系统调用:fork/execfork()把一个进程复制成两个进程 parent(old PID),child(new PID)exec()用新程序来重写当前进程,PID没有改变fork()创建一个继承的子进程复制父进程的所有变量和内存复制父进程的所有CPU寄存器(有一个寄存器例外)fork()的返回值,子进程为0,父进程返回子进程标识符。如上图,根据ID不同,父子进程执行不同的代码。
注意红字的printf(),exec()将新的程序覆盖了此段代码,红字的printf()不可能被执行到,因此如果有printf(),则出错。
上图程序中应该增加elseif(pid < 0){}。
如上图,fork()只改变了程序的pid,128是127的fork(),但是其他的代码没有改变,而exec()将程序的代码改变,open files也改变。
上图是fork()后,exec()前的状态。
下图是exec()后的状态,整个控制流完全改变。
exec()调用允许一个进程“加载”一个不同的程序并且在main()开始执行(事实上_start);它允许一个进程指定参数数量(argc)和它的字符串参数数组(argv);如果调用成功,它是相同的进程,但运行了一个不同的程序;代码、stack(栈)、heap(堆)重写(覆盖)。考虑fork()的执行开销:
在99%的情况里,调用fork()后调用exec(),fork()将父进程的地址空间全部拷贝到新空间,但是exec()重写空间,因此fork()之前的行为存在巨大无用开销;因此提出vfork()技术,在后面的程序调用exec()的情况,只做轻量级复制;又提出Copy on Write(COW)技术,只在写的时候复制。fork()时,只复制与地址空间管理相关的元数据、页表等等,如果需要写操作,才进行复制。wait()系统调用是父进程用来等待子进程结束。
一个子进程向父进程返回一个值,所以父进程必须接受这个值并处理;(子进程无法释放掉自己的PCB,父进程在子进程执行结束后,接收返回值,帮助子进程释放内存中的PCB等资源)
wait()系统调用承担上述要求。
进程结束执行后,其父进程执行wait()(存活者)时,调用exit()。
exec()执行完毕,不属于五种状态之一(因已经无法回到用户状态),是叫僵尸状态。
如果父进程先于子进程终止,子进程的PCB还可以回收吗?最早进程称为祖宗进程/根进程,会定期扫描是否存在僵尸进程。
如上图,相比与之前的五种状态,上图增加了Zombie状态。
执行exec()时,进程可能处于不同的状态。
