linux 线程同步方法

在POSIX标准中定义了三种线程同步机制: Mutexes(互斥量), Condition Variables(条件变量)和POSIX Semaphores(信号量)。NPTL基本上实现了POSIX,而glibc又使用NPTL作为自己的线程库。因此glibc中包含了这三种同步机制 的实现(当然还包括其他的同步机制,如APUE里提到的读写锁)。 
互斥锁:在于资源的互斥 
信号量:在于流程的同步 
条件变量:在于条件的满足

信号量,条件变量在使用的时候通常都结合互斥锁来共同使用。

为了能够有效的控制多个进程之间的沟通过程,保证沟通过程的有序和和谐,OS必须提供一定的同步机制保证进程之间不会自说自话而是有效的协同工作。比如在共享内存的通信方式中,两个或者多个进程都要对共享的内存进行数据写入,那么怎么才能保证一个进程在写入的过程中不被其它的进程打断,保证数据的完整性呢?又怎么保证读取进程在读取数据的过程中数据不会变动,保证读取出的数据是完整有效的呢?

常用的同步方式有: 互斥锁、条件变量、读写锁、记录锁(文件锁)和信号灯.

1、互斥锁 顾名思义,锁是用来锁住某种东西的,锁住之后只有有钥匙的人才能对锁住的东西拥有控制权(把锁砸了,把东西偷走的小偷不在我们的讨论范围了)。所谓互斥,从字面上理解就是互相排斥。因此互斥锁从字面上理解就是一点进程拥有了这个锁,它将排斥其它所有的进程访问被锁住的东西,其它的进程如果需要锁就只能等待,等待拥有锁的进程把锁打开后才能继续运行。 在实现中,锁并不是与某个具体的变量进行关联,它本身是一个独立的对象。进(线)程在有需要的时候获得此对象,用完不需要时就释放掉。 互斥锁的主要特点是互斥锁的释放必须由上锁的进(线)程释放,如果拥有锁的进(线)程不释放,那么其它的进(线)程永远也没有机会获得所需要的互斥锁。互斥锁主要用于线程之间的同步。

2、条件变量 上文中提到,对于互斥锁而言,如果拥有锁的进(线)程不释放锁,其它进(线)程永远没机会获得锁,也就永远没有机会继续执行后续的逻辑。在实际环境下,一个线程A需要改变一个共享变量X的值,为了保证在修改的过程中X不会被其它的线程修改,线程A必须首先获得对X的锁。现在假如A已经获得锁了,由于业务逻辑的需要,只有当X的值小于0时,线程A才能执行后续的逻辑,于是线程A必须把互斥锁释放掉,然后继续“忙等”。如下面的伪代码所示:

get x lock 
while(x <= 0){ 
unlock x ; 
wait some time 
get x lock } 
unlock x 
这种方式是比较消耗系统的资源的,因为进程必须不停的主动获得锁、检查X条件、释放锁、再获得锁、再检查、再释放,一直到满足运行的条件的时候才可以。因此我们需要另外一种不同的同步方式,当线程X发现被锁定的变量不满足条件时会自动的释放锁并把自身置于等待状态,让出CPU的控制权给其它线程。其它线程此时就有机会去修改X的值,当修改完成后再通知那些由于条件不满足而陷入等待状态的线程。这是一种通知模型的同步方式,大大的节省了CPU的计算资源,减少了线程之间的竞争,而且提高了线程之间的系统工作的效率。这种同步方式就是条件变量。 坦率的说,从字面意思上来将,“条件变量”这四个字是不太容易理解的。我们可以把“条件变量”看做是一个对象,一个铃铛,一个会响的铃铛。当一个线程在获得互斥锁之后,由于被锁定的变量不满足继续运行的条件时,该线程就释放互斥锁并把自己挂到这个“铃铛”上。其它的线程在修改完变量后,它就摇摇“铃铛”,告诉那些挂着的线程:“你们等待的东西已经变化了,都醒醒看看现在的它是否满足你们的要求。”于是那些挂着的线程就知道自己醒来看自己是否能继续跑下去了。

