a) 进程和线程是操作系统的概念
b) linux系统属于分时操作系统,可处理并发任务同时保证快速响应,采用时间片轮转调度机制,即
操作系统将cpu时间划分为时间片,每个任务只占用一个时间片时间,然后调度队列中的下一个任务执行
附: c++内存模型:
- 栈(编译器管理, 局部变量,函数参数,返回值)
- 堆(程序员管理)
- 静态区(操作系统管理, 全局变量,静态变量)
- 文字常量区(操作系统管理, 常量字符串)
- 程序代码区(内存中)
不同进程的内容完全隔离,不共享
进程间通信提供了进程间数据共享的途径,开销都比较大(类比深拷贝)
进程间通信的目的:
+ 数据传输: 一个进程将数据发送给另一个进程
+ 资源共享: 多个进程共享同样的资源,需要内核提供互斥和同步机制
+ 通知事件: 一个进程向别的进程发送消息,通知他们发生了某种事件
+ 进程控制: 一个进程完全控制另一个进程的执行(如DEBUG进程),控制进程希望能够拦截
另一个进程的所有陷入和异常及其状态改变
通信方式包括:
+ 信号(用于软中断和异常处理)
+ 信号量(用于进程同步,避免数据竞争)
+ 管道
+ 消息队列
+ 共享内存
+ socket(包括网络套接字和unix域套接字)
进程的状态:
阻塞(or睡眠,不可中断睡眠和可中断睡眠) 就绪 运行 停止 中止
linux内核为需要共享资源的多个进程提供的互斥和同步机制:
什么是互斥?什么是同步?
互斥指的是保证不同进程对同一资源的访问不冲突的机制
同步指的是实现不同进程对同一资源的访问顺序的机制
为什么需要同步机制?
+ 多个并发进程对共享资源的争用
+ 中断,异常处理,内核态抢占导致进程以交错的方式运行,可能对关键数据结构交错修改,
导致这些数据结构状态的不一致,导致系统的崩溃
因此,linux系统中广义的进程并发包括:
a) 中断和异常,源程序和中断处理程序访问同一个临界资源
b) 内核态抢占,更高优先级的进程在当前进程未结束时被调度,访问同一临界资源
c) 多个处理器上进程的并发
疑问: 难道用户态抢占不会造成并发吗?
内核态抢占: 当前进程进入内核态后,尚未返回到用户态,更高优先级的进程被调度执行
用户态抢占: 当前进程进入内核态后,cpu发现需要执行另一个更高优先级的进程,但直到当前进程
返回到用户态后高优先级进程才被调度执行
linux内核提供了哪些同步机制?
临界区: 对共享资源进行访问的代码片段,需要在进入临界区前启用同步机制,离开后释放
1) 禁用中断(单核不可抢占系统)
2) 自旋锁(多处理器系统,需要辅助禁用中断,禁用内核态抢占)
设计目的: 在多处理器系统中保护共享数据
实现方法: 使用全局变量V表示锁,V=1时锁定,V=0时解锁.若处理器A上的代码要进入临界区,首先
读取V的值,若V=1,则忙等(自旋,运行状态,占用cpu时间),否则设置V=1,进入临界区,离开临界区时
设置V=0,这里对V的测试和设置为原子操作
实现细节:
对所有系统,禁用中断,因为中断处理程序和当前程序访问同一全局资源时,导致死锁.当前进程拿到了
共享资源的锁,此时发生中断,中断处理进程恰好也要访问该共享资源,由于该锁已被占有,因此
中断处理进程进入忙等状态,无法返回,则当前进程处于没有执行完的状态,也不会释放锁.导致死锁
a) 对单核系统,锁定只是禁用内核态抢占,解锁只是启用内核态抢占
b) 对多核系统,锁定时首先禁止内核态抢占,然后尝试锁定,锁定失败时执行死循环等待自旋锁被释放,
解锁时首先释放自旋锁,然后使能内核态抢占
为什么需要禁用内核态抢占?
自旋的含义是持有cpu时间不松手,一直处于运行状态,如果允许其他进程抢占
内核,则当前进程就不能保持自旋状态
存在问题:
a) 忙等占用cpu,当共享资源锁定时间很短时较高效,但共享资源锁定时间较长或不确定时浪费cpu资源
b) 未对读和写操作进行区分,影响并发性能
变体:
a) 读写自旋锁: 允许多个进程同时读,同一时刻只允许一个进程写
(不互斥: 读-读, 互斥: 读-写,写-读,写-写)
b) 顺序自旋锁: 放宽 读-写,避免因为长时间的读导致写进程饿死
思路: 写进程拥有更高的优先级,写锁定请求出现时,立即满足写锁定的请求,无论此时
是否有读进程.
实现: 读不加锁,写加锁.
为保证读取时不会因为写入者的出现导致共享数据的更新,在读取者和写入者
之间引入整型变量,称为顺序值sequence,读取者读取前后分别读取该sequence,
如果不一致,说明发生了数据更新,读取操作无效.注意被保护的共享资源不能
含有指针,因为写进程可能使得指针失效,读进程再访问该指针时会出错
3) 信号量(seamaphore)
解决问题: 自旋锁上锁失败时自旋运行占用cpu时间
解决方法:
a) 进程上锁失败时主动释放cpu资源,进入可中断睡眠状态(不再占用cpu时间),阻塞
在共享资源的等待队列,其他进程释放共享资源时发出信号唤醒该队列中的一个进程
b) 共享资源可允许多个进程访问,信号量使用计数器实现,并提供两个原子操作up(),
down()(原子操作要么全执行,要么全不执行),如果操作后的结果不小于0(操作前不
小于1)则获得信号量,否则进入等待队列.访问完毕后调用up()释放信号量
c) 信号量不需要辅助禁用内核态抢占,因为被抢占后该进程进入可中断睡眠状态,效果
等同于上锁失败,猜测被抢占时仍然保留有锁,因为锁机制是为了保护共享资源,而
进程抢占是为了满足实时性的要求
疑问:
存在问题:未对读和写操作进行区分,影响并发性能
变体:
1) 读写信号量
实现: 读-读不互斥,进程无法获得锁时,睡眠并进入等待队列,唤醒时按先进先出顺序,
如果唤醒的第一个是读进程,则继续唤醒下一个读进程,直到遇见一个写进程停止
2) 互斥量: 计数器的值为1,一次只能允许一个进程进入临界区
4) RCU(待学习)
线程是进程内的实体,一个进程可以包含一个或多个线程,线程之间独享与栈相关的资源,共享
与栈无关的资源,独享的资源包括:
1) 栈
2) 寄存器(已加载进寄存器的所有程序指令)
3) 程序计数器(执行到指令的哪个位置)
4) 状态字(线程切换时寄存器的状态,用户线程切换回来后恢复现场)
引入线程的目的是提高任务执行的灵活性,因为线程之间共享进程的资源,更加轻量化
可重入and线程安全
可重入: 针对函数执行转入中断处理或异常处理的情况.如果中断处理函数恰好也调用了该函数,
则称为重入,如果中断调用返回后不会对该函数的运行造成影响,则称为可重入函数.
可重入函数必须是无状态函数,不使用任何在函数间共享的数据.
1) 不在函数内部使用静态或全局数据
2) 不返回静态或全局数据
3) 只使用本地数据,或制作全局数据的本地拷贝来保护全局数据
4) 不调用不可重入函数 —!!!—(malloc, free, 标准IO库函数)
线程安全: 针对多线程环境执行同一个函数可能导致的程序运行逻辑错误的情形.
可以使用共享数据,只要保证对共享数据的访问是互斥的即可,常用加锁实现.
可重入函数 is 线程安全函数, 线程安全函数 not necessarily 可重入函数