1.进程&线程
1.进程
定义:一个正在执行的程序 一个正在计算机上执行的程序实例 pid :进程号
进程与线程的区别总结:
本质区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
包含关系:一个进程至少有一个线程,线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
资源开销:每个进程都有独立的地址空间,进程之间的切换会有较大的开销;线程可以看做轻量级的进程,同一个进程内的线程共享进程的地址空间,每个线程都有自己独立的运行栈和程序计数器,线程之间切换的开销小。
影响关系:一个进程崩溃后,在保护模式下其他进程不会被影响,但是一个线程崩溃可能导致整个进程被操作系统杀掉,所以多进程要比多线程健壮
进程的状态
进程的状态及状态转换
1.运行态:【基本状态】占有CPU,并在CPU上运行;单核处理机的情况下,某一时刻只能处理一个进程。
2.就绪态:【基本状态】已经具备运行条件,但没有空闲的CPU,导致不能运行。万事俱备,各种资源都已经获得,只差一个CPU。
3.阻塞态(等待态):【基本状态】因等待某一事件而暂时不能运行。如:等待操作系统分配打印机、等待读磁盘操作的结果。
4.创建态(新建态):进程正在被创建,操作系统为进程分配资源,初始化PCB;
5.终止台(结束态):进程正在从系统中撤销,操作系统会回收进程拥有的资源、撤销PCB
进程间通信
管道:
管道是最简单,效率最差的一种通信方式。
管道本质上就是内核中的一个缓存,当进程创建一个管道后,Linux会返回两个文件描述符,一个是写入端的描述符,一个是输出端的描述符,可以通过这两个描述符往管道写入或者读取数据。
如果想要实现两个进程通过管道来通信,则需要让创建管道的进程fork子进程,这样子进程们就拥有了父进程的文件描述符,这样子进程之间也就有了对同一管道的操作。
缺点:
半双工通信,一条管道只能一个进程写,一个进程读。
一个进程写完后,另一个进程才能读,反之同理。
消息队列:
管道的通信方式效率是低下的,不适合进程间频繁的交换数据。这个问题,消息队列的通信方式就可以解决。A进程往消息队列写入数据后就可以正常返回,B进程需要时再去读取就可以了,效率比较高。
而且,数据会被分为一个一个的数据单元,称为消息体,消息发送方和接收方约定好消息体的数据类型,不像管道是无格式的字节流类型,这样的好处是可以边发送边接收,而不需要等待完整的数据。
但是也有缺点,每个消息体有一个最大长度的限制,并且队列所包含消息体的总长度也是有上限的,这是其中一个不足之处。
另一个缺点是消息队列通信过程中存在用户态和内核态之间的数据拷贝问题。进程往消息队列写入数据时,会发送用户态拷贝数据到内核态的过程,同理读取数据时会发生从内核态到用户态拷贝数据的过程。
共享内存:
共享内存解决了消息队列存在的内核态和用户态之间数据拷贝的问题。
现代操作系统对于内存管理采用的是虚拟内存技术,也就是说每个进程都有自己的虚拟内存空间,虚拟内存映射到真实的物理内存。共享内存的机制就是,不同的进程拿出一块虚拟内存空间,映射到相同的物理内存空间。这样一个进程写入的东西,另一个进程马上就能够看到,不需要进行拷贝。
信号量:
当使用共享内存的通信方式,如果有多个进程同时往共享内存写入数据,有可能先写的进程的内容被其他进程覆盖了。
因此需要一种保护机制,信号量本质上是一个整型的计数器,用于实现进程间的互斥和同步。
信号量代表着资源的数量,操作信号量的方式有两种:
P操作:这个操作会将信号量减一,相减后信号量如果小于0,则表示资源已经被占用了,进程需要阻塞等待;如果大于等于0,则说明还有资源可用,进程可以正常执行。
V操作:这个操作会将信号量加一,相加后信号量如果小于等于0,则表明当前有进程阻塞,于是会将该进程唤醒;如果大于0,则表示当前没有阻塞的进程。
(1)信号量实现互斥:
信号量初始化为1
进程 A 在访问共享内存前,先执行了 P 操作,由于信号量的初始值为 1,故在进程 A 执行 P 操作后信号量变为 0,表示共享资源可用,于是进程 A 就可以访问共享内存。
若此时,进程 B 也想访问共享内存,执行了 P 操作,结果信号量变为了 -1,这就意味着临界资源已被占用,因此进程 B 被阻塞。
直到进程 A 访问完共享内存,才会执行 V 操作,使得信号量恢复为 0,接着就会唤醒阻塞中的线程 B,使得进程 B 可以访问共享内存,最后完成共享内存的访问后,执行 V 操作,使信号量恢复到初始值 1。
(2)信号量实现同步:
由于多线程下各线程的执行顺序是无法预料的,有些时候我们希望多个线程之间能够密切合作,这时候就需要考虑线程的同步问题。
信号量初始化为0
如果进程 B 比进程 A 先执行了,那么执行到 P 操作时,由于信号量初始值为 0,故信号量会变为 -1,表示进程 A 还没生产数据,于是进程 B 就阻塞等待;
接着,当进程 A 生产完数据后,执行了 V 操作,就会使得信号量变为 0,于是就会唤醒阻塞在 P 操作的进程 B;
最后,进程 B 被唤醒后,意味着进程 A 已经生产了数据,于是进程 B 就可以正常读取数据了。
信号:
在Linux中,为了响应各种事件,提供了几十种信号,可以通过kill -l命令查看。
如果是运行在shell终端的进程,可以通过键盘组合键来给进程发送信号,例如使用Ctrl+C产生SIGINT信号,表示终止进程。
如果是运行在后台的进程,可以通过命令来给进程发送信号,例如使用kill -9 PID产生SIGKILL信号,表示立即结束进程。
Socket:
前面提到的管道,消息队列,共享内存,信号量和信号都是在同一台主机上进行进程间通信,如果想要跨网络和不同主机上的进程进行通信,则需要用到socket。
进程的创建:fork()
在父进程中,fork返回新创建子进程的进程ID
fork 是 创建一个子进程,并把父进程的内存数据copy到子进程中。
vfork是 创建一个子进程,并和父进程的内存数据share一起用。
exec族函数(execl、execlp、execvp) 进程替换映像
exec族函数函数的作用:
我们用fork函数创建新进程后,经常会在新进程中调用exec函数去执行另外一个程序。当进程调用exec函数时,该进程被完全替换为新程序。进程的ID没有改变。
exec函数族分别是:execl, execlp, execle, execv, execvp, execvpe
孤儿进程&僵尸进程
孤儿进程:
父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”,此时子进程就叫做孤儿进程。
Linux避免系统存在过多孤儿进程,init进程收留孤儿进程,变成孤儿进程的父进程。
Pid==1,init进程
僵尸进程:
一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁, 而是留下一个称为僵死进程的数据结构(系统调用exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵死进程,并不能将其完全销毁)。
在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等,但是仍然为其保留一定的信息(包括进程号进程PID,退出状态,运行时间等), 直到父进程通过wait/waitpid来取时才释放。此时该进程处于僵死状态,该进程成为僵死进程。
总结起来就两点:
1、子进程结束,父进程没结束,并且父进程未获取子进程的退出数据;
2、一个进程的进程主体完全释放,但是PCB还在。
三、使用阻塞调用避免产生僵死进程
这里的阻塞就要用到我们刚刚提到的wait()函数了,这个函数其原型为pid_t wait(int *stat),又主函数调用,wait()函数成功返回等待子进程的pid,失败返回-1。那什么时阻塞呢?阻塞就是函数贝调用后,并不能马上返回,而是要等待某件事的发生,就像我们过马路一样,只有当绿灯亮起来,我们才能通过。而wait()就是这个原理,既然子进程必须要结束时让父进程获取退出信息就能让其不成为僵死进程,那就让父进程阻塞,等子进程结束时,把信息获取之后,也就是让子进程瞑目之后,父进程才开始往下执行。
这虽然能解决僵死进程的问题但是显然这并不是一个好办法,父进程如果不能和子进程并发执行的话,那我们创建子进程的意义就没有了。同时一个wait只能解决一个子进程,如果有多个子进程就要用到多个wait,这也太糟糕了,所以我们就还得用其他方法解决僵死进程。
事实上,我们还有这么多方法能过避免僵死进程:
1、父进程通过wait和waitpid等函数等待子进程结束,这会导致父进程挂起
2、如果父进程很忙,那么可以用signal函数为SIGCHLD安装信号处理函数。子进程结束后,父进程会收到该信号,可以在信号处理函数中调用wait回收 。
3、如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD, SIG_IGN)通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收, 并不再给父进程发送信号。
或用sigaction函数为SIGCHLD设置SA_NOCLDWAIT,这样子进程结束后,就不会进入僵死状态
struct sigaction sa;
sa.sa_handler = SIG_IGN;
sa.sa_flags = SA_NOCLDWAIT;
sigemptyset(&sa.sa_mask);
sigaction(SIGCHLD, &sa, NULL);
4、fork两次,父进程fork一个子进程,然后继续工作,子进程fork一个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收还要父进程来做。
死锁
如果一组进程中每一个进程都在等待仅由该组进程中的其他进程才能引发的事件,那么该组进程是死锁的。
形成死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
处理死锁的思路如下:
预防死锁:破坏四个必要条件中的一个或多个来预防死锁
避免死锁:在资源动态分配的过程中,用某种方式防止系统进入不安全的状态。
检测死锁:运行时产生死锁,及时发现思索,将程序解脱出来。
解除死锁:发生死锁后,撤销进程,回收资源,分配给正在阻塞状态的进程。
预防死锁的办法:
破坏请求和保持条件:1.一次性的申请所有资源。之后不在申请资源,如果不满足资源条件则得不到资源分配。2.只获得初期资源运行,之后将运行完的资源释放,请求新的资源。
破坏不可抢占条件:当一个进程获得某种不可抢占资源,提出新的资源申请,若不能满足,则释放所有资源,以后需要,再次重新申请。
破坏循环等待条件:对资源进行排号,按照序号递增的顺序请求资源。若进程获得序号高的资源想要获取序号低的资源,就需要先释放序号高的资源。
死锁的解除办法:
1、抢占资源。从一个或多个进程中抢占足够数量的资源,分配给死锁进程,以解除死锁状态。
2、终止(撤销)进程:将一个或多个思索进程终止(撤销),直至打破循环环路,使系统从死锁状态解脱。
线程
线程间同步
1.互斥锁
互斥锁(又名互斥量),强调的是资源的访问互斥:互斥锁是用在多线程多任务互斥的,当一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这个资源。
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
读写锁
读写锁和互斥量类似,是另一种实现线程同步的方式,但是它将操作分为读、写两种方式,可以多个线程同时占用读模式,这样使得读写锁具有更高的并行性。相较于互斥锁而言读写锁有一定的性能提升,应对的是单写多读模型:
写独占:写锁占用时,其他线程加读锁或者写锁时都会阻塞(并非失败)
读共享:读锁占用时,其他线程加写锁时会阻塞,加读锁会成功
条件变量
并发有互斥和等待两大需求,前者是因为线程间存在共享数据依赖而后者是线程间存在依赖,条件变量正是为了解决等待需求。
条件变量本质上也是一个多线程间共享的全局变量,它的功能是阻塞线程,被阻塞的线程直到接收到“条件成立”的信号后才能继续执行。
条件变量并不是锁(但它几乎总是和互斥量一起使用的),而是线程间的一种通讯机制
条件变量本身也不包含条件,它被称为条件变量是因为它经常和条件语句(if/while)一起使用
假设线程A依赖线程B某个条件:
线程A获取mutex访问共享区域,判断条件是否满足
如果条件不满足,则调用wait方法等待条件达成
线程B准备好条件后通过发singal唤醒线程A
信号量
信号量分为有名信号量和无名信号量,无名信号量用于线程同步,有名信号量一般用于进程之间管理。
信号量本质上是一个非负的整数计数器,用于控制公共资源的访问,也被称为PV原子操作:
P操作:即信号量sem减一,若sem小于等于0则P操作被阻塞,直到sem变量大于0为止
V操作:即信号量sem加一
2.内存&其他
CPU 负载 和占用
CPU利用率:显示的是程序在运行期间实时占用的CPU百分比
CPU负载:显示的是一段时间内正在使用和等待使用CPU的平均任务数。CPU利用率高,并不意味着负载就一定大。举例来说:如果我有一个程序它需要一直使用CPU的运算功能,那么此时CPU的使用率可能达到100%,但是CPU的工作负载则是趋近于“1”,因为CPU仅负责一个工作嘛!如果同时执行这样的程序两个呢?CPU的使用率还是100%,但是工作负载则变成2了。所以也就是说,当CPU的工作负载越大,代表CPU必须要在不同的工作之间进行频繁的工作切换
堆:
由低地址向高地址增长
栈由操作系统自动分配释放 ,用于存放函数的参数值、局部变量等,生命周期随着函数的执行完成而结束。
栈:
由高地址向低地址增长
堆一般需要手动进去分配,申请全局变量等,需要手动释放或者程序结束自动释放(malloc(c)/new(C++))
2.内核态和用户态
1 虚拟内存被操作系统划分成两块:内核空间(1G)和用户空间(3G)
2 内核空间是内核代码运行的地方,用户空间是用户程序代码运行的地方
3 当进程运行在内核空间时就处于内核态,当进程运行在用户空间时就处于用户态
4 它们是隔离的 线程上下文切换就涉及用户态到内核态的转换
3.系统调用
系统内核通过包装一些能够实现特定功能的特殊硬件指令和硬件状态,即为内核函数,通过一组称为系统调用(system call)的接口呈现给用户,为系统调用而封装出来的API也达数百个。
举例: 进程管理:fork execve
文件管理 : open close
并行和并发
并发(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个CPU上运行。
并发不是真正意义上的“同时进行”,只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行。
如:打游戏和听音乐两件事情在同一个时间段内都是在同一台电脑上同一个CPU完成了从开始到结束的动作。那么,就可以说听音乐和打游戏是并发的。
并发示意图如下:
并行(Parallel),当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行。
还是刚才的例子:但是有多个CPU 玩游戏在1号CPU下运行,打游戏在二号CPU下运行,可以说他们是并行的。
行示意图如下:
3.虚拟地址空间
1 虚拟地址空间,操作系统为了使每个进程都拥有独立的地址空间,使得每个进程都隔离开,便为每个进程都分配了独立的地址空间。它可以用来加载程序数据(数据可能被加载到物理内存上,空间不够就加载到虚拟内存中
2、虚拟地址空间与物理地址空间的关系
我们在程序中使用的地址是虚拟地址,硬件CPU在执行的时候使用的是物理地址。那么进程中的数据是如何进出入到物理内存中的呢?其实是通过 CPU 中的内存管理单元 MMU(Memory Management Unit)从进程的虚拟地址空间中映射过去的。
通常有三种映射方式:分段,分页,段页式。
每个程序在被运行起来之后,它将拥有自己独立的虚拟地址空间这个虚拟地址空间的大小由计算机的硬件平台决定,具体是由cpu的位数决定的。比如32位的平台决定了虚拟地址空间位4G。
这4G空间的分配如下
1 内核空间
最上面的1G。
内核总是驻留在内存中,是操作系统的一部分。内核空间为内核保留,不允许应用程序读写该区域的内容或直接调用内核代码定义的函数。
2 栈(stack)
包括以下内容和用途:
1 函数的返回值和参数。
2 临时变量,包括非静态局部变量,以及编译器自动生成的临时变量。
3 保存上下文:包括函数调用前后需保持不变的寄存器。
3 内存映射段(mmap)
该区域用于映射可执行文件用到的动态链接库。,若可执行文件依赖共享库,则系统会为这些动态库在从0x40000000开始的地址分配相应空间,并在程序装载时将其载入到该空间。在Linux 内核中,共享库的起始地址被往上移动至更靠近栈区的位置。
4 堆(heap)
堆用于存放进程运行时动态分配的内存段,可动态扩张或缩减。堆中内容是匿名的,不能按名字直接访问,只能通过指针间接访问。当进程调用malloc©/new(C++)等函数分配内存时,新分配的内存动态添加到堆上(扩张);当调用free©/delete(C++)等函数释放内存时,被释放的内存从堆中剔除(缩减) 。
5 .BSS段
.BSS(Block Started by Symbol)段中通常存放程序中以下符号:
1 未初始化的全局变量和静态局部变量
2 初始值为0的全局变量和静态局部变量(依赖于编译器实现)
3 未定义且初值不为0的符号(该初值即common block的大小)
6 数据段(.Data)
数据段通常用于存放程序中已初始化且初值不为0的全局变量和静态局部变量。数据段属于静态内存分配(静态存储区),可读可写。
7 代码段(text)
代码段也称正文段或文本段,通常用于存放程序执行代码(即CPU执行的机器指令)。一般C语言执行语句都编译成机器代码保存在代码段。通常代码段是可共享的,因此频繁执行的程序只需要在内存中拥有一份拷贝即可。代码段通常属于只读,以防止其他程序意外地修改其指令(对该段的写操作将导致段错误)。某些架构也允许代码段为可写,即允许修改程序。
8 保留区
位于虚拟地址空间的最低部分,未赋予物理地址。任何对它的引用都是非法的,用于捕捉使用空指针和小整型值指针引用内存的异常情况。
分段的好处
进程运行过程中,代码指令根据流程依次执行,只需访问一次(当然跳转和递归可能使代码执行多次);而数据(数据段和BSS段)通常需要访问多次,因此单独开辟空间以方便访问和节约空间。
此外,临时数据及需要再次使用的代码在运行时放入栈区中,生命周期短。全局数据和静态数据可能在整个程序执行过程中都需要访问,因此单独存储管理。堆区由用户自由分配,以便管理。