用信號量實現進程互斥示例和解決哲學家就餐問題
一、我們在前面講進程間通信的時候提到過進程互斥的概念,下面寫個程序來模擬一下,程序流程如下圖:
即父進程打印字 符O,子進程打印字符X,每次打印一個字符後要sleep 一下,這裡要演示的效果是,在打印程序的邊界有PV操作,故每個進 程中間sleep 的時間即使時間片輪轉到另一進程,由於資源不可用也不會穿插輸出其他字符,也就是說O或者X字符都會是成 對出現的,如OOXXOOOOXXXXXXOO....
程序如下:
#include<sys/types.h> #include<unistd.h> #include<errno.h> #include<sys/ipc.h> #include<sys/sem.h> #include<sys/wait.h> #define ERR_EXIT(m) \ do { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0) union semun { int val; /* Value for SETVAL */ struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */ unsigned short *array; /* Array for GETALL, SETALL */ struct seminfo *__buf; /* Buffer for IPC_INFO (Linux-specific) */ }; int semid; /* pv操作之間的臨界區,導致打印的字符一定是成對出現的 */ void print(char op_char) { int pause_time; srand(getpid()); int i; for (i = 0; i < 10; i++) { sem_p(semid); printf("%c", op_char); fflush(stdout); pause_time = rand() % 3; sleep(pause_time); printf("%c", op_char); fflush(stdout); sem_v(semid); pause_time = rand() % 2; sleep(pause_time); } } int main(void) { semid = sem_create(IPC_PRIVATE); sem_setval(semid, 1); pid_t pid; pid = fork(); if (pid == -1) ERR_EXIT("fork"); if (pid > 0) { print('o'); wait(NULL); sem_d(semid); } else { print('x'); } return 0; }
sem_create 等函數參考工具集。在調用semget 時指定key = IPC_PRIVATE,表示創建的是私有的信號集,但具 有親緣關系的進程是可見的,比如父子進程。輸出如下:
simba@ubuntu:~/Documents/code/linux_programming/UNP/system_v$ ./print
ooxxooxxooxxooxxooooooxxooxxooxxooxxxxxx
可以看到輸出都是成對出現的字符。
分析一下:semval = 1,假設父進程先被調度執行,父進程先P了一下,此時 semval = 0,子進程在父進程睡眠時間被調度的時候嘗試P,semval = -1,然後子進程阻塞了,父進程打印完V了一下,semval = 0,喚醒子進程,子進程的P操作返回,打印字符睡眠後V了一 下,semval = 1。當然在子進程睡眠的時候父進程可能也在嘗試P,故就一直循環往復下去。
二、哲學家就餐問題的 描述可以參考這裡,下面我們嘗試解決這個問題的方法是:僅當一個哲學家兩邊筷子都可用時才允許他拿筷子。
上圖中 紅色數字表示哲學家的編號,總共5個哲學家,用5個進程來表示;黑色數字表示筷子的編號,總共有5根筷子,可以定義一 個信號集中含有5個信號量,每個信號量的初始值為1,當某個哲學家可以同時得到兩根筷子(同時P兩個信號量返回)時可 以用餐,否則阻塞等待中。用餐後需要同時V一下兩個信號量,讓其他進程可以P成功。
程序如下:
#include<stdio.h> #include<stdlib.h> #include<sys/ipc.h> #include<sys/msg.h> #include<sys/types.h> #include<unistd.h> #include<errno.h> #include<sys/ipc.h> #include<sys/sem.h> #include<sys/wait.h> #define ERR_EXIT(m) \ do { \ perror(m); \ exit(EXIT_FAILURE); \ } while(0) union semun { int val; /* Value for SETVAL */ struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */ unsigned short *array; /* Array for GETALL, SETALL */ struct seminfo *__buf; /* Buffer for IPC_INFO (Linux-specific) */ }; int semid; #define DELAY (rand() % 5 + 1) void wait_for_2fork(int no) { int left = no; int right = (no + 1) % 5; struct sembuf buf[2] = { {left, -1, 0}, {right, -1, 0} }; semop(semid, buf, 2); } void free_2fork(int no) { int left = no; int right = (no + 1) % 5; struct sembuf buf[2] = { {left, 1, 0}, {right, 1, 0} }; semop(semid, buf, 2); } void philosopere(int no) { srand(getpid()); for (; ;) { printf("%d is thinking\n", no); sleep(DELAY); printf("%d is hungry\n", no); wait_for_2fork(no); printf("%d is eating\n", no); sleep(DELAY); free_2fork(no); } } int main(void) { semid = semget(IPC_PRIVATE, 5, IPC_CREAT | 0666); if (semid == -1) ERR_EXIT("semget"); union semun su; su.val = 1; int i; for (i = 0; i < 5; i++) { semctl(semid, i, SETVAL, su); } int no = 0; pid_t pid; for (i = 1; i < 5; i++) { pid = fork(); if (pid == -1) ERR_EXIT("fork"); if (pid == 0) { no = i; break; } } philosopere(no); return 0; }
我們在前面說過,但需要對一個信號集中的多個信號量操作時,要麼全部執行,要麼全部不執行,即是一個原子 操作,某個進程需要等待兩根筷子,即對兩個信號量同時P成功才可以用餐,信號量的序號是0~4,可看作筷子的編號,此 時semop 函數操作的是2個信號量,即需定義2個struct sembuf 結構體成員的數組 struct sembuf buf[2];
simba@ubuntu:~/Documents/code/linux_programming/UNP/system_v$ ./dinning
0 is thinking
3 is thinking
2 is thinking
4 is thinking
1 is thinking
4 is hungry
4 is eating
0 is hungry
3 is hungry
1 is hungry
1 is eating
2 is hungry
3 is eating
4 is thinking
1 is thinking
0 is eating
4 is hungry
0 is thinking
1 is hungry
1 is eating
3 is thinking
4 is eating
0 is hungry
1 is thinking
2 is eating
0 is eating
4 is thinking
2 is thinking
1 is hungry
3 is hungry
3 is eating
0 is thinking
2 is hungry
1 is eating
4 is hungry
................
如果發現程序沒有運行卡著,即沒有發生死鎖現象,從中也可以發現同時最多只能 有兩個哲學家一起用餐,也不會出現相鄰哲學家一起用餐的情況。