3、读写锁 互斥锁是排他性锁,条件变量出现后和互斥锁配合工作能够有效的节省系统资源并提高线程之间的协同工作效率。互斥锁的目的是为了独占,条件变量的目的是为了等待和通知。但是现实世界是很复杂的,我们要解决的问题也是多种多样的.从功能上来说,互斥锁和条件变量能够解决基本上所有的问题,但是性能上就不一定完全满足了。人的无休止的欲望促使人发明出针对性更强、性能更好的同步机制来。读写锁就是这么一个玩意儿。 考虑一个文件有多个进程要读取其中的内容,但只有1个进程有写的需求。我们知道读文件的内容不会改变文件的内容,这样即使多个进程同时读相同的文件也没什么问题,大家都能和谐共存。当写进程需要写数据时,为了保证数据的一致性,所有读的进程就都不能读数据了,否则很可能出现读出去的数据一半是旧的,一半是新的状况,逻辑就乱掉了。 为了防止读数据的时候被写入新的数据,读进程必须对文件加上锁。现在假如我们有2个进程都同时读,如果我们使用上面的互斥锁和条件变量,当其中一个进程在读取数据的时候,另一个进程只能等待,因为它得不到锁。从性能上考虑,等待进程所花费的时间是完全的浪费,因为这个进程完全可以读文件内容而不会影响第一个,但是这个进程没有锁,所以它什么也做不了,只能等,等到花儿都谢了。 所以呢,我们需要一种其它类型的同步方式来满足上面的需求,这就是读写锁。 读写锁的出现能够有效的解决多进程并行读的问题。每一个需要读取的进程都申请读锁,这样大家互不干扰。当有进程需要写如数据时,首先申请写锁。如果在申请时发现有读(或者写)锁存在,则该写进程必须等待,一直等到所有的读(写)锁完全释放为止。读进程在读取之前首先申请读锁,如果所读数据被写锁锁定,则该读进程也必须等待读锁被释放位置。 很自然的,多个读锁是可以共存的,但写锁是完全互相排斥的。 记录锁(文件锁) 为了增加并行性,我们可以在读写锁的基础上进一步细分被锁对象的粒度。比如一个文件中,读进程可能需要读取该文件的前1k个字节,写进程需要写该文件的最后1k个字节。我们可以对前1k个字节上读锁,对最后1k个自己上写锁,这样两个进程就可并发工作了。记录锁中的所谓“记录”其实是“内容”的概念。使用读写锁可以锁定一部分,而不是整个文件。 文件锁可以认为是记录锁的一个特例,当使用记录锁锁定文件的所有内容时,此时的记录锁就可以称为文件锁了。

4、信号灯 信号灯可以说是条件变量的升级版。条件变量相当于铃铛,铃铛响后每个挂起的进程还需要自己获得互斥锁并判断所需条件是否满足,信号灯把这两步操作糅合到一起。 在Posix.1基本原理一文声称,有了互斥锁和条件变量还提供信号灯的原因是:“本标准提供信号灯的而主要目的是提供一种进程间同步的方式;这些进程可能共享也可能不共享内存区。互斥锁和条件变量是作为线程间的同步机制说明的;这些线程总是共享(某个)内存区。这两者都是已广泛使用了多年的同步方式。每组原语都特别适合于特定的问题”。尽管信号灯的意图在于进程间同步,互斥锁和条件变量的意图在于线程间同步,但是信号灯也可用于线程间,互斥锁和条件变量也可用于进程见。应当根据实际的情况进行决定。 信号灯最有用的场景是用以指明可用资源的数量。比如含有10个元素的数组,我们可以创建一个信号灯,初始值为0.每当有进程需要读数组中元素时(假设每次仅能读取1个元素),就申请使用该信号灯(信号灯的值减1),当有进程需要写元素时,就申请挂出该信号等(信号灯值加1)。这样信号灯起到了可用资源数量的作用。如果我们限定信号灯的值只能取0和1,就和互斥锁的含义很相同了

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct
{
	int head;
	int tail;
	int buf[100];
}QUEUE;

