4 用户态和内核态信号量-实验1:使用POSIX信号量实现线程同步
一.实验目的
·掌握在线程同步问题中POSIX无名信号量和有名信号量的使用方法。
·理解POSIX无名信号量和有名信号量的差异。
二.实验背景
·什么是信号量
操作系统教科书中信号量的定义,信号量是包含一个整型变量S并且带有两个原子操作wait 和 signal 的抽象数据类型。·wait 操作也被称为 down、P和 lock。调用wait 操作时,如果信号量的整型变量S>0,wait 就将S减1,调用wait 的进程或则线程继续运行;如果S <0,wait 将S减1后把调用进程或线程挂起在信号的等待队列上。
·signal 操作也被称为 up、V或unlock。调用signal 操作时,signal 将S加1;如果S加1后的数值任然大于0,说明用进程或线程挂起在信号量的等待队列上,处于信号量等待队列队首的线程将被唤醒,使其从wait 中返回。
·进程和线程的差异
进程概念
线程概念
多个进程之间可能存在父子关系或兄弟关系,但也可能完全无关;
隶属于同一个进程的线程之间,一定存在相关性,
同步问题,包含狭义的同步与互斥,教材P.202
区分同步与互斥·Recall: 广义的同步又可以分为狭义的同步和互斥,定义如下:
互斥:一组并发进程中的一个或多个程序段,因共享某一公有资源而导致它们必须以一个不允许交叉执行的单位执行。
同步(狭义):异步环境下的一组并发进程,因直接制约而互相发送消息而进行相互合作、互相等待,使得各进程按一定的速度执行的过程称为进程间的同步·单向同步问题模型 教材P.210
semaphore notempty=0;//声明信号量notempty,初值为0,表示没有产品 producer() { /* 生产产品 */ ...... signal(notempty);//设置缓冲区非空的信号量 } consumer() { wait(notempty); // 等待缓冲区非空的通知 /* 消费产品*/ ....... }
·双向同步问题模型 教材P.211
semaphore notfull=1, notempty=0;//声明信号量 producer() { wait(notfull) // 等待缓冲区非满的通知 /* 生产产品 */ ...... signal(notempty); //设置缓冲区非空的信号量 } consumer() { wait(notempty);// 等待缓冲区非空的通知 /* 消费产品*/ ....... signal(notfull); //设置缓冲区非满的信号量 }
·同步与互斥相结合 教材P.211:一个生产者对N个消费者
semaphore notfull=1, notempty=0,lockc=1;//声明信号量 producer() { wait(notfull) // 等待缓冲区非满的通知 /* 生产产品 */ ...... signal(notempty); //设置缓冲区非空的信号量 } consumer() { wait(notempty); // 等待缓冲区非空的通知 wait(lockc); // 对产品消费行为加锁 /* 消费产品*/ ....... signal(lockc); // 消费完成后解锁 signal(notfull); //设置缓冲区非满的信号量 }
·同步与互斥相结合 教材P.212:M个生产者对N个消费者
semaphore notfull=1, notempty=0,lockc=1, lockp=1;//声明信号量 producer() { wait(notfull) // 等待缓冲区非满的通知 wait(lockp); // 对生产行为加锁 /* 生产产品 */ ...... signal(lockp); // 生产完成后解锁 signal(notempty); //设置缓冲区非空的信号量 } consumer() { wait(notempty);// 等待缓冲区非空的通知 wait(lockc); // 对产品消费行为加锁 /* 消费产品*/ ....... signal(lockc); // 消费完成后解锁 signal(notfull);//设置缓冲区非满的信号量 }
.一个互斥信号量的wait和signal操作一般总是在一个线程或进程中成对出现。与之相对,一个(狭义)同步信号量的wait和signal操作总是出现在两个不同的线程或进程中。原因是,(狭义)同步调节的是两个不同线程或进程之间的行为关系。
·互斥问题是以资源为核心,其协调的是多个进程(线程)和资源之间的使用关系;而同步问题是以事件或者消息为核心,协调的是进程(线程)和进程(线程)之间的行为顺序关系。
·在互斥问题中一个信号量的初值代表的是该类资源的数量;而在同步问题中信号量代表的是事件或消息的初始状态。
同类进程(线程)多数是互斥关系,而同步关系往往发生在不同类的进程(线程)间。·POSIX信号量的定义,教材P.212·无名信号量:
象变量一样声明
使用时需要进行初始化和销毁int sem_init(sem_t *sem, int pshared,unsigned value); int sem_destroy(sem_t *sem);
与操作系统教科书中的waiti和signal对应的操作函数为
int sem_wait(sem_t *sem); int sem_post(sem_t *sem);
·有名信号量:
需要创建,创建时赋予一个系统级的名字sem_t *sem_open(const char *name,int oflag,mode_t mode,unsigned int value);
使用完毕后需要进行关闭,然后销毁
int sem_close(sem_t *sem); int sem_unlink(const char *name);
与操作系统教科书中的waiti和signal对应的操作函数为
int sem_wait(sem_t *sem); int sem_post(sem_t *sem);
三.关键代码及分析
1.无名信号量用于线程间的互斥
· sem_t lock; /* 信号量lock用于对缓冲区的互斥操作 */
sem_init(&lock,0,1);//无名信号量初始化
因为是用于线程中,各个线程共享进程的数据段,所以在sem_init 函数中传入的第二个参数为0,表示该信号只在本进程的线程间使用;第三个参数为1,表示该信号量的初始值为1.
其次在创建的子进程中访问共享变量的代码前后加入互斥锁的加锁和解锁语句:sem_wait(&lock); //信号量加锁 for (i = 0; i < upper; i++) { temp = counter; temp += 1; counter = temp; } sem_post(&lock);//信号量解锁
上述例子中,在加锁和解锁两行语句之间的代码就成为了临界区,在一个时间点上只允许一个线程进人。因此共享变量counter就不会被多个线程竞争访问,造成相互之间的覆盖。由于信号量lock的初值为1,所以第一个调用sem_wait函数的进程将通过sem_wait测试,进人临界区执行。当临界区有线程在执行时,其他调用sem_wait 函数的线程将被挂起在lock上,直到临界区内的线程在退出临界区时调用sem_post 函数。Sem_post函数会检测是否有线程阻塞在lock上,如果有就唤醒阻塞的某个线程;如果没有,就将lack的值加1,将其恢复到初始值。
2.无名信号量用于线程间的互斥
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) sem_t *lock; /* 信号量lock用于对缓冲区的互斥操作 */ lock= sem_open("lock",O_CREAT,FILE_MODE,1);//打开有名信号量
该函数创建一个名字为lock,访问权限为0644,初始值为1的信号量lock.因为是用于线程中,sem_open 创建的信号量可以在各个线程之间共享。FILE_MODE表示信号量的权限,以UGO的形式表示,其中S_IRUSR文件拥有者有读权限、S_IWUSR文件拥有者有写权限、S_ IRGRP表示文件组有读权限,S_ IROTH表示其他组有读权限。最后一个参数为1,表示该信号量的初值为1.
其次在创建的子线程中访问共享变量的代码前后加人互斥锁的加锁和解锁语句,示例如下:
sem_wait(lock); //信号量加锁 for (i = 0; i < upper; i++) { temp = counter; > temp += 1; counter = temp; } sem_post(lock);//信号量解锁
与无名信号量略有差异的是,声明lock时使用的是信号量指针类型,所以在sem_wait和sem_post中没有加&。其功能与无名信号量相同。
3.POSIX 信号量用于线程间的(狭义)同步
·无名信号量解决生产者-消费者问题
sem_t lock; /* 信号量lock用于对缓冲区的互斥操作 */
sem_t notempty; /* 缓冲区非空的信号量 */
sem_t notfull; /* 缓冲区未满的信号量 */
struct prodcons
{// 缓冲区相关数据结构
int buf[BUFFER_SIZE]; /* 实际数据存放的数组*/
int readpos, writepos; /* 读写指针*/
};
struct prodcons buffer;
init(&buffer);
//无名信号量初始化
sem_init(&lock,0,1);
sem_init(¬empty,0,0);
sem_init(¬full,0,BUFFER_SIZE);
/* 创建生产者和消费者线程*/
pthread_create(&th_c, NULL, producer, 0);
pthread_create(&th_p, NULL, consumer, 0);
/* 等待两个线程结束*/
pthread_join(th_c, &retval);
pthread_join(th_p, &retval);
//销毁无名信号量
sem_destroy(&lock);
sem_destroy(¬empty);
sem_destroy(¬full);
/* 其中,notfull 的初始值由缓冲区的个数来决定
其次在生产者和消费者线程代码中加入互斥和同步的命令:
*/
void *producer(void *data)
{
int n;
for (n = 0; n <= PRO_NO; n++)
{
sem_wait(¬full);
sem_wait(&lock);
/* 写数据,并移动指针 */
if (n < PRO_NO)
{
buffer.buf[buffer.writepos] = n;
printf("%d --->\n", n);
usleep(PSLEEP);
}
else
{
buffer.buf[buffer.writepos] = OVER;
printf("%d --->\n", OVER);
}
buffer.writepos++;
if (buffer.writepos >= BUFFER_SIZE)
buffer.writepos = 0;
/* 设置缓冲区非空的条件变量*/
sem_post(¬empty);
sem_post(&lock);
}
return NULL;
}
void *consumer(void *data)
{
int d;
while (1)
{
/* 等待缓冲区非空*/
sem_wait(¬empty);
sem_wait(&lock);
/* 读数据,移动读指针*/
d = buffer.buf[buffer.readpos];
//usleep(CSLEEP);
buffer.readpos++;
if (buffer.readpos >= BUFFER_SIZE)
buffer.readpos = 0;
printf("--->%d \n", d);
if (d == OVER)
break;
/* 设置缓冲区未满的条件变量*/
sem_post(¬full);
sem_post(&lock);
}
return NULL;
}
·有名信号量解决生产者-消费者问题
//创建模式权限
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define BUFFER_SIZE 16 // 缓冲区数量
#define SEM_MUTEX "lock" /* 信号量lock用于对缓冲区的互斥操作 */
#define SEM_NEMPTY "notempty" /* 缓冲区非空的信号量 */
#define SEM_NFULL "notfull" /* 缓冲区未满的信号量 */
sem_t *lock; /* 信号量lock用于对缓冲区的互斥操作 */
sem_t *notempty; /* 缓冲区非空的信号量 */
sem_t *notfull; /* 缓冲区未满的信号量 */
struct prodcons
{// 缓冲区相关数据结构
int buf[BUFFER_SIZE]; /* 实际数据存放的数组*/
int readpos, writepos; /* 读写指针*/
};
init(&buffer);
//创建有名信号量
if((lock = sem_open(SEM_MUTEX,O_CREAT,FILE_MODE,1)) == SEM_FAILED)
{
perror("sem_open() error");
exit(-1);
}
if((notempty = sem_open(SEM_NEMPTY,O_CREAT,FILE_MODE,0)) == SEM_FAILED)
{
perror("sem_open() error");
exit(-1);
}
if((notfull = sem_open(SEM_NFULL,O_CREAT,FILE_MODE,BUFFER_SIZE)) == SEM_FAILED)
{
perror("sem_open() error");
exit(-1);
}
/* 创建生产者和消费者线程*/
pthread_create(&th_c, NULL, producer, 0);
pthread_create(&th_p, NULL, consumer, 0);
/* 等待两个线程结束*/
pthread_join(th_c, &retval);
pthread_join(th_p, &retval);
//关闭有名信号量
sem_close(lock);
sem_close(notempty);
sem_close(notfull);
//撤销有名信号量
sem_unlink(SEM_MUTEX);
sem_unlink(SEM_NEMPTY);
sem_unlink(SEM_NFULL);
void *producer(void *data)
{
int n;
for (n = 0; n <= PRO_NO; n++)
{
sem_wait(notfull);
sem_wait(lock);
/* 写数据,并移动指针 */
if (n < PRO_NO)
{
buffer.buf[buffer.writepos] = n;
printf("%d --->\n", n);
usleep(PSLEEP);
}
else
{
buffer.buf[buffer.writepos] = OVER;
printf("%d --->\n", OVER);
}
buffer.writepos++;
if (buffer.writepos >= BUFFER_SIZE)
buffer.writepos = 0;
/* 设置缓冲区非空的条件变量*/
sem_post(notempty);
sem_post(lock);
}
return NULL;
}
void *consumer(void *data)
{
int d;
while (1)
{
/* 等待缓冲区非空*/
sem_wait(notempty);
sem_wait(lock);
/* 读数据,移动读指针*/
d = buffer.buf[buffer.readpos];
//usleep(CSLEEP);
buffer.readpos++;
if (buffer.readpos >= BUFFER_SIZE)
buffer.readpos = 0;
printf("--->%d \n", d);
if (d == OVER)
break;
/* 设置缓冲区未满的条件变量*/
sem_post(notfull);
sem_post(lock);
}
return NULL;
}
四.实验结果与分析
1.无名信号量用于线程间的互斥
·gcc -o mythread_posix3_semu mythread_posix3_semu.c -lpthread //编译连接生成可执行程序
·./mythread_posix3_semu 100000000 //执行生成的可执行程序
由实验结果可以看出,当有线程在访问共享变量的时候其他的线程是无法进入临界区的只能被挂起在lock上,直到临界区内的线程退出后才能进行访问。由于使用了无名信号量对临界区的访问进行互斥锁的加锁和解锁操作,解决了线程间的互斥问题。所以进程执行完后counter的值为400000000.
2.无名信号量用于线程间的互斥
·gcc -o mythread_posix3_semn mythread_posix3_semn.c -lpthread //编译连接生成可执行程序
** ·./mythread_posix3_semn 100000000 //执行生成的可执行程序**
由实验结果可以看出,当有线程在访问共享变量的时候其他的线程是无法进入临界区的只能被挂起在lock上,直到临界区内的线程退出后才能进行访问。由于使用了有名信号量对临界区的访问进行互斥锁的加锁和解锁操作,解决了线程间的互斥问题。所以进程执行完后counter的值为400000000.
3.POSIX 信号量用于线程间的(狭义)同步
·无名信号量解决生产者-消费者问题
·gcc -o pro_csm_semu pro_csm_semu.c -lpthread //编译连接生成可执行程序
** ·./pro_csm_semu //执行生成的可执行程序 **
由实验结果可以看出如果消费者消费时buffer为空时消费者线程会挂起在notempty上,生产者生产时 buffer 满时生产者会挂起在notfull上。由结果可以看出当生产者开始生产时,对临界区加锁,生产完成后,set_post函数会唤醒阻塞的某个线程,并且释放锁,由于生产者生产了产品,post后消费者就可以消费了,中间消费者和生产则交替的执行,不论是生产者还是消费者访问在访问临界区时都需要加互斥锁,并且通过无名信号量来解决线程之间的同步问题,当个生产者生产达到设置的最大值时,生产者线程结束,然后消费者继续消费直到消费完 buffer。
·有名信号量解决生产者-消费者问题
· gcc -o pro_csm_namedsem pro_csm_namedsem.c -lpthread //编译连接生成可执行程序
· ./pro_csm_namedsem //执行生成的可执行程序
由实验结果可以看出如果消费者消费时buffer为空时消费者线程会挂起在notempty上,生产者生产时 buffer 满时生产者会挂起在notfull上。由结果可以看出当生产者开始生产时,对临界区加锁,生产完成后,set_post函数会唤醒阻塞的某个线程,并且释放锁,由于生产者生产了产品,post后消费者就可以消费了,中间消费者和生产则交替的执行,不论是生产者还是消费者访问在访问临界区时都需要加互斥锁,并且通过有名信号量来解决线程之间的同步问题,当个生产者生产达到设置的最大值时,生产者线程结束,然后消费者继续消费直到消费完 buffer。