一、概念
1 概念
(1)进程
1)进程是操作系统资源分配的基本单位
2)一个进程可以有多个线程,但至少有一个线程
3)资源分配给进程,同一进程的所有线程共享该进程的所有资源
4)同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),拓展段(堆存储)
5)进程有独立的地址空间
(2)线程
1)线程是任务调度和执行的基本单位
2)一个线程只能属于一个进程
3)每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量或临时变量
4)CPU分给线程,即真正在CPU上运行的是线程(通过CPU调度,在每个时间片中只有一个线程执行)
5)线程之间没有单独的地址空间
2 Linux虚拟内存结构
(1)代码段(Text Segment)
保存CPU将要执行的机器指令
(2)已初始化数据段(initialized data segment)
保存程序中被初始化的全局或静态变量
(3)未初始化数据段(uninitialized data segment)
也被称为BSS(block started by symbol),这个段中的数据在程序执行之前被内核初始化为0或者NULL
(4)栈(Stack)
存储临时变量,函数相关信息
(5)堆(heap)
动态内存分配位置。堆的位置位于未初始化数据段和栈的中间
3 进程的创建
(1)exec
当进程调用一种exec函数时,原进程完全由新程序代换,而新程序则从其main函数开始执行。
(2)fork
新进程几乎与原进程一模一样,执行的代码也完全相同,但是新进程有自己的数据空间、环境和文件描述符。
(3)system
使用system函数远非启动其他进程的理想手段,因为它必须用一个shell来启动需要的程序。
特点:
1)建立独立进程,拥有独立的代码空间,内存空间;
2)等待新的进程执行完毕,system才返回(阻塞)。
4 线程的创建
4.1 创建
(1)接口
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
(2)返回值
成功返回0,失败返回错误号。
在一个线程中调pthread_create()创建线程后,当前线程从pthread_create()返回继续往下执行,新的线程所 执行的代码由我们传给pthread_create()的函数指针start_routine()决定。
4.2 终止
(1)接口
void pthread_exit(void *retval)函数用于进程终止
retval是void*类型,其它线程可以调pthread_join获得这个指针
(2)终止线程的3种方法
从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit
一个线程可以调用pthread_cancel()终止同一进程中的另一个线程
线程可以调pthread_exit()终结自己
4.3 容易混淆的两个“线程ID”
(1)内核中的ID
1)属于调度范畴
2)本质为一个整数
3)此ID数据类型为pid_t
4)通过syscall(SYS_gettid)获取
5)对于不同进程之间的所有线程,在任意时刻此ID全局唯一
6)在procfs文件系统中,可通过查看/proc/pid/task/tid获取对应的线程信息
(2)POSIX中的ID
1)属于线程库范畴
2)本质为一个地址
3)此ID数据类型为pthread_t
4)通过pthread_self()获取
5)不同进程之间此ID不唯一,因此此ID仅在同一进程内有意义
4.4 线程分离
(1)非分离状态
1)原有的线程等待创建的线程结束
2)只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源
(2)分离状态
1)没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源
2)2种方法可以设置线程分离状态
a)在线程创建时将其属性设为分离状态(detached),调用函数:pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
b)在线程创建后将其属性设为分离状态(detached),调用函数:pthread_detach(pthread_self())
二、竞态资源保护
1 竞态条件、竞态资源
(1)竞态条件
当两个或多个线程/进程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件(race condition)。
程序运行结果取决于多线程/多进程的执行顺序。
(2)竞态资源
竞态条件下多线程争抢的是“竞态资源”、“临界资源”。
导致竞态条件发生的代码片段称作“临界区”。
在临界区中使用适当的同步就可以避免竞态条件。
(3)竞态资源的主要保护手段
锁机制(lock)是多线程编程中最常用的同步机制,用来对多线程间共享的临界区(Critical Section)进行保护。
2 常用同步工具
2.1 互斥锁
(1)基本操作
1)原理
在编程中,引入对象互斥锁的概念,来保证共享数据操作的完整性。
2)创建
pthread_mutex_init();
3)加锁
pthread_mutex_lock(); pthread_mutex_trylock();
4)解锁
pthread_mutex_unlock();
5)销毁
pthread_mutex_destory();
(2)死锁概念
1)如果同一个线程先后两次调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此就永远处于挂起等待状态了,产生死锁(dead lock)。
2)若线程A获得了锁1,线程B获得了锁2,这时线程A调用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和B都永远处于挂起状态了。
(3)必要条件
1)互斥条件
一个资源每次只能被一个进程使用
2)请求与保持条件
一个进程因请求资源而阻塞时,对已获得的资源保持不放
3)不剥夺条件
进程已获得的资源,在未使用完之前,不能强行剥夺
4)循环等待条件
若干进程之间形成一种头尾相接的循环等待资源关系
(4)使用规则
1)识别需要保护的数据后用锁保护
2)锁只允许 保护数据,不允许保护过程
3)在同一模块、同一抽象层中加锁和释放锁
4)控制锁的数量
5)在等待某个资源时,使用超时机制
6)控制锁的范围不要太大
2.2 信号量
(1)基本操作
1)原理
用来保护共享资源,使得资源在一个时刻只有一个进程(线程)所拥有。
2)创建
sem_init(); sem_open();
3)阻塞等待
sem_wait();
4)试图等待
sem_trywait();
5)释放
sem_post();
6)销毁
sem_destory(); sem_close(); sem_unlink();
(2)特性
1)信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问;
2)当公共资源增加时,调用函数sem_post()增加信号量;
3)只有当信号量值大于0时,才能使用公共资源,使用后,函数sem_wait()减少信号量;
4)信号量可以是二元的,也可以是多元的,通过初始化时指定;
5)信号量允许多个线程共同等待一个共享资源。
(3)互斥锁、信号量区别
1)互斥锁
只允许一个线程进入临界区
2)信号量
允许多个线程同时进入临界区
(4)无名、有名信号量区别
1)无名信号量
创建sem_init();
销毁sem_deatory();
存在于内存中;
2)有名信号量
创建sem_open();
销毁sem_close() sem_unlink()
通过名字访问
3)注意事项
同一进程的不同线程间建议使用无名信号量;
采用无名信号量时,使用完毕注意通过sem_destory()销毁;
采用有名信号量时,使用完毕注意通过sem_unlink()销毁。
2.3 条件变量
(1)基本操作
1)原理
在编程中,条件变量用于等待
2)创建
pthread_cond_init();
3)等待
pthread_cond_wait(); pthread_cond_timedwait();
4)激活
pthread_cond_signal(); pthread_cond_broadcast();
5)销毁
pthread_cond_destory();
(2)等待、激活规则
1)等待线程
pthread_mutex_lock(&mutex);
while(线程执行的条件是否成立)
pthread_cond_wait(&cond, &mutex);
//处理共享资源
pthread_mutex_unlock(&mutex);
2)激活线程
pthread_mutex_lock(&mutex);
pthread_cond_signal(&cond); //激活一个等待该条件的线程,存在多个线程时按入队顺序激活其中一个
pthread_mutex_unlock(&mutex);
2.4 自旋锁
(1)基本操作
1)原理
自旋锁主要为在短时间内快速执行轻量级的加锁
2)创建
pthread_spin_init();
3)加锁
pthread_spin_lock(); pthread_spin_trylock();
4)解锁
pthred_spin_unlock();
5)销毁
pthread_spin_destory();
(2)特性
1)忙等待:连续测量一个变量直到某个值出现为止;
2)表示一直在原地旋转,并未去睡眠。可能存在的问题:死锁,过多占用cpu资源等;
3)只有在有理由认为等待时间非常短的情形下,才只用忙等待;
4)自旋锁的设计初衷,在短时间内进行轻量级加锁;
5)自旋锁不能够在能够导致睡眠的环境中使用。
(3)使用场景
临界区可以快速完成读写,并且读写概率不定。
(4)自旋锁与互斥锁区别
1)互斥锁
sleep-waiting
让权等待
pthread_mutex_unlock();
资源被占用,资源申请者就会进入睡眠状态
2)自旋锁
busy-waiting
忙等待
pthread_spin_unlock();
一直在原地旋转,并未去睡眠
2.5 读写锁
(1)基本操作
1)原理
读写锁是一种特殊的自旋锁,它把对共享资源的访问者划分为读者和写者
2)创建
pthread_rwlock();
3)加锁
pthread_rwlock_relock() pthread_rwlock_wrlock()
pthread_rwlock_tryrdlock() pthread_rwlock_trywrlock()
4)解锁
Pthread_rwlock_unlock()
5)销毁
Pthread_rwlock_destory()
(2)特性
1)本质为一种特殊的自旋锁;
2)多个读者可以同时进行读(即读并行);
3)写者必须互斥(只允许一个写者写,也不能读者、写者同时进行,即写串行);
4)写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者,即写的优先级高);
5)自旋锁不能够在能够导致睡眠的环境中使用。
(3)读写锁、互斥锁比较
1)互斥锁
读写串行
只有一个线程拥有互斥锁
pthread_mutex_unlock();
2)读写锁
读并行,写串行
可多个线程同时拥有锁
pthread_rwlock_unlock();
3)说明
读写锁也叫共享-独占锁,当读写锁以读模式锁住时,它是以共享模式锁住的;当读写锁以写模式锁住时,它是以独占模式锁住的。
(4)使用场景
读的概率远远大于写的概率。
三、进程间通信
1 进程、线程间通信的概念
(1)进程间通信
a)进程用户空间是相互独立的,一般而言是不能相互访问的;
b)但很多情况下进程间需要互相通信,来完成系统的某项功能;
c)进程通过与内核及其他进程之间的相互通信来协调他们的行为。
(2)线程间通信
a)同步:同步就是协调步调,按预定的先后次序进行运行;
b)互斥:线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,知道占用资源者释放该资源。
c)线程互斥可以看成是一种特殊的线程同步(统称为同步)。
2 通信类工具
3通信方式
(1)进程间通信方式
管道(Pipe)
有名管道(Named Pipe)
信号(Signal)
信号量(Semophore)
消息队列(Message Queue)
共享内存(Shared Memory)
套接字(Socket)
(2)线程间通信方式
锁机制(互斥锁、条件变量、读写锁)
信号量机制(Semaphore,无名线程信号量和命名线程信号量)
信号机制(Signal,类似进程间的信号处理)
4通信方式差异
(1)进程间通信
a)实现方式:
需要通过内核调度实现
b)主要场景/目的:
传递消息
数据交换
(2)线程间通信
a)实现方式:
无需通过内核调度
b)主要场景/目的
线程同步
线程互斥
5消息队列
(1)基本操作
原理:是消息链表,是一种内核资源,使用前需要预分配消息缓存。主要用于从一个线程向另外一个线程发送数据块。
创建:mq_open
发送:mq_send
接收:mq_receive
关闭:mq_close
销毁:mq_unlink
(2)特性
1)读消息与写消息异步,它允许读消息者在消息发送很长时间后再读取消息,即使写消息者写完消息后终止该消息队列(mq_close),读消息者仍能在后续某个时间打开消息队列(mq_open)来读取消息。我们常需要读消息者一直轮询该消息队列才能读取到最新消息;
2)消息队列承载的信息是有格式的数据,如特定的结构体或字符串信息等;
3)消息可由写消息者赋予一个优先级,读消息者读取到的总是最高优先级的最早消息;
4)相较于信号,消息队列能够传递更多的数据,但仍然有大小限制;
5)消息队列具有随内核的持续性,消息队列对象一直在内存直到内核重启或显式删除该对象为止。
(3)注意事项
1)mq_系列函数不是标准c库函数,链接时需要指定链接库,通过-lrt链接
2)通过mq_open创建消息队列时,名称必须以/开头,并且后面不能还有/
3)消息队列的名字中打头的/字符不计入长度
4)通过mq_open失败,错误码为38,表示内核不自持消息队列(可通过cat/proc/sys/fs确认)
5)mq_close仅是关闭消息队列描述符,与close一样,仅关闭消息队列,不会删除消息队列
6)未执行mq_unlink可能出现创建新消息队列时资源不足,报错errno[24:Too many open files.]