pthread_mutex_t mutex;
pthread_cond_t notempty;
pthread_cond_t notfull;

QUEUE q;
int is_full(void)
{
	return (q.tail+1)%100 == q.head;
}
int is_empty(void)
{
	return q.tail == q.head;
}
void *producer(void *args)
{
	int i;
	for(i = 0; i < 200; i++)
	{
		pthread_mutex_lock(&mutex);
		while(is_full())
		{
			printf("queue is full, wait...\n");
			pthread_cond_wait(¬full, &mutex);
		}
		q.buf[q.tail] = i;
		q.tail = (q.tail + 1)%100;
		printf("push %d\n", i);
		pthread_mutex_unlock(&mutex);
		pthread_cond_signal(¬empty);
	}
}
void *consumer(void *args)
{
	int i, val;
	for(i = 0; i < 200; i++)
	{
		pthread_mutex_lock(&mutex);
		while(is_empty())
		{
			printf("queue is empty, wait...\n");
			pthread_cond_wait(¬empty, &mutex);
		}
		val = q.buf[q.head];
		q.head = (q.head + 1) % 100;
		printf("pop %d\n", val);
		pthread_mutex_unlock(&mutex);
		pthread_cond_signal(¬full);
	}
}
int main(int argc, char *argv[])
{
	pthread_t pro_tid, con_tid;

	q.head = 0;
	q.tail = 0;
	pthread_mutex_init(&mutex, NULL);
	pthread_cond_init(¬empty, NULL);
	pthread_cond_init(¬full, NULL);

	pthread_create(&pro_tid, NULL, producer, NULL);
	pthread_create(&con_tid, NULL, consumer, NULL);

	pthread_join(pro_tid, NULL);

	pthread_join(con_tid, NULL);

	pthread_cond_destroy(¬empty);
	pthread_cond_destroy(¬full);
	pthread_mutex_destroy(&mutex);
	exit(0);
}

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

pthread_mutex_t mutex;
int val = 0;
void *thread_fn(void *arg)
{
	int i;
	for(i = 0; i < 3; i++)
	{
		pthread_mutex_lock(&mutex);
		printf("thread_fn: %d\n", val);
		pthread_mutex_unlock(&mutex);
		sleep(1);
	}
	pthread_exit(0);
}

int main(int argc, char *argv[])
{

	pthread_t tid;
	int i;
	pthread_mutex_init(&mutex, NULL);
	if((tid = pthread_create(&tid, NULL, thread_fn, NULL)) < 0)
	{
		perror("pthread_create");
	}
	for(i = 0; i < 3; i++)
	{
		pthread_mutex_lock(&mutex);
		val++;
		printf("main: %d\n", val);
		pthread_mutex_unlock(&mutex);
		sleep(5);
	}
	pthread_join(tid, NULL);
	pthread_mutex_destroy(&mutex);
	printf("\n\ntest over\n");

}

一、读写锁

1.基本概念

当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。因而有时候将读和写访问区分开来是有益处的。
读写锁就提供了这样的能力。通过读写锁,可以对受保护的共享资源进行并发读取和独占写入。其规则为:
  1. 只要没有线程持有某个特定的读写锁用于写,那么任意数目的线程都可以持有该读写锁进行读
  2. 仅当没有任何线程持有某个特定的读写锁用于写或读时,才能分配该读写锁给某个线程让其进行写
读写锁适用于有很多并发读请求,同时写比较少的场景。由于读写锁允许多个读者并存因而提高了并发性;同时由于任意时刻只有一个写者可以持有读写锁,又可以保护数据在写期间不被其它读者或写者所干扰。

