信号量与 PV 操作
计算机中信号量的本质是整数,数值表示可用的资源数量
P 操作 (Passeren => 通过, 原子操作)
- 若信号量 == 0,当前任务阻塞 (进入信号量等待队列)
- 若信号量 > 0,则:将信号量数值减一,当前任务继续执行
V 操作 (Vrijgeven => 释放, 原子操作)
- 将信号量数值加一
- 若 信号量 > 0,则:唤醒阻塞的其它任务,当前任务继续执行
信号量与 PV 操作注意事项
程序中的 PV 操作必须成对出现 (P 操作 => 临界区 => V 操作)
信号量初始值一般为 1 (初始值与相应资源数量相关)
信号量也看做特殊的互斥量 (信号量初始值为 1 时,退化为互斥量)
若 信号量 == S && S > 0,则:可进行 P 操作并且不阻塞的次数为 S
信号量模拟用法示例
Linux 中的信号量
下面的程序输出什么?为什么?
test1.c
#define _GNU_SOURCE /* To get pthread_getattr_np() declaration */
#define _XOPEN_SOURCE >= 500 || _POSIX_C_SOURCE >= 200809L
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <memory.h>
#include <semaphore.h>
void* customer_thread(void* arg)
{
printf("thread begin %ld\n", pthread_self());
sem_wait(arg);
printf("thread end %ld\n", pthread_self());
return NULL;
}
int main()
{
pthread_t t = {0};
sem_t sem = {0};
int i = 0;
int v = 0;
sem_init (&sem, PTHREAD_PROCESS_PRIVATE, 1);
sem_getvalue(&sem, &v);
printf("sem = %d\n", v);
for(i=0; i<5; i++)
{
pthread_create(&t, NULL, customer_thread, &sem);
}
sleep(5);
sem_getvalue(&sem, &v);
printf("sem = %d\n", v);
printf("End!\n");
return 0;
}
第 28 行,初始化信号量,将信号量的初始值设置为 1,那么这里的信号量等同与互斥锁
第 36 行,主线程创建了 5 个子线程,子线程通过 sem_wait() 去获取信号量
第 41 行,主线程通过 sem_getvalue() 来获取当前信号量的值
由于信号量的初始值为 1,所以只能有一个子线程获取到信号量,其它的线程来获取信号量时,发现信号量的值为 0,就会阻塞等待信号量被释放
程序运行结果如下图所示:
只有一个子线程获取到了信号量,最后信号量的值为 0
生产消费者问题示例
信号量初体验
我们使用信号量来解决生产者消费者问题
test3.c
#define _GNU_SOURCE /* To get pthread_getattr_np() declaration */
#define _XOPEN_SOURCE >= 500 || _POSIX_C_SOURCE >= 200809L
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <memory.h>
#include <semaphore.h>
typedef struct
{
sem_t r;
sem_t w;
} Sem;
void* customer_thread(void* arg)
{
Sem* s = arg;
sleep(1);
while( 1 )
{
sem_wait(&s->r);
printf("%s : get\n", __FUNCTION__);
sem_post(&s->w);
}
return NULL;
}
int main()
{
pthread_t t = {0};
Sem sem = {0};
sem_init (&sem.r, PTHREAD_PROCESS_PRIVATE, 0);
sem_init (&sem.w, PTHREAD_PROCESS_PRIVATE, 1);
pthread_create(&t, NULL, customer_thread, &sem);
printf("Hello World!\n");
while( 1 )
{
sem_wait(&sem.w);
printf("%s : set\n", __FUNCTION__);
sem_post(&sem.r);
sleep(3);
}
printf("End!\n");
return 0;
}
该程序中使用的两个信号量,信号量 w 和信号量 r,信号量 w 的初始值为 1,信号量 r 的初始值为 0
只有主线程写完以后,信号量 r 的值会加一,子线程才能去读;子线程读完以后,信号量 w 的值会加一,主线程才能去写;会一直重复这个流程
程序运行结果如下图所示:
思考
进程之间是否需要进行同步与互斥?
多进程场景
多个进程共享一段内存,即:读写共享内存
此时共享内存的访问就是临界区访问
因此,需要对临界区进行保护 (防止多个进程同时写操作)
问题:是否存在跨进程使用的互斥量?
多进程内存共享
Linux 中的跨进程信号量
多进程内存共享
多进程与信号量
test5.c
#define _GNU_SOURCE /* To get pthread_getattr_np() declaration */
#define _XOPEN_SOURCE >= 500 || _POSIX_C_SOURCE >= 200809L
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <memory.h>
#include <fcntl.h>
#include <semaphore.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>
#define SEM_NAME "delphi_tang"
#define PATH_NAME "/home/book/Documents"
#define PROJ_ID 199
int get_shared_memory(key_t k)
{
int ret = shmget(k, 0, 0);
if( ret == -1 )
{
ret = shmget(k,
128,
IPC_CREAT | IPC_EXCL | S_IRWXU);
}
return ret;
}
sem_t* get_sem(int v)
{
sem_t* ret = sem_open(SEM_NAME, 0);
if( ret == SEM_FAILED )
{
ret = sem_open(SEM_NAME,
O_CREAT | O_EXCL,
S_IRWXU,
v);
}
return ret;
}
int main(int argc, char* argv[])
{
key_t k = ftok(PATH_NAME, PROJ_ID);
char* shmaddr = NULL;
sem_t* sem = NULL;
int shmid = get_shared_memory(k);
printf("shmid = %d\n", shmid);
if( shmid == -1 )
{
printf("shmget error\n");
exit(1);
}
sem = get_sem(1);
if( sem )
{
printf("sem is %p\n", sem);
}
shmaddr = shmat(shmid, NULL, 0);
while( (argc > 1) && shmaddr )
{
static int i = 0;
if( strcmp(argv[1], "write") == 0 )
{
sem_wait(sem);
sprintf(shmaddr, "shared string %d", i++);
printf("write: %s\n", shmaddr);
sem_post(sem);
usleep(1000 * 1000);
}
else if( strcmp(argv[1], "read") == 0 )
{
sem_wait(sem);
printf("read: %s\n", shmaddr);
sem_post(sem);
usleep(250 * 1000);
}
else
{
break;
}
}
printf("Press any key to finish process...\n");
system("read -s -n 1");
shmctl(shmid, IPC_RMID, NULL);
sem_close(sem);
return 0;
}
第 50 行,使用 ftok() 函数用于生成System V IPC(Inter-Process Communication,进程间通信)对象(如信号量、消息队列和共享内存)的键(key)
第 53 行,get_shared_memory() 函数调用了 shmget() 函数,shmget() 是一个Linux系统调用函数,用于创建一个新的共享内存段(segment)或获取一个已存在的共享内存段。这个函数会返回一个整数类型的共享内存标识符(ID),用于在后续的系统调用中引用共享内存段
第 63 行,get_sem() 函数调用了 sem_open() 函数,sem_open() 用于打开或创建一个命名信号量
第 70 行,shmat() 是一个Linux系统调用函数,用于将一个共享内存段附加到当前进程的地址空间。这使得进程可以通过指针访问共享内存段中的数据。shmat() 函数通常在调用 shmget() 函数后使用,以便将获得的共享内存段附加到进程的地址空间
第 72 行 - 95 行,通过信号量来访问多进程中的共享内存
程序运行结果如下图所示:
运行了 2 个进程,一个进程写共享内存,一个进程读取共享内存,信号量用于同步多进程间共享内存的读写
跨进程的信号量是通过文件的方式实现的,创建出的跨进程信号量的文件位置存放在 /dev/shm/目录下面
Linux 信号量的注意事项
信号量之间不能相互初始化,也不能相互赋值 (行为未定义)
跨进程信号量通过文件的方式实现,因此涉及读写权限
sem_close() 仅仅关闭信号量,信号量未删除
sem_unlink() 延迟删除信号量 (/dev/shm/)
- 即:所有访问信号量的信号量结束后,信号量才被删除