Unix同步方式
【重要的一句话】进程终止时系统不会自动释放所持有的锁,互斥锁、读写锁和Posix信号量。进程终止时内核总是自动清理的唯一同步锁类型是fcntl记录锁 。进程终止时(无论自愿与否),内核会对其上仍然打开着的所有 有名信号量 自动执行 sem_close
信号量关闭操作(不是释放)。
System V信号量,应用程序可以选择进程终止时内核是否自动清理某个信号量。
同步
同步(英语:synchronization),指在一个系统中所发生的事件(event)之间进行协调,在时间上出现一致性与统一化的现象。
为允许在线程或进程间共享数据,同步通常是必需的。本文涉及到的同步方式有:
- 互斥锁
- 互斥锁与条件变量
- 读写锁
- 记录锁
- Posix 信号量
互斥锁
互斥锁指代相互排斥,是最基本的同步形式。互斥锁用于保护 临界区 ,以确保任何时刻只有一个线程(或进程)在执行其中的代码。伪代码如下:
lock_mutex(mutex); // 1.获取互斥锁
do_something(); // 2.访问临界区
unlock_mutex(mutex); // 3.释放互斥锁
尽管我们说互斥锁保护的是临界区,实际上保护的是在临界区中的被操作的数据,也就是共享数据。
互斥锁是 协作性锁 。也就是说操作流程应该是在实际操作前获取互斥锁,但是没有办法防止某个线程(或进程)不先获取该互斥锁就操作数据。
初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
- 动态分配的,或者分配在共享内存区中的互斥锁必须使用 pthread_mutex_init 初始化。静态分配的可以初始化为常量
PTHREAD_MUTEX_INITIALIZER
。 attr = NULL
,则使用默认属性。
销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
获取锁
// 阻塞,直到互斥锁解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 非阻塞,立即返回
int pthread_mutex_trylock(pthread_mutex_t *mutex);
- 阻塞方式,若已有另外进程对互斥锁上锁,将阻塞到该互斥锁解锁
- 非阻塞方式,立即返回,若已有另外进程对互斥锁上锁,返回 EBUSY 错误
释放锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
互斥锁属性
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
// 获取共享属性
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *restrict attr, int *restrict pshared);
// 设置共享属性
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);
PTHREAD_PROCESS_SHARED
,指定互斥锁在不同进程间共享PTHREAD_PROCESS_PRIVATE
,指定互斥锁只能在单个进程内不同线程间共享(默认)
互斥锁与条件变量
当期待的条目未准备好时,消某线程持有锁阻塞等待,锁住整个临界区。这样的情况还可能造成死锁,这不是我们想要的。我们的期望是在线程等待其他条件的时候,释放已获得的互斥锁。这时条件变量就完美符合我们的要求。
互斥锁用于上锁,条件变量则用户等待。伪代码如下:
lock_mutex(mutex); // 1.获取互斥锁
wait(cond, mutex); // 2.等待条件
do_something(); // 3.访问临界区
unlock_mutex(mutex); // 4.释放互斥锁
初始化条件变量
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
- 动态分配的,或者分配在共享内存区中的互斥锁必须使用 pthread_cond_init 初始化。静态分配的可以初始化为常量
PTHREAD_COND_INITIALIZER
销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
若有线程等待,则返回 EBUSY 错误。
等待条件成立
// 阻塞等待
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
// 定时等待 允许设置阻塞时间限制
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
struct timespec {
time_t tv_sec; // 秒
long tv_nsec; // 纳秒
}
- 每个条件变量总是与一个互斥锁关联,当条件不满足时,阻塞等待条件变量满足,并释放已获得的该互斥锁(原子操作)
- 当被唤醒,函数返回时,解除阻塞并重新申请获取互斥锁
pthread_cond_timedwait
,允许线程就阻塞设置一个时间限制,该限制为 绝对时间 ,超时返回 ETIMEDOUT 错误- 使用绝对时间的好处:如果函数过早返回,那么同一函数无须改变其时间参数的内容就能再次被调用。
告知条件满足
// 至少唤醒一个等待该条件的线程
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒等待该条件的所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);
pthread_cond_signal
至少唤醒一个等待该条件的线程pthread_cond_broadcast
唤醒等待该条件的所有线程,会造成 ”惊群“。
条件变量属性设置
int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);
// 获取共享属性
int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr, int *restrict pshared);
// 设置共享属性
int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared);
PTHREAD_PROCESS_SHARED
,指定条件变量在不同进程间共享PTHREAD_PROCESS_PRIVATE
,指定条件变量只能在单个进程内不同线程间共享(默认)
条件满足信号丢失问题
当条件满足,向一个条件变量发送信号时,若没有线程(或进程)等待该条件,则信号将会丢失。丢失和不再告知条件满足。
测试结果:
[root@localhost sync]# ./signal_ignore
主线程:子线程创建完毕
主线程:获取到互斥锁
主线程:发出 signal...
主线程:释放掉互斥锁
子线程:获取到互斥锁
子线程:尝试获取条件变量...
^C
读写锁
与互斥量类似,但读写锁允许更高的并行性。其特性为:读时共享,写时独占(写优先级高)
【读写锁】分配规则:共享-独占 上锁(读锁--共享锁,写锁--独占锁)
- 若无线程持有给定的读写锁 写 ,那么任意数目的线程可持有该读写锁 读 ;
- 当且仅当无线程持有读写锁 读写 时,才分配读写锁用于 写 。
基本操作:
// 静态分配赋值常量 PTHREAD_RWLOCK_INITIALIZER
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
// 读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);
// 写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);
// 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
属性操作:
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *restrict attr, int *restrict pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);
PTHREAD_PROCESS_SHARED
,在不同进程间共享PTHREAD_PROCESS_PRIVATE
,只能在单个进程内不同线程间共享(默认)
fcntl记录上锁
假设我们现在要设置某个锁,以实现对文件访问的同步。上面谈到的锁作用于整个文件,粒度较大,我们期望这个锁只作用与文件的 记录(即字节范围) 上。
Unix内核没有文件内的记录这一概念,但内核提供的上锁特性却用 记录上锁(record locking)这一术语来描述。
粒度:用于标记能够锁住的对象的大小。通常情况下粒度越小,允许同时使用的用户数就越多。Posix 记录上锁的粒度为单字节。
Posix fcntl记录上锁
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* struct flock *arg */ );
struct flock {
short l_type; /* 锁类型: F_RDLCK, F_WRLCK, F_UNLCK */
short l_whence; /* 偏移基准: SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start; /* 相对偏移起始位置 */
off_t l_len; /* 字节数 0--表示直到文件末尾*/
pid_t l_pid; /* 进程ID(仅 F_GETLK) */
};
cmd 参数:
-
F_SETLK
:- 获取(
l_type = F_RDLCK
或l_type = F_WRLCK
)或释放(l_type = F_UNLCK
)由 arg 指向的 flock 结构所描述的锁。 - 不阻塞,若无法将该锁授予调用进程,则立即返回 EACCESS 或 EAGAIN 错误。
- 获取(
-
F_SETLKW
:阻塞,作用同 F_SETLK -
F_GETLK
:- 检查有 arg 指向的锁以确定是否有某个已存在的锁会妨碍将新锁授予调用进程
- 当前没有锁,返回 l_type = F_UNLCK
- 已存在锁,返回锁信息,包括持有该锁的进程ID
- 提供该命令的原因:当指向 F_SETLK 命令返回错误时,获取导致错误文档某个锁信息(当然,连续使用两个命令不是原子操作,两个命令之间可能被解锁)
- 检查有 arg 指向的锁以确定是否有某个已存在的锁会妨碍将新锁授予调用进程
记录锁几个注意点
- 进程对文件上锁的类型受文件打开方式影响。
- 一个进程可以对某个文件的特定字节范围多次发出 F_SETLK 或 F_SETLKW 命令。每次成功与否取决于 其他进程 当时是否锁住该字节范围以及锁的类型,与本进程先前是否锁住该字节范围无关。
- 对于一个文件的任意字节,最多只能存在一种类型的记录锁(读锁或写锁)。一个给定字节可以有多个读锁,但只能有一个写锁。
- 进程终止内核自动清理 fcntl 记录锁,对于一个打开着某个文件的进程来说,当关闭文件的所有描述符或进程本身终止时,与该文件关联的所有记录锁都被删除。
- 不能继承,记录锁不能通过 fork 由子进程继承,记录锁跟进程ID紧密关联。
- 记录上锁不应该同标准I/O一块使用,因为标准I/O函数库会执行内部缓冲。应使用 read 和 write 系统调用。
劝告性上锁
Posix 记录上锁称为 劝告性上锁 (advisory locking)。其含义是内核维护着由各个进程上锁的所有文件的正确信息,但是它不能防止一个进程写一个读锁定文件,或者读一个写锁定文件。(类似互斥锁的协作性)
强制性上锁
有些系统提供另一种类型文档记录上锁,称为 强制性上锁 (mandatory locking)。使用强制性锁后,内核检查每个 read 和 write 请求,已验证起草组不会干扰由某个进程持有的某个锁。
若有干扰,对于阻塞式,冲突的 read 或 write 将把进程投入睡眠,直到锁释放。对于非阻塞式,返回 EAGAIN 错误。
强制性上锁也会导致不一致的数据,若下图:
Posix信号量
信号量是一种用于提供进程(或线程)间同步手段的原语。
- Posix有名信号量:由与文件系统中的路径名对应的名字来标识(并不要求它们真正的存放在文件系统内的某个文件)。
- Posix基于内存的信号量:即无名信号量,
信号量、互斥锁和条件变量间的差异
- 互斥锁总是由给它上锁的线程(或进程)解锁,信号量的挂出却不必由执行过它的等待操作的同一线程(或进程)执行。
- 互斥锁要么被锁在,要么被解开(二值状态,类似于 二值信号量 ,其值或为0或为1)。
- 信号量有一个与之关联的状态(计数值),信号量的挂出操作总是被记住。当条件满足,向一个条件变量发送信号时,若没有线程(或进程)等待该条件,则信号将会丢失。
- 在同步技巧(互斥锁、条件变量、读写锁、信号量)中,能够从信号处理程序中安全调用的唯一函数是
sem_post
。
Posix信号量函数调用
sem_open、sem_close和sem_unlink
// 创建一个新的有名信号量,或打开一个已存在的有名信号量
sem_t *sem_open(const char *name, int oflag /* , mode_t mode, unsigned int value */);
// 关闭 sem_open 打开的有名信号量
int sem_close(sem_t *sem);
// 从系统中删除指定的有名信号量
int sem_unlink(const char *name);
- 进程终止时(无论自愿与否),内核会对其上仍然打开着的所有 有名信号量 自动执行
sem_close
信号量关闭操作(不是释放)。 - 关闭一个信号量斌没有将它从系统中删除,即,Posix有名信号量 至少是 随内核持续 的:即使当前没有进程打开着某个信号量,它的值仍然保持。
sem_unlink
类似于文件I/O的 unlink 函数,当引用计数还大于0时,name
就能从文件系统中删除,但是信号量的析构(不同于将它名字从文件系统中删除)要等到最后一个sem_close
发生时为止。
sem_wait和sem_trywait
若信号量值大于0,将它减1。
int sem_wait(sem_t *sem); // 睡眠等待
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
sem_post和sem_getvalue
int sem_post(sem_t *sem);
int sem_getvalue(sem_t *sem, int *sval);
-
sem_post
:信号量值加1,若信号量值因此大于0,唤醒一个等待的进程(或线程)If the semaphore's value consequently becomes greater than zero, then another process or thread blocked in a sem_wait(3) call will be woken up and proceed to lock the semaphore.
-
sem_getvalue
:获取指定信号量的当前值。若有不少于一个进程(或线程)等待,返回 valp 值:
- = 0;
- < 0,绝对值为等待的进程(或线程)数量。
sem_init和sem_destroy
Posix基于内存的信号量 操作。
// 初始化一个基于内存的信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
shared = 0
:进程内各线程共享。shared != 0
:进程间共享。此时该信号量必须存放在某种类型的 共享内存区中 ,使用它的进程都要能访问该共享内存区。- 对于一个已初始化的信号量调用
sem_init
,其结果时未定义的。 - 基于内存的信号量 具有 随进程 的持续性,其真正的持续性取决于存放信号量的内存区的类型。
- 单个进程内线程共享,持续性随进程;
- 进程间共享,只要该共享内存区仍然存在,该信号量就继续存在。
信号量限制
Posix定义了两个信号量限制:
SEM_NSEMS_MAX
:一个进程可同时打开着的最大信号量数;SEM_VALUE_MAX
:一个信号量的最大值。