2.API

POSIX定义的读写锁的数据类型是: pthread_rwlock_t
[cpp]  view plain copy
  1. #include <pthread.h>  
  2. int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); 成功返回0,其它返回值表示出错  
  3. int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock ); 成功返回0,其它返回值表示出错  
  4. int pthread_rwlock_timedrdlock(pthread_rwlock_t *rwlock, const struct timespec *abs_timeout); 成功返回0,其它返回值表示出错  
  5. int pthread_rwlock_reltimedrdlock_np(pthread_rwlock_t *rwlock, const struct timespec *abs_timeout); 成功返回0,其它返回值表示出错  
  6. int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); 成功返回0,其它返回值表示出错  
  7. int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock ); 成功返回0,其它返回值表示出错  
  8. int  pthread_rwlock_timedwrlock(pthread_rwlock_t  *rwlock, const struct timespec *abs_timeout); 成功返回0,其它返回值表示出错  
  9. int pthread_rwlock_reltimedwrlock_np(pthread_rwlock_t *rwlock, const struct timespec *abs_timeout); 成功返回0,其它返回值表示出错  
  10. int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); 成功返回0,其它返回值表示出错  
  11. int pthread_rwlock_unlock (pthread_rwlock_t *rwlock); 成功返回0,其它返回值表示出错  
  12. int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); 成功返回0,其它返回值表示出错  
  13. int pthread_rwlockattr_init(pthread_rwlockattr_t *attr); 成功返回0,其它返回值表示出错  
  14. int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr); 成功返回0,其它返回值表示出错  
  15. int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared); 成功返回0,其它返回值表示出错  
  16. int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared); 成功返回0,其它返回值表示出错  

1)初始化读写锁

如果读写锁变量是静态的则可以直接用PTHREAD_RWLOCK_INITIALIZER来初始化它,比如:
static pthread_rwlock_t my_rwlock = PTHREAD_RWLOCK_INITIALIZER
如果读写锁变量是动态分配的,则必须在使用它之前用pthread_rwlock_init来初始化它。
pthread_rwlock_init用来初始化rwlock所指向的读写锁,如果attr为NULL则会使用缺省的属性初始化读写锁,否则使用指定的attr初始化读写锁。
使用PTHREAD_RWLOCK_INITIALIZER 宏与动态分配具有null 属性的 pthread_rwlock_init等效,不同之处在于PTHREAD_RWLOCK_INITIALIZER 宏不进行错误检查。

2)在读写锁上获取读锁(读锁定)

pthread_rwlock_rdlock用来读锁定指定的读写锁。
  1. 如果没有写者持有该锁,并且没有写者阻塞在该锁上,则调用线程会获取读锁。
  2. 如果没有写者持有该锁,但是有写者阻塞在该锁上,则调用线程是否能获取该锁是不确定的。
  3. 如果有写者持有该锁,则调用线程无法获取该锁。
  4. 如果在进行调用时,调用线程已经写锁定了该锁,则结果是不确定的。
  5. 如果调用pthread_rwlock_rdlock时所指定的读写锁未初始化,则结果是不确定的。
如果调用线程未获取读锁,则它将阻塞直到它获取了该锁。为避免写者饥饿,允许将写者的优先级设置的高于读者。
一个线程可以在一个读写锁上多次执行读锁定。线程可以成功调用pthread_rwlock_rdlock n 次,但是之后该线程必须调用 pthread_rwlock_unlock() n 次才能解除锁定。
信号处理程对这里的等待是透明的:线程信号处理程序可以处理传送给等待读写锁的线程的信号。从信号处理程序返回后,线程将继续等待读写锁,就好像线程未被中断一样。

3)在指定的时间之前在读写锁上获取读锁(读锁定)

