操作系统(Operating System, OS),介于硬件资源和应⽤程序之间的⼀个系统软件。
操作系统位于硬件资源之上,管理硬件资源;应⽤程序之下,为应⽤程序提供服务,同时管理应⽤程序。
1.进程管理:
①进程控制:创建和撤销进程,分配资源、资源回收,控制进程运行过程中的状态转换。
②进程同步:多进程运行进行协调–进程互斥(临界资源上锁)、进程同步。
③进程通信:实现相互合作之间的进程的信息交换。
④调度:作业调度,进程调度。
2.内存管理:
为多道程序的运行提供良好的环境,提高存储器的利用率,方便用户使用,并能从逻辑上扩充内存。
①内存分配:静态分配、动态分配。
②内存保护:各在其内存空间内运行(设两界限寄存器),互不干扰。
③地址映射:地址空间中的逻辑地址转换为内存空间中与之对应的物理地址。
④内存扩充:借助于虚拟存储技术,逻辑上扩充内存容量。
3.设备IO管理:
完成用户进程提出的 I/O 请求,为其分配所需I/O设备,完成指定I/O操作;提高CPU和I/O设备的利用率,提高I/O速度,方便用户使用I/O设备。
①缓冲管理
②设备分配
③设备处理:设备驱动程序,用于实现CPU和设备控制器之间的通信。
4.文件管理:
对用户文件和系统文件进行管理以方便用户使用,并保证文件的安全性。
①文件存储空间的管理:为文件分配合理外存空间,文件存储空间的使用情况。
②目录管理:为每个文件建立一个目录项。
③文件的读/写管理和保护
计算机必要重要的硬件资源⽆⾮就是 CPU、内存、硬盘、I/O设备。
⽽这些资源总是有限的,因此需要有效管理,资源管理最终只有两个问题:资源分配、资源回收。
操作系统将硬件资源的操作封装起来,提供相对统⼀的接⼝(系统调⽤)供开发者调⽤。
如果没有操作系统,应⽤程序将直接面对硬件,除去给开发者带来的编程困难不说,直接访问硬件,使⽤不当极有可能直接损坏硬件资源。
即控制进程的⽣命周期:进程开始时的环境配置和资源分配,进程结束后的资源回收、进程调度等。
(1)进程调度能力: 管理进程、线程,决定哪个进程、线程使⽤CPU。
(2)内存管理能力: 决定内存的分配和回收。
(3)硬件通信能力: 管理硬件,为进程和硬件之间提供通信。
(4)系统调用能力: 应⽤程序进行更⾼限权运行的服务,需要系统调用,⽤户程序和操作系统之间的接口
操作系统的角度
计算机启动后启动的第⼀个软件就是操作系统,随后启动的所有进程都运行在操作系统之上,使⽤操作系统提供的服务,同时被操作系统监控,进程结束后也由操作系统回收。
进程角度
调用操作系统提供的服务,实现自己的功能。
操作系统的核心是内核(kernel),它独立于普通的应用程序,负责管理系统的进程、内存、设备驱动程序、文件和网络系统,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。
为了保证内核的安全,现在的操作系统一般都强制用户进程不能直接操作内核。
在 32 位的操作系统的虚拟地址空间中,最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF)由内核使用,称为内核空间。而较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF)由各个进程使用,称为用户空间。
因此,最高 1G 的内核空间是被所有进程共享的,只有剩余的 3G 才归每个进程自己使用。
CPU 的指令分为特权指令和非特权指令,有些指令使用不当会非常危险,比如清内存、设置时钟、修改用户访问权限、分配系统资源等等,可能导致系统崩溃。
当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。只有处于内核态的进程才能使用特权指令和非特权指令,即内核态进程可以调用系统的一切资源;而用户态进程只能使用非特权指令,也就是说用户态进程只能执行简单运算,不能直接调用系统资源。
CPU中有一个程序状态字PSW(Program Status Word),标志着线程的运行状态。用户态和内核态对应着不同的值,用户态为3,内核态为0。
对于 Linux 来说,通过区分内核空间和用户空间的设计,隔离了操作系统代码与应用程序代码(操作系统的代码要比应用程序的代码健壮很多)。即便是单个应用程序出现错误也不会影响到操作系统的稳定性,这样其它的程序还可以正常的运行。
「所以,区分内核空间和用户空间本质上是要提高操作系统的稳定性及安全性。」
其实所有的系统资源管理都是在内核空间中完成的。比如读写磁盘文件,分配回收内存,从网络接口读写数据等等。
我们的应用程序是无法直接进行这样的操作的。但是我们可以通过内核提供的接口来完成这样的任务。
比如应用程序要读取磁盘上的一个文件,通过一个特殊的指令让进程从用户态进入到内核态(到了内核空间),在内核空间中,CPU 可以执行任何的指令,当然也包括从磁盘上读取数据。具体过程是先把数据读取到内核空间中,然后再把数据拷贝到用户空间并从内核态切换到用户态。
从用户态切换到内核态的方式有三种:
这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,而其核心是使用了操作系统为用户特别开放的一个**(软)中断**来实现。
进程调用:exit、fork
文件系统访问:chmod、chown
设备调用:read、write
信息读取:读取设备信息
通信:mmap、pipe等
当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
当外设完成用户的请求时,会向CPU发送中断信号。
我们编译的代码可执行文件只是储存在硬盘的静态文件,运行时被加载到内存,CPU执行内存中指令,这个运行的程序被称为进程。
进程是对运行时程序的封装,是操作系统进行资源(CPU、内存等)调度和分配的基本单位。
线程是CPU调度和分配的基本单位(程序执行的最小单位)。
因为进程的 pid 是用 pid_t
来表示的,pid_t 的最大值是 32768,所以理论上最多有32768个进程。
至于线程,进程最多可以创建的线程数是根据分配给调用栈的大小,以及操作系统位数(32位和64位不同)共同决定的。Linux32位下是300多个。
进程的执行过程需要经过三大步骤:编译,链接和装入。
编译:将源代码编译成若干模块;
链接:将编译后的模块和所需的库函数进行链接;
链接包括三种形式:静态链接,装入时动态链接(将编译后的模块在链接时一边链接一边装入),运行时动态链接(在执行时才把需要的模块进行链接)
装入:将模块装入内存运行。
将进程装入内存时,通常使用分页技术,将内存分成固定大小的页,进程分为固定大小的块,加载时将进程的块装入页中,并使用页表记录。减少外部碎片。
通常操作系统还会使用虚拟内存的技术将磁盘作为内存0-的扩充。
把 main 函数的入口地址写入到下一行指令寄存器中
(1)执行:进程分到CPU时间片,正在执行
(2)就绪:进程已经就绪,只要分配到CPU时间片,随时可以执行
(3)阻塞(等待):有IO事件或者等待其他资源(请求I/O,申请缓冲空间等);阻塞态的进程占⽤着物理内存,但无法参与系统进程调度。
(3.1)可中断睡眠状态:也称为浅度睡眠,表示睡的不够“死”,还可以被唤醒,一般来说可以通过信号来唤醒;
(3.2)不可中断睡眠状态:也称为深度睡眠,深度睡眠无法被信号唤醒,只能等待相应的条件成立才能结束睡眠状态。
(4)新建:进程刚被创建时的状态,尚未进入就绪队列。创建步骤包括:申请空白的 PCB,向 PCB 中填写一些控制和管理信息,系统向进程分配运行时所需的资源。
(5)终止:进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终止权的进程所被终止时所处的状态。
(6-7)挂起:把阻塞的进程置换到磁盘中,此时进程未占用物理内存,我们称之为挂起;挂起不仅仅可能是物理内存不足,比如sleep系统调用,或用户执行Ctrl+Z也可能导致挂起。
–(6)就绪挂起:进程在外存(硬盘),但只要进入内存,马上运⾏。
–(7)阻塞挂起:进程在外存(硬盘)并等待某个事件的出现(进入就绪挂起态)。
只有就绪态和运⾏态可以互相转换,其他都是单向转换。就绪态的进程通过调度算法从⽽获得CPU 时间,转为运行状态;
进程因为等待资源⽽阻塞,但是该资源不包括 CPU 时间,缺少 CPU 时间会从运⾏态转换为就绪态。
进程PCB控制块是标志进程存在的数据结构,其中包含系统对进程进行管理所需要的的全部信息。系统通过PCB控制块控制和管理进程。
进程ID、进程堆栈空间、进程优先级只是PCB控制块中的一项基本信息。
操作系统对进程的感知,是通过进程控制块PCB数据结构来描述的。内核为每个进程分配一个 PCB(Processing Control Block)进程控制块,维护进程相关的信息,Linux 内核的进程控制块是 task_struct 结构体。
它是进程存在的唯⼀标识,其包括以下信息:
PCB通过链表形式组织起来,比如有就绪队列、阻塞队列等,方便增删,方便进程管理。
父进程调用 fork()
以后,克隆出一个子进程,子进程的代码从 fork() 之后开始执行,初始用户区数据和父进程一样,但所使用的内存空间不同;内核区也会拷贝过来,但是 pid 进程号不同。
子进程和父进程拥有相同内容的代码段、数据段和用户堆栈(读时共享,写时拷贝)。
父进程和子进程谁先执行不一定,看CPU。所以我们一般我们会设置父进程等待子进程执行完毕。
父子进程操作同一个文件的情况:
孤儿进程与僵尸进程[总结]
在unix/linux中,正常情况下,子进程是通过父进程创建的,子进程再创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束。 当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。
进程通信的方式主要有六种:管道,信号量,信号,共享内存,消息队列,套接字。
管道是半双工的,双方需要通信的时候,需要建立两个管道。
管道的实质是一个内核缓冲区,进程以先进先出 FIFO的方式从缓冲区存取数据(一端发一端读):管道一端的进程顺序地将进程数据写入缓冲区,另一端的进程则顺序地读取数据,该缓冲区可以看做一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,读出以后内存中数据将会清空,不能使用 lseek() 改变读写位置。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或写进程是否进入等待队列,当空的缓冲区有新数据写入或慢的缓冲区有数据读出时,就唤醒等待队列中的进程继续读写。管道是最容易实现
的。
匿名管道 pipe 和命名管道除了建立、打开、删除的方式不同外,其余都是一样的。
匿名管道没有文件实体,有名管道有文件实体,但不存储数据。
匿名管道只允许有亲缘关系的进程之间通信,也就是父子进程之间的通信,命名管道允许具有非亲缘关系的进程间通信。
当以阻塞的方式写操作时,当没有任何进程访问读端时,写操作会收到 SIGPIPE 的信号,write函数会返回 -1;
当以阻塞的方式读管道,如果没有任何进程访问写端,那么读操作会立即返回,并按如下操作:
(1)如果管道现有数据无数据,立即返回 0;
(2)如果管道现有数据大于读出数据,立即读取期望大小的数据;
(3)如果管道现有数据小于读出数据,立即读取现有所有数据。
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。信号量只有等待和发送两种操作。
等待(P(sv))就是将其值减一或者挂起进程,发送(V(sv))就是将其值加一或者将进程恢复运行。
信号是Linux系统中用于进程之间通信或操作的一种机制,信号可以在任何时候发送给某一进程,而无须知道该进程的状态。如果该进程并未处于执行状态,则该信号就由内核保存起来,直到该进程恢复执行并传递给他为止。如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。 信号是开销最小
的。
共享内存允许两个或多个进程共享一个给定的存储区,这一段存储区可以被两个或两个以上的进程映射至自身的地址空间中,就像由malloc()分配的内存一样使用。一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取读出,从而实现了进程间的通信。共享内存的效率最高
,缺点是没有提供同步机制,需要使用锁等其他机制进行同步。
消息队列就是一个消息的链表,是一系列保存在内核中消息的列表。用户进程可以向消息队列添加消息,也可以向消息队列读取消息。
消息队列与管道通信相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件接收特定类型的消息。
可以把消息看做一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程可以从消息队列中读取消息。
套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同设备及其间的进程通信。
操作系统的常见进程调度算法
1、批处理系统中的调度
先来先服务(FCFS):按照作业到达任务队列的顺序调度。FCFS是非抢占式的,易于实现,效率不高,性能不好,比较有利于长进程,而不利于短进程,有利于CPU 繁忙的进程,而不利于I/O 繁忙的进程。
最短作业优先(SJF):每次从队列里选择预计时间最短的作业运行。SJF是非抢占式的,优先照顾短作业,具有很好的性能,降低平均等待时间,提高吞吐量。但是不利于长作业,长作业可能一直处于等待状态,出现饥饿现象;完全未考虑作业的优先紧迫程度,不能用于实时系统。
最短剩余时间优先(SRTF):该算法首先按照作业的服务时间挑选最短的作业运行,在该作业运行期间,一旦有新作业到达系统,并且该新作业的服务时间比当前运行作业的剩余服务时间短,则发生抢占;否则,当前作业继续运行。该算法确保一旦新的短作业或短进程进入系统,能够很快得到处理。
2、交互式系统中的调度
时间片轮转: 用于分时系统的进程调度。基本思想:系统将CPU处理时间划分为若干个时间片(q),进程按照到达先后顺序排列。每次调度选择队首的进程,执行完1个时间片q后,计时器发出时钟中断请求,该进程移至队尾。以后每次调度都是如此。该算法能在给定的时间内响应所有用户的而请求,达到分时系统的目的。
优先级调度:为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。
多级反馈队列:时间片轮转算法对于需要运行较长时间的进程很不友好,假设有一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。因此发展出了多级反馈队列的调度方式。
多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个就绪队列,每个队列时间片大小都不同,例如 :1, 2, 4, 8, … 这样呈指数增长。如果进程在第一个队列没执行完,就会被移到下一个队列。
在这种情况下,一个需要 100 个时间片才能执行完的进程只需要交换 7 次就能执行完 (1 + 2 + 4 + 8 + 16 + 32 + 64 = 127 > 100)。
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。
在大多数情况下,同步已经实现了互斥,特别是所有写入共享资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时读访问资源。
对比维度 | 多进程 | 多线程 | 占优选项 |
---|---|---|---|
共享数据、同步问题 | 共享数据复杂,需要IPC;数据独立,同步简单 | 共享进程数据,简单;导致同步问题 | 各有优点 |
内存、CPU | 占用内存多,切换复杂,CPU利用率低 | 占用内存少,切换简单,CPU利用率高 | 线程 |
创建、销毁、切换 | 复杂,速度慢 | 简单,速度快 | 线程 |
编程、调试 | 简单 | 复杂 | 进程 |
可靠性 | 进程间不会相互影响 | 一个线程挂掉将导致整个进程挂掉 | 进程 |
分布式 | 适用于多核、多机分布式,多机扩展更为简单 | 适用于多核分布式 | 进程 |
死锁(deadlock) 是指在多道程序系统中,一组进程中的每一个进程都无限期等待被该组进程中的另一个进程所占有且永远不会释放的资源。
死锁的发生必须同时满足四个条件:互斥,持有并等待,非抢占, 形成等待环。
饥饿(starvation) 是指系统不能保证某个进程的等待时间上界,从而使该进程长时间等待,当等待时间给进程推进和响应带来明显影响时,称发生了进程饥饿。
饿死(starve to death) 即是当饥饿到一定程度的进程所赋予的任务即使完成也不再具有实际意义。
死锁的检测:
死锁的预防即打破死锁的条件之一:
https://blog.csdn.net/cui240558023/article/details/103948907/
一个程序被加载到内存中,这块内存首先就存在两种属性:
静态分配内存:是在程序编译和链接时(运行前)就确定好的内存。
动态分配内存:是在程序加载、调入、执行的时候分配/回收的内存。
.text 代码段:
用来存放程序执行的二进制代码,也有可能包含一些只读的常数变量(字符串常量等)。
该段内存为静态分配,只读(某些架构可能允许修改)。
这块内存是共享的,当有多个相同进程(Process)存在时,共用同一个text段。
.data 数据段:
也叫GVAR(global value),用来存放程序中已初始化的全局/静态变量;静态分配。
.bss 数据段:
存放程序中未初始化的全局/静态变量。静态分配,在程序开始时通常会被清零。
代码段和初始化数据段存放在程序可执行文件中,在编译时已经分配了空间;
而 .bss 段并不占用可执行文件的大小,它是由链接器来获取内存的。
heap 堆区:
用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。
堆是从低地址位向高地址位增长,采用链式存储结构。
进程调用malloc / free等函数进行动态分配和手动释放;频繁操作会造成内存空间的不连续,产生碎片。
堆空间的大小由系统内存/虚拟内存上限决定,速度较慢,但自由性大,可用空间大。
stack 栈区:
存放程序临时创建的局部变量(static声明的变量存放在数据段);除此以外,在函数被调用时的参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中(动态分配)。
堆是从高地址位向低地址位增长。
内存由编译器自动分配和释放,是一块连续的内存区域。
栈空间的最大大小在编译时确定,速度快,但自由性差,最大空间不大(2M)。
每个线程都会从内存区域中划分出自己的栈和代码段,但是其他空间是共用的。
int a = 0; // 全局初始化区
char *p1; // 全局未初始化区
int main(){
int b; //栈
char s[] = "abc"; //栈
char *p2; //栈
char *p3 = "123456"; //123456\0在常量区,p3在栈上。
static int c =0; //全局(静态)初始化区
p1 = (char *)malloc(10); //堆
p2 = (char *)malloc(20); //堆
}
映射的内存在用户区堆和栈之间的共享库中,进程结束后会自动释放。
不会。进程间通信使用的管道、socket、共享内存、消息队列、信号量等,是属于内核级的,一旦创建后就由内核管理,若进程不对其主动释放,那么这些变量会一直存在,除非重启系统。
会,每个进程的PCB也都不一样,维护的文件描述符表也不一样。
wait 函数只有两种返回值,即成功则返回终止的子进程对应的进程号;失败则返回 -1。
如果其所有子进程都还在运行,则wait()会一直阻塞等待,直到某一个子进程终止,然后返回该子进程的进程号;
如果该进程并没有子进程,也就意味着该进程并没有需要等待的子进程,那么 wait() 将返回错误,也就是返回 -1,并且会将 errno 设置为ECHILD。
atexit 函数是 linux标准 I/O 自带的库函数,使用该函数需要包含头文件
用于注册一个进程在正常终止时要调用的函数,我们可以通过该函数打印或者执行一些其他功能代码。
该函数原型是:int atexit(void (*function)(void));
;
function:函数指针,指向注册的函数,此函数无需传入参数、无返回值。
返回值:成功返回0;失败返回非0。
PID 为 0 的是调度进程,该进程是内核的一部分,也称为系统进程;
PID 为 1 的是 init 进程,它是由内核启动的第一个用户进程,以超级用户特权运行,管理着系统上所有其它进程,因此理论上说它没有父进程。它是所有子进程的父进程,一切从1开始、一切从init进程开始!
PID 为 2 的是页守护进程,负责支持虚拟存储系统的分页操作。
在linux中,对于有些程序设计来说,程序只能被执行一次,只要该程序没有结束,就无法再次运行,我们把这种情况称为单例模式运行。
譬如系统中守护进程,这些守护进程一般都是服务器进程,服务器程序只需要运行一次即可,能够在系统整个的运行过程中提供相应的服务支持,多次同时运行并没有意义、甚至还会带来错误!
多核和单核CPU对进程运行几次没有关系。
在Linux系统中,
从可靠性方面将信号分为可靠信号和不可靠信号;
从时间关系上将信号分为实时信号和非实时信号。
前31个信号编号(1~31)为非实时信号,等同于不可靠信号,不支持排队;
其他(32~64)为实时信号,等同于可靠信号,都支持排队。
Linux信号机制基本上是从Unix系统中继承过来的,早期不可靠信号的主要问题是:
进程每次处理信号后,就将对信号的响应重置为默认动作。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用 signal(),重新安装该信号。
因此,早期unix下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失。
Linux支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用该信号的安装函数(信号安装函数是在可靠机制上的实现)。
因此,Linux下的不可靠信号问题主要指的是信号可能丢失,因为这些不可靠信号阻塞的时候是不支持排队的,即未决信号集只有一个 0 或 1 的标记位,不能记录相关信号的触发次数。
信号值位于SIGRTMIN和SIGRTMAX(32~64)之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。
信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。
目前Linux中的signal()是通过sigation()函数实现的,因此,即使通过signal()安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。同时,由signal()安装的实时信号支持排队,同样不会丢失。
程序段、相关数据段和PCB三部分构成进程的实体,一般简称为进程。
所谓创建进程就是创建进程实体中的PCB,而撤销进程也就是撤销进程的PCB。
在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁。如何利用已有对象来服务就是一个需要解决的关键问题,其实这就是一些 "池化资源"技术 产生的原因,线程池为线程生命周期开销问题和资源不足问题提供了解决方案。
线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。
创建线程池时可以设置线程池的最大线程数和最小线程数;
当任务队列当中没有任务时,线程池阻塞在条件变量上,等待任务;
当有任务进来时,条件变量发信号或者广播,唤醒线程,此时对任务队列而言属于共享资源,需要使用互斥量,避免资源冲突。
线程池的伸缩性对性能有较大的影响。
1、创建太多线程,将会浪费一定的资源,有些线程未被充分使用。
2、销毁太多线程,将导致之后浪费时间再次创建它们。
3、创建线程太慢,将会导致长时间的等待,性能变差。
4、销毁线程太慢,导致其它线程资源饥饿。
线程池的主要组成部分:
1、线程池管理器(ThreadPoolManager):用于创建并管理线程池;
2、工作线程(WorkThread):线程池中线程;
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行;
4、任务队列:用于存放没有处理的任务。提供一种缓冲机制。
线程池的应用场景:
1、需要大量的线程来完成任务,且完成任务的时间比较短;
2、对性能要求苛刻的应用;
3、接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。
在linux中,信号是事件发生时对进程的通知机制,也可以把它称为软件中断。一个具有合适权限的进程能够向另一个进程发送信号,产生信号的情况包括:
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数,如:临时文件夹位置和系统文件夹位置等。环境变量是在操作系统中一个具有特定名字的对象,它包含了一个或者多个应用程序所将使用到的信息。
环境变量是进程中一组变量信息,环境变量分为系统环境变量、用户环境变量和进程环境变量。
系统有全局的环境变量,在进程创建时,进程继承了系统的全局环境变量、当前登录用户的用户环境变量和父进程的环境变量。进程也可以有自己的环境变量。
在linux系统中,每一个进程都有一组与其相关的环境变量,这些环境变量以字符串形式存储在一个字符串数组列表中,把这个数组称为环境列表。
其中每个字符串都是以“名称=值(name=value)”形式定义,所以环境变量是“名称-值”的成对集合,譬如在shell终端下可以使用env命令查看到shell进程的所有环境变量。
(1)相关进程:在逻辑上具有某种联系的进程称为相关进程。例如:属于同一进程家族内的所有进程(父子进程、兄弟进程);
(2)无关进程:在逻辑上没有任何联系的进程称为无关进程。
(3)直接相互作用:进程之间不需要通过某种媒介而发生的相互作用,这种相互作用通常是有意识的。
(4)间接相互作用:进程之间需要通过某种媒介而发生的相互作用,这种相互作用通常是无意识的。
inux线程默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级。
在多线程应用程序中,线程不单独存在、而是包含在进程中,
线程是参与系统调度的基本单位,每个线程都可以参与系统调度、被CPU执行,同一进程的多个线程之间可并发执行,在宏观上实现同时运行的效果;
同一进程中的各个线程,可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量等等。
互斥锁:进程共享属性 和 类型属性;
读写锁:进程共享属性;
条件变量:进程共享属性。
在linux中,可以使用系统调用setsid()
可以创建一个会话,这里需要注意的是:如果调用者进程不是进程组的组长进程,调用setsid()将创建一个新的会话,调用者进程是新会话的首领进程,同样也是一个新的进程组的组长进程,调用setsid()创建的会话将没有控制终端。
系统调用 setpgid() 或 setpgrp() 可以加入一个现有的进程组或创建一个新的进程组。
在linux中,信号通常是发送给对应的进程,当信号到达后,该进程需要做出相应的处理措施,常见的信号处理措施包括:
(1)忽略信号。也就是说,当信号到达进程后,该进程并不会去理会它、直接忽略,就好像是没有出该信号,信号对该进程不会产生任何影响。事实上,大多数信号都可以使用这种方式进行处理。
(2)捕获信号。当信号到达进程后,执行预先绑定好的信号处理函数。为了做到这一点,要通知内核在某种信号发生时,执行用户自定义的处理函数,该处理函数中将会对该信号事件作出相应的处理,Linux系统提供了 signal()
或 sigaction()
系统调用可用于注册信号的处理函数。
(3)执行系统默认操作。进程不对该信号事件作出处理,而是交由系统进行处理,每一种信号都会有其对应的系统默认的处理方式。需要注意的是,对大多数信号来说,系统默认的处理方式就是终止该进程。
进程并不会主动删除信号,对于不需要的信号,忽略就可以了。
子进程拷贝父进程的地址空间,子进程是父进程的一个复制品。
父子进程同时并发运行,但执行次序不确定。
在子进程中,成功的 fork( ) 调用会返回 0。在父进程中 fork( ) 返回子进程的 pid。如果出现错误,fork( )返回一个负值。
新进程的目的通常是 exec 一个新程序,如shell。
在调用 exec 或 exit 之前,子进程共享父进程的地址空间;
vfork 并不将父进程的地址空间完全复制给子进程,因为子进程会立即调用exec 或 exit,也就不会访问该地址空间,只在子进程调用 exec 之前,它在父进程空间中运行,共享父进程数据;这种优化工作方式在 fork() 写时拷贝出现之前可以提高效率。
vfork 保证子进程先运行,在它调用 exec(进程替换) 或 exit(进程退出) 之后父进程才能调度运行。
如果在调用这两个函数之前子进程依赖于父进程的进一步操作,或 子进程没有调用 exec / exit,程序则会导致死锁。
如果子进程修改了父进程的数据(除了vfork返回值的变量)、进行了函数调用、或者没有调用exec或_exit就返回将可能带来未知的结果。
system 函数和 exec 函数一样,都是执行进程外的命令,而 system() 则是把 exec 函数封装起来,指定执行任意shell命令。
#include
int system(const char * command);
参数 command 就是需要读取的命令,函数的返回值表示执行结果。
基本原理说明:
(1)fork一个子进程;
(2)在子进程中调用 execl 函数来调用 /bin/sh-c command 拉起 sh 执行 command 命令;
(3)使父进程等待子线程执行完毕;
(4)返回出错信息或者子进程执行后的返回值。
system() 会调用 fork() 产生子进程,由子进程来调用 /bin/sh-c command 来执行参数 command 字符串所代表的命令,此命令执行完后随即返回原调用的进程。在父进程中调用 wait 去等待子进程结束。
在调用 system() 期间 SIGCHLD 信号会被暂时阻塞,SIGINT 和 SIGQUIT 信号则会被忽略。
返回值:
若参数 command 为空指针(NULL),则立即返回非零值,一般为 1;
如果 fork() 失败,即无法创建子进程,返回 -1;
如果 exec() 失败,表示不能执行 shell,相当于 shell 执行了exit,返回 127;
如果执行成功则返回执行 command 的 shell 进程的终止状态。(该 shell 进程不一定执行成功,因此返回值情况众多,若无法获取子进程的终止状态,返回 -1)
那么什么时候system()函数返回0呢?只在 command 命令返回 0 时。
system() 与 exec() 的区别:
1、system 是在原进程上开辟了一个新的进程再执行 exec,但是 exec 是用新进程(命令)覆盖了原有的进程;
2、system() 和 exec() 都有能产生返回值,system 的返回值并不影响原有进程,但是 exec 的返回值影响了原进程(exec调用成功时无返回值,原进程被替代)
两者都采用离散分配方式,且都地址映射机构来实现地址的转换
分页 | 分段 | |
---|---|---|
目 的 | 页是信息的物理单位,分页是为实现离散分配方式,以消减内存的外零头,提髙内存的利用率。或者说,分页仅权是由于系统管理的需要而不是用户的需要 | 是信息的逻辑单位,它含有一组其意义相对完整的信息。分段的目的是为了能更好地满足用户的需要 |
长 度 | 页的大小固定且由系统决定,由系统把逻辑地址划分为页号和页内地址两部分,是由机器硬件实现的,因而在系统中只能有一种大小的页面 | 段的长度不固定,决定于用户所编写的程序, 通常由编译程序在对流程序进行编译时,根据信息的性质来划分 |
地址空间 | 作业地址空间是一维的,即单一的线性地址空间,程序员只需利用一个记忆符,即可表示 一个地址 | 作业地址空间是二维的,程序员在标识一个地址时,既需给出段名,又需给出段内地址 |
碎 片 | 有内部碎片,无外部碎片 | 有外部碎片,无内部碎片 |
”共享“和“动态链接” | 不容易实现 | 容易实现 |
内存泄漏(memory leak):是指程序在申请内存后,无法释放已申请的内存空间,导致系统无法及时回收内存并且分配给其他进程使用。一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后就会导致内存不够用,最终导致内存溢出。
内存溢出 (out of memory):如果过度占用资源而不及时释放,在程序申请内存时,没有足够的内存供申请者使用,导致数据无法正常存储到内存中。也就是说给你个 int 类型的存储数据大小的空间,但是却存储一个 long 类型的数据,这样就会导致内存溢出。
内存泄露是由于GC无法及时或者无法识别可以回收的数据进行及时的回收,导致内存的浪费;内存溢出是由于数据所需要的内存无法得到满足,导致数据无法正常存储到内存中。内存泄露的多次表现就是会导致内存溢出。
在请求分页系统中,每当所要访问的页面不在内存时,便产生一个缺页中断,请求操作系统将所缺的页调入内存。
此时应将缺页的进程阻塞(调页完成唤醒),如果内存中有空闲块,则分配一个块,将要调入的页装入该块,并修改页表中相应页表项;若此时内存中没有空闲块,则要淘汰某页(若被淘汰页在内存期间被修改过,则要将其写回外存)。
中断次数 = 进程的物理块数 + 页面置换次数
缺页中断率 =(缺页中断次数 / 总访问页数 )
缺页中断作为中断同样要经历,诸如保护CPU环境、分析中断原因、转入缺页中断处理程序、恢复CPU环境等几个步骤。但与一般的中断相比,它有以下两个明显的区别:
最佳(Optimal, OPT) 置换算法所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。
【无法实现】
先进先出(FIFO) 页面置换算法:优先淘汰最早进入内存的页面,亦即在内存中驻留时间最久的页面。该算法只需把调入内存的页面根据先后次序链接成队列,设置一个指针总指向最早的页面。
【FIFO算法实现简单,但性能差,会出现物理块数增大而页故障数不减反增的Belady异常现象】
最近最久未使用(LRU,Least Recently Used) 置换算法选择最近最长时间未访问过的页面予以淘汰,它认为过去一段时间内未访问过的页面,在最近的将来可能也不会被访问。
该算法为每个页面设置一个访问字段,来记录页面自上次被访问以来所经历的时间,淘汰页面时选择现有页面中值最大的予以淘汰。
【LRU性能较好,但需要寄存器和栈的硬件支持,开销更大。】
最不经常访问淘汰算法(LFU,Least Frequently Used):根据数据的历史访问频率来淘汰数据。最近使用频率高的数据很大概率将会再次被使用,而最近使用频率低的数据,很大概率不会再使用。
每个数据块一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序,每次淘汰队尾数据块。
时钟(CLOCK)置换算法:给每一帧关联一个附加位,称为使用位。当某一页首次装入主存,以及后续被访问时,使用位被置为1。( 最近未用(Not Recently Used, NRU)算法 )
malloc() 函数其实就在内存中找一片指定大小的空间,然后将这个空间的首地址范围给一个指针变量;这里 malloc 分配的内存空间在逻辑上连续的,而在物理上可以连续也可以不连续。
从操作系统层面上看,malloc 是通过两个系统调用来实现的: brk
和 mmap
(不考虑共享内存)。
这两种方式分配的都是虚拟内存,没有分配物理内存。
进程先通过这两个系统调用获取或者扩大进程的虚拟内存,获得相应的虚拟地址,在第一次访问这些虚拟地址的时候,通过缺页中断,让内核分配相应的物理内存,建立虚拟内存和物理内存之间的映射关系,这样内存分配才算完成。
通常,分配的内存小于 128k 时,使用 brk 调用来获得虚拟内存,大于 128k 时就使用 mmap 来获得虚拟内存。
brk 分配的虚拟内存和物理内存需要等到高地址内存释放以后才能释放,因此有内存碎片的产生;但是这块被 free 的内存地址可以重用,分配给其他大小合适的变量。
当最高地址空间的连续空闲内存超过 128k(可由M_TRIM_THRESHOLD选项调节)时,通过 brk 分配的内存将被执行内存紧缩操作(trim),即释放掉分配的虚拟内存和物理内存。
而 mmap 分配的内存可以可以通过 free 直接单独释放。
持续学习更新中…