1. 实验目的 本实验介绍在 Linux 中使用信号量进行进程同步、互斥的方法。读者可以通过实验进一步理解进程间同步与互斥、临界区与临界资源的概念与含义,并学会 Linux 信号量的基本使用方法。
2. 实验内容 2.1 参考
System V:Unix 众多版本中的一支,最初由 AT&T 定义,目前为第四个版本,其中定义了较为复杂的 API。
POSIX:Portable Operating System Interface,IEEE 为了统一 Unix 接口而定义的标准,定义了统一的 API 接口。Linux 既支持 System V API,又支持 POSIX API。
2.2 信号量 Linux 提供两种信号量:
内核信号量:用于内核中资源共享控制
用户态信号量:主要包括 POSIX 信号量和 SYSTEM V 信号量
其中 POSIX 信号量分为两类:
无名信号量:主要用于线程间同步,也可用于进程间(由 fork 产生)同步
有名信号量:既可用于进程间同步,也可用于线程间同步
POSIX 有名信号量主要包括:
sem_t* sem_open(const char *name, int oflag, mode_t mode, int value):
int sem_wait(sem_t *sem): 测试指定的信号量的值,相当于 P 操作。若 sem > 0,则减 1 立刻返回;若 sem = 0,则睡眠直到 sem > 0,此时立刻减 1,然后返回;
int sem_post(sem_t *sem): 释放资源,相当于 V 操作,信号量 sem 的值加 1,唤醒正在等待该信号量的进程/线程。
int sem_close(sem_t *sem): 关闭有名信号量。进程中,如果使用完信号量,应使用该函数关闭有名信号量。
int sem_unlink(const char *name): 删除系统中的信号量,如果有任何进程/线程引用这个信号量,sem_unlink() 函数不会起到任何作用,即只有最后一个使用该信号量的进程来执行 sem_unlink() 才有效。
2.3 通过实例查看不使用互斥时的情况 创建一个 C 程序,4-1.cpp,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main (int argc,char *argv[]) { char message = 'x' ; if (argc > 1 ) { message = argv[1 ][0 ]; } for (int i = 0 ; i < 10 ; i++) { printf ("%c" , message); fflush(stdout ); sleep(rand() % 3 ); printf ("%c" , message); fflush(stdout ); sleep(rand() % 2 ); } sleep(10 ); exit (0 ); }
编译链接为 a 文件,同时运行两个进程,命令如下:
1 2 gcc 4-1.cpp -o a ./a & ./a o
观察 x 和 o 的出现规律,并分析原因:
在未使用信号量控制互相排斥的程序中,x、o 的出现是无规律,两个进程只是并发进行,随机出现 x、o。
2.4 使用信号量来对临界资源进行互斥 创建一个 C 程序,4-2.cpp,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <stdio.h> #include <fcntl.h> #include <stdlib.h> #include <unistd.h> #include <sys/ipc.h> #include <sys/stat.h> #include <sys/types.h> #include <semaphore.h> int main (int argc, char *argv[]) { char message = 'x' ; if (argc > 1 ) { message = argv[1 ][0 ]; } sem_t *mutex = sem_open("my_sem" , O_CREAT, 0666 , 1 ); for (int i = 0 ; i < 10 ; i++) { sem_wait(mutex); printf ("%c" , message); fflush(stdout ); sleep(rand() % 3 ); printf ("%c" , message); fflush(stdout ); sem_post(mutex); sleep(rand()%2 ); } sleep(10 ); sem_close(mutex); sem_unlink("my_sem" ); exit (0 ); }
编译链接为 a 文件,同时运行两个进程,命令如下:
1 2 gcc 4-2.cpp -o a ./a & ./a o
观察 x 和 o 的出现规律,并分析原因:
x 和 o 呈现偶数个交替出现。该程序使用信号量 mutex 进行进程间的互斥操作。因为只有在当前进程执行完两次 printf(); 之后,才会释放信号量 mutex,所以只会输出偶数个 x 或者 o,交替输出。进程互斥,使用信号量之后,必然会有进程先访问 mutex,执行、释放之后才能再次执行下一个进程。
2.5 使用信号量来模拟下象棋红黑轮流走子的情况 编写两个 C 语言程序 red_chess.cpp 以及 black_chess.cpp,分别模拟下象棋过程中红方走子和黑方走子过程。走子规则:红先黑后,红、黑双方轮流走子,到第 10 步,红方胜,黑方输。
编程思路:
设置以下两个同步信号量:
black:初值为 1,代表黑方已经走子,轮到红方走子(满足棋规“红先黑后”)。
red:初值为 0,代表红方尚未走子。
红棋走子之前,先测试信号量 black,判断黑方是否已经走子。如是,则轮到红方走子,否则阻塞等待黑子走子,由于 black 的初值为 1。因此一定是红方先走。红方走子完毕后,置信号量 red = 1,通知黑方走子。
黑方走子之前,先测试信号量 red,判断红方是否已经走子,如是,则轮到黑方走子,否则阻塞等待红方走子,由于 red 的初值为 0,因此在红方没有走子之前,黑方不会走子。黑方走子完毕后,置信号量 black = 1,通知红方走子。
red_chess.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <stdio.h> #include <fcntl.h> #include <stdlib.h> #include <unistd.h> #include <sys/ipc.h> #include <sys/stat.h> #include <sys/types.h> #include <semaphore.h> int main () { sem_t *red = sem_open("chess_red_sem" , O_CREAT, 0666 , 0 ); sem_t *black = sem_open("chess_black_sem" , O_CREAT, 0666 , 1 ); for (int i = 0 ; i < 10 ; i++) { sem_wait(black); if (i != 9 ) { printf ("Red chess had moved, black chess go!\r\n" ); } else { printf ("Red chess win!\r\n" ); } fflush(stdout ); sem_post(red); } sleep(10 ); sem_close(red); sem_close(black); sem_unlink("chess_red_sem" ); sem_unlink("chess_black_sem" ); exit (0 ); }
black_chess.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <stdio.h> #include <fcntl.h> #include <stdlib.h> #include <unistd.h> #include <sys/ipc.h> #include <sys/stat.h> #include <sys/types.h> #include <semaphore.h> int main () { sem_t *red = sem_open("chess_red_sem" , O_CREAT, 0666 , 0 ); sem_t *black = sem_open("chess_black_sem" , O_CREAT, 0666 , 1 ); for (int i = 0 ; i < 10 ; i++) { sem_wait(red); if (i != 9 ) { printf ("Black chess had moved, red chess go!\r\n" ); } else { printf ("Black chess lost!\r\n" ); } fflush(stdout ); sem_post(black); } sleep(10 ); sem_close(red); sem_close(black); sem_unlink("chess_red_sem" ); sem_unlink("chess_black_sem" ); exit (0 ); }
编译链接为 red 和 black 文件,同时运行两个进程,命令如下:
1 2 3 gcc red_chess.cpp -o red gcc black_chess.cpp -o black ./red & ./black
3. 分析与思考 对程序结果进行分析,并结合操作系统课程中讲授的原理,总结信号量在进程同步和互斥中所起的作用。
对信号量的操作如果没有成对出现,会导致什么现象发生?
缺少 P 操作就不能访问临界资源,缺少 V 操作就不能释放临界资源,如果临界资源不够用,下一个等待临界资源的进程就一直得不到执行。
用于实现同步、互斥的信号量再出现的位置上各有什么特点?
同一进程的 P/V 操作应成对出现,实现互斥的信号量成对的 P 原语在 V 原语之前,实现同步的信号量成对的 P 原语在 V 原语之后。
如果改成“黑先红后”,红、黑双方轮流走子,到第 20 步,黑方胜,红方输,如何编程实现?
red_chess.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <stdio.h> #include <fcntl.h> #include <stdlib.h> #include <unistd.h> #include <sys/ipc.h> #include <sys/stat.h> #include <sys/types.h> #include <semaphore.h> int main () { sem_t *red = sem_open("chess_red_sem" , O_CREAT, 0666 , 1 ); sem_t *black = sem_open("chess_black_sem" , O_CREAT, 0666 , 0 ); for (int i = 0 ; i < 20 ; i++) { sem_wait(black); if (i != 19 ) { printf ("Red chess had moved, black chess go!\r\n" ); } else { printf ("Red chess lost!\r\n" ); } fflush(stdout ); sem_post(red); } sleep(20 ); sem_close(red); sem_close(black); sem_unlink("chess_red_sem" ); sem_unlink("chess_black_sem" ); exit (0 ); }
black_chess.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <stdio.h> #include <fcntl.h> #include <stdlib.h> #include <unistd.h> #include <sys/ipc.h> #include <sys/stat.h> #include <sys/types.h> #include <semaphore.h> int main () { sem_t *red = sem_open("chess_red_sem" , O_CREAT, 0666 , 1 ); sem_t *black = sem_open("chess_black_sem" , O_CREAT, 0666 , 0 ); for (int i = 0 ; i < 20 ; i++) { sem_wait(red); if (i != 19 ) { printf ("Black chess had moved, red chess go!\r\n" ); } else { printf ("Black chess win!\r\n" ); } fflush(stdout ); sem_post(black); } sleep(20 ); sem_close(red); sem_close(black); sem_unlink("chess_red_sem" ); sem_unlink("chess_black_sem" ); exit (0 ); }
编译链接为 red 和 black 文件,同时运行两个进程,命令如下:
1 2 3 gcc red_chess.cpp -o red gcc black_chess.cpp -o black ./red & ./black