pthread_rwlock_timedrdlock用来读锁定指定的读写锁,如果必须等待以获取读锁,则最多等待指定的时长后就不再等待。
如果锁可以立即被获得,那么时间参数就不会被检查。
信号处理程对这里的等待是透明的:如果等待被信号处理程序打断了,则从信号处理程序返回后,会继续等待就像没有被打断一样。
如果在进行调用时,调用线程已经写锁定了该锁则可能导致死锁。
pthread_rwlock_reltimedrdlock_np与pthread_rwlock_timedrdlock基本相同,它们唯一的区别在于前者使用相对时间间隔而不是将来的绝对时间作为其最后一个参数的值。

4)尝试在读写锁上以非阻塞的方式获取读锁(读锁定)

pthread_rwlock_tryrdlock用于尝试以非阻塞的方式来在读写锁上获取读锁。如果有任何的写者持有该锁或有写者阻塞在该读写锁上,则立即失败返回。

5)在读写锁上获取写锁(写锁定)

pthread_rwlock_wrlock用来写锁定指定的读写锁。
  1. 如果没有写者持有该锁,并且没有写者读者持有该锁,则调用线程会获取写锁。否则线程阻塞。
  2. 如果在进行调用时,调用线程已经写锁定或读锁定了该锁,则结果是不确定的。
  3. 如果调用pthread_rwlock_rdlock时所指定的读写锁未初始化,则结果是不确定的。
如果调用线程未获取写锁,则它将阻塞直到它获取了该锁。为避免写者饥饿,允许将写者的优先级设置的高于读者。
一个线程可以在一个读写锁上多次执行读锁定。线程可以成功调用pthread_rwlock_rdlock n 次,但是之后该线程必须调用 pthread_rwlock_unlock() n 次才能解除锁定。
信号处理程对这里的等待是透明的:线程信号处理程序可以处理传送给等待读写锁的线程的信号。从信号处理程序返回后,线程将继续等待读写锁,就好像线程未中断一样。

6)在指定的时间之前在读写锁上获取写锁(写锁定)

pthread_rwlock_timedwrlock用来写锁定指定的读写锁,如果没有写者持有该锁,并且没有写者读者持有该锁,则调用线程会获取写锁,否则将等待,但是最多等待指定的时长后就不再等待。
pthread_rwlock_reltimedwrlock_np与pthread_rwlock_timedwrlock基本相同,它们唯一的区别在于前者使用相对时间间隔而不是将来的绝对时间作为其最后一个参数的值。

7)尝试在读写锁上以非阻塞的方式获取读锁(读锁定)

pthread_rwlock_trywrlock用于尝试以非阻塞的方式来在读写锁上获取写锁。如果有任何的读者或写者持有该锁,则立即失败返回。

8)尝试在读写锁上以非阻塞的方式获取读锁(读锁定)

pthread_rwlock_destroy用于销毁一个读写锁,并释放所有相关联的资源(所谓的所有指的是由pthread_rwlock_init自动申请的资源)

9)初始化读写锁属性

pthread_rwlockattr_init使用缺省的属性来初始化读写锁属性指定的读写锁属性对象。
如果指定的读写锁属性对象已经初始化,则结果是不确定的。

10)销毁读写锁属性

pthread_rwlockattr_destroy可用来销毁读写锁属性对象。
在再次调用 pthread_rwlockattr_init重新初始化该对象之前,使用该读写锁的结果是不确定的。

11)设置/获取读写锁的作用范围属性

pthread_rwlockattr_setpshared用来设置读写锁的作用范围,设置的值存放在指定的读写锁属性对象中。
作用范围的取值及其含义:
  • PTHREAD_PROCESS_SHARED:该读写锁可以在多个进程中的线程之间共享。
  • PTHREAD_PROCESS_PRIVATE:仅初始化本读写锁的线程所在的进程内的线程才能够使用该读写锁。
pthread_rwlockattr_getpshared用来获取指定的读写锁属性对像上的作用范围属性。

