并发程序设计(实验1)
掌握在程序中创建新进程的方法, 观察并理解多道程序并发执行的现象。
void main (void){
int x=5;
if(fork()){
x += 30;
printf ("%d\n",x);
}
else
printf("%d\n",x);
printf("%d\n",x);
}
P1 | P2 | P3 | P4 | P5 | P6 |
---|---|---|---|---|---|
35 | 35 | 35 | 5 | 5 | 5 |
35 | 5 | 5 | 5 | 35 | 35 |
5 | 5 | 35 | 35 | 5 | 35 |
5 | 35 | 5 | 35 | 35 | 5 |
(猜想父子进程间的输出应该是乱序的)
// fork0.c
#include
#include
void main (void){
int x=5;
if(fork()){
x += 30;
printf ("%d\n",x);
}
else
printf("%d\n",x);
printf("%d\n",x);
}
#include
#include
#include
void main (){
int x=5;
/*
fork()
父进程返回新创建子进程的进程ID
子进程,返回0
出现错误返回一个负值
*/
pid_t pid;
if(fork()){ // 若未出现错误,父进程运行以下代码
pid = getpid();
printf("父进程编号:%d\n",pid);
x += 30;
printf ("进程%d运行 x=%d\n",pid,x);
}
else{ // 子进程运行以下代码
pid = getpid();
printf("子进程编号:%d\n",pid);
printf ("进程%d运行 x=%d\n",pid,x);
}
printf ("进程%d运行 x=%d\n",pid,x);
}
fork()函数用于创建一个新的进程(子进程),所创建的进程复制父进程的代码段、数据段、堆、栈等所有用户空间信息;在内核中操作系统重新为其申请了一个PCB,并使用父进程的PCB进行初始化;其中父进程返回新创建子进程的进程ID,子进程返回0,出现错误返回一个负值。
以上图运行结果为例:进程3378通过fork()函数创建了一个子进程3379,由于子进程得到父进程地址空间的一个复制,所以此时父、子进程中的x值均为5。父进程的fork函数返回值为3379(非零),执行if的内容;子进程fork函数返回值等于0,执行else的内容。fork函数后,父、子进程的地址空间位置已经不同了,执行后续的操作相互不影响,所以结果如上图所示。
但经过多次运行代码,分析发现:每次输出的结果均是 父进程结果在前,子进程结果在后,即
35
35
5
5
查找资料,究其原因可能在于:子进程创建需要时间,在这个空闲时间,父线程继续执行代码,子进程创建完成后显示。
于是,添加sleep函数,让父进程等待子进程创建完成后,再一起运行:
#include
#include
void main (void){
int x=5;
int t = fork();
sleep(1); // 添加sleep函数,父子进程将会乱序输出
if(t){
x += 30;
printf ("%d\n",x);
}
else
printf("%d\n",x);
printf("%d\n",x);
}
fork()函数 用于创建一个新的进程(子进程),所创建的进程复制父进程的代码段、数据段、堆、栈等所有用户空间信息;在内核中操作系统重新为其申请了一个PCB,并使用父进程的PCB进行初始化。(其中父进程返回新创建子进程的进程ID,子进程返回0,出现错误返回一个负值)
但子进程创建需要时间,在这个空闲时间,父线程继续执行代码,子进程创建完成后显示。父进程与子进程是并行的。创建于启动所需要的时间,如果主进程不加sleep函数,足以跑完主进程,直到一个临界点才会进入执行子进程,会影响程序的输出顺序,并不会影响最后的结果。
进程通信(实验2)
掌握用邮箱方式进行进程通信的方法,并通过设计实现简单邮箱理解进程通信中的同步问题以及解决该问题的方法。
进程通信:采用邮箱的方式进行进程间的通信
信号量主要用来控制多个进程对共享资源的访问。
(1)检测控制这个资源的信号量的值
(2)如果信号量值为正,即可使用该资源。进程将信号量的值减一,表示它正在使用该资源的某个小单元。
(3)如果信号量的值为零,那么该进程进入睡眠状态,直到信号量的值重新大于零被唤醒,转入第一步操作。
共享内存允许多个进程共享一个内存,实现交换信息的进程通信机制。共享内存需要一定的同步机制控制多个进程对同一块内存的读写。(信号量实现)
send() receive() 利用 P/ V 操作来实现控制多个进程对于共享内存的读写操作。
利用操作系统平台上用于进程通信的系统调用具体形式,实现邮箱通讯
利用消息通信函数 msgget、msgctl、msgsnd、msgrcv
- msgget(得到消息队列标识符或创建一个消息队列对象)
得到消息队列标识符或创建一个消息队列对象并返回消息队列标识符
- msgctl (获取和设置消息队列的属性)
获取和设置消息队列的属性
- msgsnd (将消息写入到消息队列) — send操作
将msgp消息写入到标识符为msqid的消息队列
- msgrcv (从消息队列读取消息) — recevice操作
从标识符为msqid的消息队列读取消息并存于msgp中,读取后把此消息从消息队列中删除
// 定义消息结构
typedef struct msgbuf{
long mtype;
char mtxt[100];
}msg;
// code A.c
#include
#include
#include
#include
#include
#include
#define MSGKEY1 100
#define MSGKEY2 200
// 定义消息结构
typedef struct msgbuf{
long mtype;
char mtxt[100];
}msg;
int main(){
msg amsg;
int pid,*pint,msgqid1,msgqid2;
int cmd,msglen,msgtype;
//---------------------get-------------------------//
msgqid1 = msgget(MSGKEY1,IPC_CREAT|0666);
if(msgqid1 == -1){ // 出错:-1
printf("error\n");
return 0;
}
msgqid2 = msgget(MSGKEY2,IPC_CREAT|0666);
if(msgqid2 == -1){
printf("error\n");
return 0;
}
//-----------------消息处理----------------------//
while(1){
printf("请选择 1-发送消息 2-接收消息 3-EXIT\n");
scanf("%d",&cmd);
if(cmd == 1){ // 发送消息
printf("请输入消息类型(type):\n");
scanf("%ld",&amsg.mtype);
getchar();
printf("请输入要发送的消息:\n");
scanf("%s",amsg.mtxt);
msglen = strlen(amsg.mtxt);
msgsnd(msgqid1,&amsg,msglen,0);
}
if(cmd == 2){ // 接收消息
memset(&amsg,0,sizeof(msg));
printf("请输入消息类型(type):\n");
scanf("%d",&msgtype);
if(msgrcv(msgqid2,&amsg,100,msgtype,IPC_NOWAIT)==-1){
//不超过100字节
printf("读取消息失败!!!\n");
}
else{
printf("接收到 B 的一条消息:\n %s\n",amsg.mtxt);
}
}
if(cmd == 3){
msgctl(msgqid1,IPC_RMID,0);
msgctl(msgqid2,IPC_RMID,0);
break;
}
}
return 0;
}
// code B.c
#include
#include
#include
#include
#include
#include
#define MSGKEY1 100
#define MSGKEY2 200
// 定义消息结构
typedef struct msgbuf{
long mtype;
char mtxt[100]; //max 100
}msg;
int main(){
msg bmsg;
int pid,*pint,msgqid1,msgqid2;
int cmd,msglen,msgtype;
//-------------------get-----------------------//
msgqid1 = msgget(MSGKEY1,IPC_CREAT|0666);
if(msgqid1 == -1){
printf("error\n");
return 0;
}
msgqid2 = msgget(MSGKEY2,IPC_CREAT|0666);
if(msgqid2 == -1){
printf("error\n");
return 0;
}
//-----------------消息处理--------------------//
while(1){
printf("请选择 1-发送消息 2-接收消息 3-EXIT\n");
scanf("%d",&cmd);
if(cmd == 1){ // 发送消息
printf("请输入消息类型(type):\n");
scanf("%ld",&bmsg.mtype);
getchar();
printf("请输入要发送的消息:\n");
scanf("%s",bmsg.mtxt);
msglen = strlen(bmsg.mtxt);
msgsnd(msgqid2,&bmsg,msglen,0);
}
if(cmd == 2){ // 接收消息
memset(&bmsg,0,sizeof(msg));
printf("请输入消息类型(type):\n");
scanf("%d",&msgtype);
if(msgrcv(msgqid1,&bmsg,100,msgtype,IPC_NOWAIT)==-1){
printf("读取消息失败!!!\n");
}
else{
printf("接收到 A 的一条消息:\n %s\n",bmsg.mtxt);
}
}
if(cmd == 3){ // 退出·
msgctl(msgqid1,IPC_RMID,0);
msgctl(msgqid2,IPC_RMID,0);
break;
}
}
return 0;
}
自定义信号量调用结构以及信号量操作函数实现邮箱通讯
共享内存函数 shmget()、shmat()、shmdt()、shmctl()。
共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。
共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取。 所以我们可以通过设置 P/V操作 来实现对共享内存的访问。
- int shmget(key_t key, size_t size, int shmflg);
用来创建共享内存
- void *shmat(int shm_id, const void *shm_addr, int shmflg);
用来启动对共享内存的访问,并把共享内存连接到当前进程的地址空间。
- int shmdt(const void *shmaddr);
用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。
- int shmctl(int shm_id, int command, struct shmid_ds *buf);
与信号量的semctl()函数一样,用来控制共享内存
- int semget(key_t key, int nsems, int flag)
创建或打开信号量集。key 相当于是一个信号量集的标识符,nsems 是信号量集中创建的信号量的数量, flag设置为 IPC_CREAT, 执行创建或者打开的操作。调用成功返回信号量集合标识符,否则返回-1。
- int semop(int semid, struct sembuf *sops, size_t nops)
信号量的操作,semid 为信号集的标识符;sops 为 sembuf 结构的数组,nops为数组中元素的个数。
sembuf 结构中记录了对信号集的一个操作。 P/V 原语的操作主要是用到这个函数。
- int semctl(int semid, int semnum, int cmd, [union semnun arg]);
信号量的控制,semid 为信号集的标记;semnum 信号集中某个信号的编号; cmd 给出操作命令,可取值如 IPC_RMID (删除指定的信号量集合)
// 信号量 以及 P/V原语的封装
// sem.c
union semun{ // 信号量固定结构
int val;
struct semid_ds *buf;
unsigned short *array;
};
void init(int sid,int semnum,int initval){
union semun semopts;
semopts.val = initval;
semctl(sid,semnum,SETVAL,semopts); // 信号量操作函数
}
int semaphore_p(int sem_id){ // p操作封装
struct sembuf sb;
sb.sem_num = 0;
sb.sem_op = -1; // 减一
sb.sem_flg = SEM_UNDO;
if(semop(sem_id,&sb,1)==-1){
printf("P操作运行失败!\n");
return 0;
}
return 1;
}
int semaphore_v(int sem_id){ // v操作封装
struct sembuf sb;
sb.sem_num = 0;
sb.sem_op = 1; // 加一
sb.sem_flg = SEM_UNDO;
if(semop(sem_id,&sb,1)==-1){
printf("V操作运行失败!\n");
return 0;
}
return 1;
}
// code read.c
// P/V原语
int main(){
char *shm;
int shmid; // 共享内存表示符
int producer,consumer;
//----------------------创建共享内存-------------------------//
if((consumer = semget((key_t)1234, 1, IPC_CREAT|0660)) == 1){
printf("创建失败!\n");
exit(1);
}
init(consumer,0,1); // 初始化信号量
if((producer = semget((key_t)2345, 1, IPC_CREAT|0660)) == -1){
printf("创建失败!\n");
exit(1);
}
init(producer,0,0);
if((shmid=shmget(1, shmsz, 0660|IPC_CREAT)) == -1){ // 创建or获取共享内存id
printf("创建失败!\n");
exit(1);
}
//-----------将共享内存连接到当前进程的地址空间--------------//
shm = (char *)shmat(shmid,0,0);
if(shm == (void*)-1){
printf("shmat 失败!");
exit(1);
}
//-------------------读取共享内存中的数据------------------//
while(1){
printf("请选择 1-读取消息 0-EXIT\n");
int cmd;
scanf("%d",&cmd);
getchar();
if(cmd == 1){
semaphore_p(producer); // p操作,让其他进程无法操作给内存
printf("消息接收: %s",shm);
semaphore_v(consumer); // v操作,释放给内存操作权限
}
else if(cmd == 0) break;
}
semctl(producer, 0, IPC_RMID, 0);
semctl(consumer, 0, IPC_RMID, 0);
}
// code write.c
// P/V原语
int main(){
char *shm, *s;
int shmid;
int producer,consumer;
char readbuf[shmsz]; // 用于保存输入文本
if((consumer = semget((key_t)1234, 1, IPC_CREAT|0660)) == 1){ // 创建或者获取信号量id
printf("semget创建失败!\n");
exit(1);
}
init(consumer,0,1); // 初始化信号量
if((producer = semget((key_t)2345, 1, IPC_CREAT|0660)) == -1){
printf("semget创建失败!\n");
exit(1);
}
init(producer,0,0);
//--------------------创建共享内存-----------------------//
if((shmid=shmget(1, shmsz, 0660|IPC_CREAT)) == -1){ // 创建or获取共享内存id
printf("shmget失败!\n");
exit(1);
}
//--------------将共享内存映射到自己的进程空间--------//
shm = (char *)shmat(shmid,0,0);
//---------------向共享内存中写数据-----------------//
for(int i=0; ;i++){
printf("请选择 1-发送消息 2-删除消息 0-EXIT\n");
int cmd;
scanf("%d",&cmd);
getchar();
if(cmd == 1){
printf("请输入要发送的消息:");
fgets(readbuf,shmsz,stdin);
semaphore_p(consumer);
sprintf(shm,"消息 %d为 %s",i,readbuf);
semaphore_v(producer);
}
else if(cmd == 2){ // 删除消息
semaphore_p(consumer); // p操作,让其他进程无法操作给内存
sprintf(shm,"无消息!!\n");
semaphore_v(producer); // v操作,释放给内存操作权限
}
else if(cmd == 0) break; // 退出
}
}
利用 semget()、semop()、semctl()函数,实现信号量控制,能够设置 P/V操作来实现对共享内存的访问。即可实现:
信号量S1=1,S2=0
Send操作:
A: code1
P(S1)
发送/删除消息
V(S2)
goto A
Receive操作:
B: code2
P(S2)
读取数据
V(S1)
goto B
eg. 只写了一次数据,但进行连续两次读操作时,由于P/V操作对于信号量的控制,此时的读进程将会被挂起,直到下一次写操作的执行。
利用系统调用的消息队列(即消息通信函数 msgget、msgctl、msgsnd、msgrcv)实现邮箱通讯,内部的实现由系统封装完成。我们只需调用相应的函数即可。
自定义信号量调用结构以及信号量操作函数实现邮箱通讯(即利用共享内存函数 shmget、shmat、shmdt、shmctl,以及共享内存函数semget、semop、semctl),需要我们自己利用信号量操作来实现P/V原语,完成对共享内存的访问。读写操作要利用P/V原语控制,保证不会出现同时对一个内存的操作,防止出现错误。