信号量与 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 199int 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/)
- 即:所有访问信号量的信号量结束后,信号量才被删除