二、信号量

1.基本概念

信号量是用来在进程间或者一个进程的多个线程之间进行同步/互斥的原语。
信号量一般用整数表示,经典的信号量上的操作是P操作(wait,down,lock)和V操作(post,up,unlock)。其使用方法为:
  1. 线程发起P操作尝试将信号量的值减1,如果信号量的值在发起该P操作之前为正值,则P操作成功返回,并且信号量的值被减1;否则线程阻塞直到信号量的值变为正值
  2. 线程完成自己需要在信号量保护下进行的操作
  3. 线程调用V操作将信号量的值加1
P、V操作必须以原子方式执行,不能再将其划分成子操作,即,在这些子操作之间不能对信号量执行其他操作。在P操作中,信号量的值在减小之前必须为正,从而确保生成的信号量的值不为负,并且比该值减小之前小 1。信号量通常用来协调对资源的访问,其中信号量计数会初始化为可用资源的数目。
有两种基本信号量:二进制信号量和计数信号量。二进制信号量的值只能是 0 或 1,计数信号量可以是任意非负值。二进制信号量在逻辑上相当于一个互斥锁。因而二进制的信号量可以像互斥锁那样来使用:
sem_wait(...)
临界区
sem_post(...)
但是二者存在一些区别,互斥锁一般应当仅由持有该锁的线程来解除锁定,但是由于不存在“持有信号量的线程”这一概念,所以,任何线程都可以执行V操作。
另外注意到由于信号量的V操作不一定要由发起P操作的线程来执行,因而V操作就可以由其它线程来执行,这就类似于条件变量的功能了:一个线程A等待某个条件被满足,另一个线程B通知线程A“你所等待的条件被满足了”。进一步的说信号量可用于异步事件通知,如用于信号处理程序中,并且不用象条件变量那样要求获取互斥锁。但是,信号量的效率不如互斥锁高。
缺省情况下,如果有多个线程正在等待信号量,则解除阻塞的顺序是不确定的。信号量在使用前必须先初始化,但是信号量没有属性。
总体上来说,信号量和互斥锁、条件变量之间有类似之处,但是也有区别,它们之间的区别:
  1. 互斥锁一般由锁定该锁的线程(即持有者)解锁,但是信号量的V操作不一定要由发起P操作的线程来执行,条件变量的唤醒一般由其它线程来执行
  2. 互斥锁要么是锁定的,要么是非锁定的,它类似于二进制信号量
  3. 只有条件变量会因为被信号打断而返回
  4. 由于信号量有一个相关的状态即其计数值,因而信号量上的V操作是被记住了的;但是对于条件变量来说如果发出signal时没有线程在等待该signal,则这个通知就会丢失
  5. 信号量不用像条件变量那样和互斥锁配合使用,但是其效率不如条件变量高
  6. 只有sem_post可以在信号处理程序中调用
POSIX定义了两种信号量:
  1. 命名信号量:命名信号量由符合Posix定义的IPC命名规则的名字标识,可以被用于进程之间或线程之间的同步互斥。命名信号具有属主用户 ID、组 ID 和保护模式。
  2. 未命名信号:未命名信号量在进程内存中分配,需要进行初始化,它可以被用于进程之间或线程之间的同步互斥,是否可用于多进程环境取决于信号的分配和初始化方式。

2.API

与信号量相关的数据结构类型是sem_t
[cpp]  view plain copy
  1. #include <semaphore.h>  
  2. int sem_init(sem_t *sem, int pshared, unsigned int value);成功返回0,其它返回值表示出错  
  3. int sem_destroy(sem_t *sem); 成功返回0,其它返回值表示出错  
  4. int sem_post(sem_t *sem); 成功返回0,其它返回值表示出错  
  5. int sem_wait(sem_t *sem); 成功返回0,其它返回值表示出错  
  6. int sem_trywait(sem_t *sem); 成功返回0,其它返回值表示出错  
  7. int sem_getvalue(sem_t *sem, int *value);成功返回0,其它返回值表示出错  
  8. sem_t *sem_open(const char *name, int oflag, .../* mode_t mode, unsigned int value */);成功时返回指向信号量的指针,否则返回SEM_FAILED.  
  9. int sem_close(sem_t *sem);成功返回0,其它返回值表示出错  
  10. int sem_unlink(const char *sem);成功返回0,其它返回值表示出错  
