本文章转载地址
实验4 进程之间的通信
实验性质:验证性
实验学时:4学时
一、实验目的
1.掌握管道、信号、共享内存、消息队列等进程间通信机制;
2.能够利进程间通信机制求解一些常见问题。
二、实验预备知识
1.阅读并掌握C语言基本语法,操作。
2.熟悉Linux常用命令及使用方法。
3.实验内容
实验4 进程之间的通信
实验性质:验证性
实验学时:4学时
一、实验目的
1.掌握管道、信号、共享内存、消息队列等进程间通信机制;
2.能够利进程间通信机制求解一些常见问题。
二、实验预备知识
1.阅读并掌握C语言基本语法,操作。
2.熟悉Linux常用命令及使用方法。
3.实验内容
1. 编写程序实现以下功能:
利用匿名管道实现父子进程间通信,要求
父进程发送字符串“hello child”给子进程;
子进程收到父进程发送的数据后,给父进程回复“hello farther”;
父子进程通信完毕,父进程依次打印子进程的退出状态以及子进程的pid。
源代码:
#include
#include
#include
int main(int argc,char *argv[])
{
int fd1[2],fd2[2];
pipe(fd1);
pipe(fd2);
int pid=fork();
if(pid>0)
{
close (fd1[0]);
close (fd2[1]);
char str[25];
write(fd1[1],"hello child!\n",25);
read(fd2[0],str,25);
printf("%s",str);
int status;
int i=wait(&status);
printf("pid:%d\t%d\n",i,WIFEXITED(status));
exit(0);
}
else if(pid==0)
{
close (fd1[1]);
close (fd2[0]);
char str[25];
read(fd1[0],str,25);
printf("%s",str);
write(fd2[1],"hello father\n",25);
}
return 0;
}
匿名管道:是指用于连接一个读进程和一个写进程,以实现它们之间通信的共享文件,又称pipe文件,是一个只存在于内存的特殊文件。
匿名管道特点:
匿名管道是半双工的,数据只能向一个方向流动;
一个进程将数据写入管道,另一进程从管道中读取数据;
写入的内容添加在管道缓冲区的末尾,每次都是从缓冲区头部读出数据;
数据读出后将从管道中移走,其它读进程都不能再读到这些数据。
双向通信的建立
需要建立起两个管道
使用限制
只能用于具有亲缘关系的进程之间
如父子进程或兄弟进程之间
2. 编写程序实现以下功能:
利用匿名管道实现兄弟进程间通信,要求
兄进程发送字符串“This is elder brother ,pid is (兄进程进程号)”给第进程;
第进程收到兄进程发送的数据后,给兄进程回复“This is younger brother ,pid is(第进程进程号)”;
实验步骤:
父进程先创建一个子进程(这个就是兄进程) 然后通过判断进入父进程中,然后再次创建一个进程 这个进程就是 弟进程 父进程先创建一个子进程(这个就是兄进程) 然后通过判断进入父进程中,然后再次创建一个进程 这个进程就是 弟进程
或者
我们都知道利用fork函数能创建一个子进程,但是如何利用fork函数创建兄弟进程呢?
我们可以利用fork函数先建立一个子进程,在子进程中,将要发送的信息写入管道,然后再在父进程中再次调用fork函数,那么父进程里创建的子进程就是先前创建的进程的弟进程。我们可以让子进程将自己的父进程的pid打印,验证两进程是否是兄弟进程。
程序如下:
#include
#include
#include
#include
#include
int main()
{
int fd1[2],fd2[2];
pipe(fd1);
pipe(fd2);
int pid;
pid = fork();
if(pid == 0)
{
printf("This is the elder brother!\n");
printf("The elder's father's pid is: %d\n",getppid());
close(fd1[1]);
close(fd2[0]);
char str1[64],str2[64];
sprintf(str1,"This is the elder brother,pid is %d",getpid());
if(write(fd2[1],str1,64) < 0)
perror("write");
if(read(fd1[0],str2,64) < 0)
perror("read");
else
printf("The news from younger is: %s\n",str2);
}
else
{
if(fork() == 0)
{
printf("This is the younger brother!\n");
printf("The younger's father's pid is: %d\n",getppid());
close(fd1[0]);
close(fd2[1]);
char buf1[64],buf2[64];
if(read(fd2[0],buf1,64) > 0)
{
printf("The news form elder is: %s\n",buf1);
sprintf(buf2,"This is the younger brother,pid is %d",getpid());
if(write(fd1[1],buf2,64) < 0)
perror("write");
}
else
perror("read");
}
}
}
通过实验结果我们可以发现,两个进程的父进程并不相同。但是通过在ubuntu的环境下,测试成功。
实验步骤:
有名管道
匿名管道缺点:没有名字,只能用于具有亲缘关系的进程间通信
FIFO(有名管道)
严格遵循先进先出的读写规则
有名字,FIFO的名字包含在系统的目录树结构中,支持无亲缘关系的进程按名字访问,类似匿名管道,在文件系统中不存在数据块,而是与一块内核缓冲区相关联
read和write操作也由pipe_read()和pipe_write() 实现
与匿名管道主要区别:
FIFO索引节点出现在系统目录树上而不是pipefs特殊文件系统中
FIFO是一种双向通信管道,可以以读/写模式打开一个FIFO
1.有名管道的建立
基本函数
int mkfifo(const char * pathname, mode_t mode);
参数说明
pathname:创建的FIFO名字
mode:规定FIFO的读写权限
返回值
成功时返回0
失败时返回-1
若路径名存在,则返回EEXIST错误
说明
一般文件的I/O函数都可用于管道,如open(), close(), read(), write()等。
2.打开规则
为读操作而打开FIFO文件
若已有进程为写而打开该FIFO,则当前打开操作将成功返回
否则,可能阻塞直到有相应进程为写而打开该FIFO(当前打开操作未设置O_NONBLOCK标志)
或立即返回(当前打开操作设置O_NONBLOCK标志)
为写操作而打开FIFO文件
如果已经有进程为读而打开该FIFO,则当前打开操作将成功返回
否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作未设置O_NONBLOCK标志)
或者,返回ENXIO错误(当前打开操作设置O_NONBLOCK标志)
创建w.c
创建r.c
实验结果:
运行w.c ,w.c处于阻塞状态,再运行r.c 输出结果 ,同时管道ttfifo也存在。
或者
#include
#include
#include
#include
#include
int main()
{
int pid,fd;
if(mkfifo("fifotest",0666) < 0)
perror("mkfifo");
pid = fork();
if(pid < 0)
perror("fork");
else if(pid == 0)
{
printf("This is the write process!\n");
int fd = open("fifotest",0666);
for(int i = 0; i < 10;i++)
{
if(write(fd,"hello world",12) < 0)
perror("write");
sleep(1);
}
close(fd);
}
else
{
char str[128];
printf("This is the read process!\n");
int fd1 = open("fifotest",0666);
for(int i = 0;i < 10;i++)
{
if(read(fd1,str,128) < 0)
perror("read");
else
printf("%s\n",str);
}
system("rm -f fifotest");
}
}
实验步骤:
这个实验分成两个小部分。要把这两个小程序分开执行,在执行这两个程序之前先在终端上进入root用户,打开两个终端,分别执行这两个程序
创建send.c
实验结果:
运行recv.c 发现进程阻塞 ,再打开一个终端,运行send.c,输入进程号,发现两个程序都输出。
或者
信号是比较复杂的通信方式,用于通知进程中某种事件的发生。除了进程间的通信之外,进程还能发送信号给进程本身;每种信号类型都有对应信号处理程序。大多数的信号的系统默认操作是结束进程,当然,进程同样可以向系统请求采取某些代替的操作。
例如:忽略信号、恢复信号的默认操作和执行一个预先设定的信号处理函数。
信号的本质是在软件层次上对进程的中断机制的一种模拟。在原理上,一个进程收到某种信号和处理器收到中断请求是一样的。
信号是所有的进程间的通信机制中唯一一个异步通信机制,可以看作是异步通知。
信号的生命周期如下:
接下来我们通过signal和kill两种方式分别实现进程间的通信。
signal.c
#include
#include
#include
#include
#include
void fun(int sig)
{
if(sig = SIGUSR1)
printf("Received SIGUSR1!\n");
}
int main()
{
printf("This is A process,mypid is: %d\n",getpid());
signal(SIGUSR1,fun);
pause();
return 0;
}
此运行程序的方式和以往有些许不同,运行结果如下:
因为程序中有pause()语句,那么程序运行到此就会停下知道有信号发送给此进程。
然后新建一个终端,在终端输入kill -SIGUSR 11344,那么第二个进程就会发送SIGUSR1信号给pid为11344的进程,也就是进程A。之后程序输出字符串,进程结束。
接下来利用kill函数完成实验。
kill.c
#include
#include
#include
#include
#include
void fun(int sig)
{
if(sig == SIGUSR1)
printf("Reseived SIGUSR1!\n");
}
int main()
{
int pid;
if(signal(SIGUSR1,fun) < 0)
perror("signal");
pid = fork();
if(pid < 0)
perror("fork");
else if(pid == 0)
{
printf("This is B process!\n");
sleep(2);
}
else
{
printf("This is A process!\n");
if(kill(pid,SIGUSR1) < 0)
perror("kill");
return 0;
}
}
实验步骤:
使用setitimer函数,定时,发送信号(ITIMER_REAL: 以系统真实的时间来计算,它送出SIGALRM信号。ITIMER_VIRTUAL: -以该进程在用户态下花费的时间来计算,它送出SIGVTALRM信号。ITIMER_PROF: 以该进程在用户态下和内核态下所费的时间来计算,它送出SIGPROF信号。),然后使用signal函数来接收信号,运行创建好的那个判断函数,来判断是哪一个信号并输出,然后使用while(1)来使程序阻塞,使该程序可以一直发送信号。
实验结果:
或者
setitimer函数的作用是提供精确的定时功能。通过改变settitime函数的第一个参数就能够改变函数触发的信号。
程序如下:
#include
#include
#include
#include
#include
#include
void fun(int sig)
{
if(sig == SIGALRM)
printf("Received the SIGALRM!\n");
else if(sig == SIGVTALRM)
printf("Receive the SIGVTALRM!\n");
else if(sig == SIGPROF)
printf("Receive the SIGPROf!\n");
}
int main()
{
if(signal(SIGALRM,fun) < 0)
perror("signal");
if(signal(SIGVTALRM,fun) < 0)
perror("signal");
if(signal(SIGPROF,fun) < 0)
perror("signal");
struct itimerval new_value1,new_value2,new_value3;
new_value1.it_value.tv_sec = 1;
new_value1.it_value.tv_usec = 0;
new_value1.it_interval.tv_sec = 2;
new_value1.it_interval.tv_usec = 0;
setitimer(ITIMER_REAL,&new_value1,NULL);
new_value2.it_value.tv_sec = 1;
new_value2.it_value.tv_usec = 0;
new_value2.it_interval.tv_sec = 2;
new_value2.it_interval.tv_usec = 0;
setitimer(ITIMER_VIRTUAL,&new_value2,NULL);
new_value3.it_value.tv_sec = 1;
new_value3.it_value.tv_usec = 0;
new_value3.it_interval.tv_sec = 2;
new_value3.it_interval.tv_usec = 0;
setitimer(ITIMER_PROF,&new_value3,NULL);
while(1);
return 0;
}
实验步骤:
这个实验分成两个小部分。要把这两个小程序分开执行,在执行这两个程序之前先在终端上进入root用户,打开两个终端,分别执行这两个程序
实验结果:
先运行recv1.c 进程被阻塞,再打开一个终端,运行send1.c 输出进程号,在recv1.c中输入进程号,两个程序同时输出。
或者
sigaction和signal两个函数都是信号安装函数,但是他们两个之间还是有一定的区别的。
signal不能传递除信号之外的信息,而sigaction能够传递额外的信息;同时,sigaction能够设置进程的掩码,并且能够阻塞进程。
sigqueue函数只要针对实时信号,并且支持传递的信号附带参数,常常和sigaction函数配合使用。
sigaction.c
#include
#include
#include
#include
#include
void fun(int sig)
{
if(sig == SIGUSR1)
printf("Received SIGUSR1!\n");
}
int main()
{
printf("This is the receive process!\n");
printf("The process pid is: %d\n",getpid());
struct sigaction act,oldact;
act.sa_handler = fun;
act.sa_flags = 0;
sigaction(SIGUSR1,&act,&oldact);
pause();
return 0;
}
igaction.c的操作方式与之前相似。
程序运行结果:
sigqueue.c
#include
#include
#include
#include
#include
void handler(int sig,siginfo_t* p,void* q)
{
if(sig == SIGUSR1)
printf("Received SIGUSR1!\n");
}
int main()
{
union sigval mysigval;
struct sigaction act;
int pid;
pid = fork();
if(pid < 0)
perror("fork");
else if(pid == 0)
{
printf("This is the received process!\n");
act.sa_sigaction = handler;
act.sa_flags = SA_SIGINFO;
if(sigaction(SIGUSR1,&act,NULL) < 0)
perror("sigaction");
while(1);
}
else
{
printf("This is the send process!\n");
sleep(1);
if(sigqueue(pid,SIGUSR1,mysigval) < 0)
perror("sigqueue");
}
return 0;
}
程序如下:
#include
#include
#include
#include
void handler(int sig,siginfo_t* info,void *p)
{
printf("The str is: %s\n",info->si_value.sival_ptr);
}
int main()
{
int pid;
struct sigaction act;
act.sa_sigaction = handler;
act.sa_flags = SA_SIGINFO;
pid = fork();
if(pid < 0)
perror("fork");
else if(pid == 0)
{
printf("This is the receive process!\n");
if(sigaction(SIGUSR1,&act,NULL) < 0)
perror("sigaction");
while(1);
}
else
{
printf("This is the send process!\n");
union sigval mysigval;
mysigval.sival_ptr = "hello world";
sleep(1);
if(sigqueue(pid,SIGUSR1,mysigval) < 0)
perror("sigqueue");
}
return 0;
}
共享内存就是多个进程同时访问一个逻辑内存区域,共享内存是两个不相关的进程传递数据的重要方式。进程将同一段物理内存连接到他们自己的地址空间之后,所有连接的进程都能访问这块内存。如果一个进程对这段内存进行更改,所做的更改将影响更改之后访问这段内存的进程。需要注意的是,共享内存并没有设置同步机制,也就是说,在上一个进程对内存进行更改操作完成之后 ,并没有机制阻止下一个进程对这段内存的更改。因此,我们需要利用其它的机制对共享内存来同步进程对共享内存的访问。例如:信号量。
因为是直接对内存进行操作,省去了数据传输这一步骤,因此共享内存的速度最快。
程序如下:
shmread.c
#include
#include
#include
#include
#define MAXSIZE 1024
struct shm{
int write; //记录读进程是否已经将内容读取
char buffer[MAXSIZE];
};
int main()
{
int shmid;
struct shm *share;
void *shmptr = NULL;
if(shmid = shmget(0X44,MAXSIZE,0666|IPC_CREAT) < 0)
perror("shmget");
if((shmptr = shmat(shmid,0,0)) == (void *)-1)
perror("shmat");
printf("This is the read process!!!\n");
share = (struct shm *)shmptr;
while(1)
{
if(share->write != 0)
{
if(!strncmp(share->buffer,"end",3) == 0)
{
printf("%s",share->buffer);
share->write = 0;
}
else
break;
}
}
if(shmdt(shmptr) < 0)
perror("shmdt");
exit(0);
}
shmwrite.c
#include
#include
#include
#include
#define MAXSIZE 1024
struct shm{
int write; //记录读进程是否已经将内容读取
char buffer[MAXSIZE];
};
int main()
{
int shmid;
void *shmptr = NULL;
char str[MAXSIZE]; //存储输入的内容
struct shm *share;
if(shmid = shmget(0X44,MAXSIZE,0666|IPC_CREAT) < 0)
perror("shmget");
if((shmptr = shmat(shmid,0,0)) == (void *)-1)
perror("shmat");
printf("This is the write process!!!\n");
share = (struct shm *)shmptr;
while(1)
{
if(share->write == 1)
{
sleep(1);
printf("Waiting the read process!!!\n");
}
printf("please input hello world!!!\n");
fgets(str,MAXSIZE,stdin);
sprintf(share->buffer,"%s",str);
share->write = 1;
if(strncmp(str,"end",3) == 0)
break;
sleep(1);
}
if(shmdt(shmptr) < 0)
perror("shmdt");
exit(0);
}
程序运行截图
消息队列也叫报文队列,是一个消息的链表。可以把消息看作是一个记录,具有特定的格式以及优先级。对消息队列具有写权限的进程可以按照一定的规则向消息队列中添加消息,而对消息队列具有写权限的进程可以从消息队列中读走消息。和管道相似的是,消息一旦从消息队列中被读走,则消息队列中便不在存在此条消息。
IPC消息队列的缺省最大数为16;
每个消息缺省最大值为8192字节;
队列中的最大值缺省为16384字节;
每个消息队列都有其对应的属性信息,存储在struct_msqid_ds结构体中。
每个消息队列都有一个对应的id,标识消息队列的唯一性。
程序如下:
#include
#include
#include
#include
#include
#include
struct msg{
char msg_str[128];
};
int main()
{
int qid;
struct msg mymsg;
if(qid = msgget(0x66,0666|IPC_CREAT) < 0)
perror("msgget");
int pid;
pid = fork();
if(pid < 0)
perror("fork");
else if(pid == 0)
{
printf("This is A process!\n");
sprintf(mymsg.msg_str,"hello world");
if(msgsnd(qid,&mymsg,128,0) < 0)
perror("msgsnd");
}
else
{
if(fork() == 0)
{
printf("This is B process!\n");
if(msgrcv(qid,&mymsg,128,0,0) < 0)
perror("msgrcv");
printf("The msg is: %s\n",mymsg.msg_str);
}
else if(fork() == 0)
{
printf("This is the C process!\n");
sprintf(mymsg.msg_str,"wangguagnjie");
if(msgsnd(qid,&mymsg,128,0) < 0)
perror("msgsnd");
}
else
{
printf("This is D process!\n");
if(msgrcv(qid,&mymsg,128,0,0) < 0)
perror("msgrcv");
printf("The msg is: %s\n",mymsg.msg_str);
}
}
return 0;
}
该程序利用fork函数创建了四个进程,分别完成发送和接受的任务
匿名管道:是指用于连接一个读进程和一个写进程,以实现它们之间通信的共享文件,又称pipe文件,是一个只存在于内存的特殊文件。
匿名管道特点:
匿名管道是半双工的,数据只能向一个方向流动;
一个进程将数据写入管道,另一进程从管道中读取数据;
写入的内容添加在管道缓冲区的末尾,每次都是从缓冲区头部读出数据;
数据读出后将从管道中移走,其它读进程都不能再读到这些数据。
双向通信的建立
需要建立起两个管道
使用限制
只能用于具有亲缘关系的进程之间
如父子进程或兄弟进程之间