需要说明的是
sem_wait,sem_trywait,sem_post,sem_getvalue四个API对于命名信号和未命名信号都适用
sem_init和sem_destroy仅适用于未命名信号
sem_open,sem_close和sem_unlink仅适用于命名信号

1)初始化未命名信号 

sem_init用于将指定的信号量的初始值设置为value。
如果pshared的值为零,则该信号量只能由创建它的线程所在的进程内的线程使用。如果pshared的值不为零,则该信号量必须位于进程的共享内存内,信号量可以由所有可以访问这片共享内存的进程内的线程所使用。
多个线程不能初始化同一个信号量。
不能重新初始化其它线程正在使用的信号量。

2)销毁未命名信号 

sem_destroy用于销毁与 sem 所指示的未命名信号相关联的任何状态(但是不会释放存储信号量的内存)。

3)增加信号量(V操作)

sem_post实现V操作,用来以原子方式增加 sem 所指示的信号量额值。

4)基于信号量的计数值阻塞 

sem_wait实现P操作,它尝试将信号量的值减1,如果信号量的值在发起该P操作之前为正值,则立即返回,并且信号量的值被减1;否则将阻塞直到信号量的值变为正值

5)尝试基于信号量的计数值阻塞 

sem_wait实现P操作,它尝试将信号量的值减1,如果信号量的值在发起该P操作之前为正值,则立即返回,并且信号量的值被减1;否则将返回EAGAIN错误

6)返回指定信号量的当前计数值

sem_getvalue返回指定信号量的当前计数值。
如果指定的信号量当前处于阻塞状态,则返回值为0或者负值;如果是负值,则该值的绝对值等于当前阻塞在该信号量上的线程的数目

7)创建一个新的命名信号量或者打开已经存在的命名信号量

sem_open用于创建一个新的命名信号量或者打开已经存在的命名信号量,命名信号总是可用于进程间或线程间同步、互斥。
oflag可以为0,O_CREAT或O_CREAT|O_EXCL的取值及其含义
如果oflag包含O_CREAT,则需要指定第3和第4个参数,其中第三个参数的含义类似于文件的模式;第4个参数即为信号量的初始值
如果oflag未包含O_EXCL而是仅包含O_CREAT,则其含义是如果指定名字的信号量不存在就用指定的模式和初始值创建并初始化它,因而即使信号量已经存在也不算是错误
如果oflag同时包含O_EXCL和含O_CREAT,则如果指定名字的信号量已经存在就会返回错误

7)关闭指定的命名信号量

sem_close用来关闭用sem_open打开的命名信号量。
在进程终止时,所有由该进程打开的命名信号量都会自动被关闭,不管进程是主动退出的还是被“杀死的”。
但是关闭命名信号量并不会从系统删除该命名信号量。命名信号量至少具有内核持久性。
内核持久性是和资源的存在时间相关的一个概念,其相关的概念:
  • 内核持久性:指的是只要系统没有重启并且资源没有被显式的删除它,它就会一直存在。
  • 进程持久性:指的是只要还有任何一个打开该资源的进程还没有关闭它,它就会一直存在
  • 文件持久性:只要没有显式的删除资源,它就一直存在,即便系统重启也如此

8)删除指定的命名信号量

sem_unlink用于从系统删除指定的命名信号量。它类似于文件系统的unliink函数,真正的删除只有在所有打开该命名信号量的线程都关闭了它之后才会发生。

三、记录锁

1.基本概念

除了POSIX定义的这些同步对象外,*nix系统还提供另外一种锁--记录锁。
记录锁用于对文件的访问控制,可以通过文件描述符找到被锁的文件,上锁是通过fcntl来完成的。记录锁可以用于相关联的进程之间也可以用于无关的进程之间。
记录锁的功能是:一个进程正在读或修改文件的某个部分时,可以阻止其他进程修改同一文件区。记录锁锁定的是文件的一个区域(也可能是整个文件)。记录锁通常维护在内核中,锁的持有者由锁持有者的进程ID标识。

2.API

[cpp]  view plain copy
  1. #include <fcnt1.h>  
  2. int fcnt1(int fd ,int cmd,.../* struct flock *arg */ ) ;  
  3. 对于记录锁, cmd是FGETLK、FSETLK或FSETLKW。第三个参数是一个指向flock结构的指针。  
  4. struct flock{  
  5.     short l_type;  
  6.     short l_whence;  
  7.     off_t l_start;  
  8.     off_t l_len;  
  9.     pid_t l_pid;  
  10. };  
flock结构的域极其含义:
  • l_type:所希望的锁类型,F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)或F_UNLCK(解锁一个区域)
  • l_start:要加锁或解锁的区域的起始地址,由l_start和l_whence两者决定。l_start是相对位移量(字节),l_whence则决定了相对位移量的起点。
  • l_whence:SEEK_SET,SEEK_CUR,SEEK_END.其含义类似于文件系统中的同名宏的含义
  • l_len: 加锁区域的长度。
关于加锁和解锁区域的还要注意:
  • 该区域可以在当前文件尾端处开始或越过其尾端处开始,但是不能在文件起始位置之前开始或越过该起始位置。
  • 如若l_len为0,则表示锁的区域从其起点(由l_start和l_whence决定)开始直至最大可能位置为止。也就是不管添写到该文件中多少数据,它都处于锁的范围。
  • 为了锁整个文件,通常的方法是将l_start设置为0,l_whence设置为SEEK_SET,l_len设置为0。
共享读锁(l_type为F_RDLCK)和独占写琐(F_RDLCK)的规则:
  • 多个进程在一个给定的字节上可以有一把共享的读锁,但是在一个给定字节上的写锁则只能由一个进程独用。更进一步而言,如果在一个给定字节上已经有一把或多把读锁,则不能在该字节上再加写锁;如果在一个字节上已经有一把独占性的写锁,则不能再对它加任何读锁。
  • 加读锁时,该描述符必须是读打开;
  • 加写锁时,该描述符必须是写打开。
fcntl函数的三种命令:
  • F_GETLK:判断由arg所指定的锁是否被另外一把锁所排斥(阻塞)。如果存在一把锁,它阻止创建由arg所描述的锁,则这把现存的锁的信息写到arg指向的结构中。如果不存在这种情况,则除了将l_type设置为F_UNLCK之外,arg所指向结构中的其他信息保持不变。
  • F_SETLK:设置由arg所描述的锁。如果试图设置的锁不被规则所允许,则立即出错返回,此时errno设置为EACCES或EAGAIN。
  • F_SETLKW:它是F_SETLK的阻塞版本(命令名中的W表示等待(wait))。如果由于存在其他锁而导致由arg所要求的锁不能被创建,则调用进程睡眠。如果捕捉到信号则睡眠中断。
一个进程持有的记录锁会在进程关闭与该锁相关联的文件时或者进程终止时被自动删除。记录在进程调用fork时不会被继承。
记录锁不能和标准I/O库一起使用,因为标准I/O库使用了缓存。如果要使用记录锁,就要直接使用read和write。
记录锁也是一种协作性锁(某些系统支持强制性的记录锁。强制性锁机制中,内核对每一个open、read和write都要检查调用进程对正在存取的文件是否违背了某一把锁的作用。)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值