进程 第四天(进程间通信)
印象笔记:进程 第四天(进程间通信)
一,进程间通信简介
由于每个进程都有自己独立的运行环境,因此进程与进程间是相对封闭的。如何让两个封闭的进程之间实现数据通信是进程编程的重点与难点。
Linux的内的进程通信机制基本来源于Unix的系统对Unix的发展做出巨大贡献的两大主力--AT&T公司的贝尔实验室和加州大学伯克利分校 - 。在进程通信领域研究的侧重点不同。
贝尔实验室对Unix系统早期的进程间通信手段进行了改进与扩充,形成了System V IPC(进程间通信)。互相通信的进程被限定在单个计算机内。而伯克利分校则跳出了解System V IPC的限制,发展出了以套接字(socket)为基本点的进程间通信机制。
Linux的系统将二者的优势全部继承下来现在的Linux系统内比较常用的进程间通信方式有以下几种:
- 传统UNIX系统内的进程通信方式:
1)无名管道(管)和有名管道(FIFO):
管道提供了进程间通信消息传递的实体,其原型来自于数据结构的“队列”。无名管道用于具有亲缘关系的进程(例如父子进程,兄弟进程),而有名管道则允许不具有亲缘关系的进程使用。
2)信号(信号):
信号是在软件层面上对中断的一种模拟机制,用于通知进程某个事件发生。
-System V IPC进程通信方式:
3)消息队列(消息队列):
消息队列是消息所构成的链表,包括POSIX消息队列与系统V消息队列两种。消息队列克服了管道与信号两种通信方式中信息量有限的缺点。
4)共享内存(共享内存):
最有效的进程通信方式。它使得多个进程共享一块内存空间,不同进程间可以实时观察到其他进程的数据更新。不过使用该方式需要某种同步与互斥机制。
5)信号量(信号量):
主要作为进程间以及同一进程的不同线程间的同步与互斥手段。
-BSD进程通信方式:
6)套接字(socket):
更广泛的进程通信机制,常用于网络的不同主机之间的进程通信。
在本课程中我们只学习前5中进程间通信的方式,而第6中(套接字通信socket)将会在后续的网络课程中详细学习。
二、管道通信——无名管道pipe
管道是Linux中进程间通信的一种常用方式,它将一个程序的输出直接作为另一个程序的输入。Linux内的管道通信主要有无名管道与有名管道两种。
1、无名管道简介
无名管道是Unix系统内一种原始的进程通信方法。使用无名管道需要注意:
1.只能用于具有亲缘关系的进程间通信(父子进程、兄弟进程)
2.半双工通信模式,即无法同时读写管道。管道具有固定的读端与写端
3.管道可以看做特殊的文件,可以使用read()/write()函数对管道进行读写操作(但是不能使用lseek()进行定位操作)。不过管道不属于文件系统,并且只存放在内存中
2、无名管道编程
无名管道是基于文件描述符的通信方式。当一个管道被创建时,它会创建两个文件描述符fd[0]与fd[1],其中fd[0]固定用于读管道内容,fd[1]固定用于写管道内容。
函数pipe()
所需头文件:#include<unistd.h>
函数原型:int pipe(int fd[])
函数参数:
fd[] 包含两个文件描述符的数组,其中fd[0]固定用于读管道,fd[1]固定用于写管道
函数返回值:
成功:0
失败:-1
创建管道使用pipe()函数,而其余的操作诸如读取管道read()、写入管道write()、关闭管道close()函数与文件IO的函数使用方式相同,这里不再赘述。
那么如何使用无名管道实现父子进程间的通信呢?
由于无名管道具有固定的读端与写端,因此,如果父子进程需要使用无名管道进行通信,可以进行以下操作:
父进程->子进程:父进程对自己的fd[1]执行写操作,数据流入管道内,然后子进程对自己的fd[0]执行读操作,得到管道内数据
子进程->父进程:子进程对自己的fd[1]执行写操作,数据流入管道内,然后父进程对自己的fd[0]执行读操作,得到管道内数据
注意:无名管道的工作方式是半双工方式,即在一个进程内要么读管道,要么写管道,无法同时进行读写操作。也就是说,在同一时刻内,要么父进程写数据、子进程读数据(父进程->子进程),要么子进程写数据、父进程读数据(子进程->父进程),数据流动方向唯一,不能同时存在两个数据流动方向。在使用时,对于该进程内未使用的文件描述符应当关闭。
示例:使用无名管道实现父子进程间的通信(子进程->父进程)
使用无名管道编程时需要注意以下事项:
1.无名管道只能用于具有亲缘关系的进程间(通常是父子进程间)
2.fd[0]固定用于读取管道,fd[1]固定用于写入管道,两个文件描述符不可弄混否则会报错
3.只有管道存在读端,向管道内写入数据才有意义,否则会返回SIGPIPE信号报错
4.如果管道使用完毕,关闭所有的文件描述符即可
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#define MAXLEN 100
int main()
{
int n;
int fd[2];
pid_t pid;
char message[MAXLEN]={0};
if(pipe(fd)<0)//创建一个无名管道
{
perror("cannot create a pipe");
exit(0);
}
if((pid = fork())<0)//创建子进程
{
perror("cannot fork");
exit(0);
}
else if(pid==0)//子进程
{
printf("This is Child Process\n");
close(fd[0]);//关闭该进程内的fd[0](读端),保留fd[1](写端),即接下来对该管道进行写操作
strcpy(message,"Helloworld\n"); //将字符串赋值到字符数组中
write(fd[1],message,strlen(message)); //将数组里的数据写入管道中
close(fd[1]);//管道使用完毕,关闭fd[1]
}
else//父进程
{
printf("This is Parent Process\n");
close(fd[1]);//关闭该进程内的fd[1](写端),保留fd[0](读端),即接下来对该管道进行读操作
sleep(1);//保证子进程先写数据
n = read(fd[0],message,MAXLEN); //fd[0]从无名管道中读取数据,读入到message数组中,n 为返回的字符个数
printf("Parent read %d characters, Message is:%s",n,message);
close(fd[0]);//管道使用完毕,关闭fd[0]
waitpid(pid,NULL,0);//父进程等待回收子进程
}
return 0;
}
/*******************管道的方向与流管道**********************/
细心的同学可能发现,我们在示例程序中使用管道的时候,关闭了父进程的写端与子进程的读端,相当于强行规定了管道的数据流动方向(子进程->父进程)。那么如果我们想复用该管道传输数据,实现“父进程->子进程”该怎么办呢?
非常遗憾,我们无法改变已经确定数据传输方向的管道的方向,即无法复用管道实现“父进程->子进程”的功能。那么管道为什么必须有方向呢?
实际上,管道方向算是一个历史遗留问题。管道通信是UNIX系统内最古老的通信方式。在早期的内核代码中,由于技术受限以及硬件性能不足,管道是必须确定方向的。现在的操作系统虽然已经足够强大,但是“管道必须确定传输方向”还是被保留下来。出于内核移植性的考虑,我们在使用管道的时候也必须确定管道的数据传输方向。
那么,有没有能够双向传输数据的管道呢?实际上是存在的,这种管道叫做“流管道”。使用流管道可以实现数据在管道内的双向流动。
如果想创建一个流管道,可以使用s_pipe()函数。遗憾的是,流管道只存在极个别的操作系统中。Linux系统是不支持流管道的。如果想使用流管道,我们可以使用socketpair()函数来模拟流管道。有关socketpair()函数的使用以及流管道的相关知识请同学们课外查阅资料,这里不再赘述。
/*******************管道的方向与流管道end*******************/
三、管道通信——有名管道fifo
1、有名管道简介
无名管道的使用范围比较狭隘,因为它只能实现有亲缘的进程之间的通信任务。如果想使用管道实现没有亲缘关系的进程间通信,我们可以采用有名管道的方式。
有名管道可以实现互不相干的两个进程之间的通信。有名管道在文件系统中可见,而且可以通过路径访问。进程通过文件IO的方式来读写管道内的数据。但是无法使用lseek()函数进行定位操作。
//有名管道的原型来自于数据结构的队列,因此有名管道遵循“先进先出”的原则,这也是有名管道的名称(FIFO)的由来。
除了本地通信外,有名管道的另一个典型应用是在网络中的客户机——服务器之间传输数据。如果有一个服务器,这个服务器与许多客户机有关,那么该服务器会创建一个“众所周知的FIFO”(即所有的客户机都知道该FIFO的访问路径),所有的客户机都可以使用该“众所周知的FIFO”向服务器提出请求。
但是这种通信方式的问题是,服务器如何将数据送回给客户机?通常情况下,服务器在接收到客户机的请求后,会专门建立一个FIFO与客户机进行通信,每个专用的FIFO都是与客户机的进程ID为基础的。
但是这种通信方式仍然具有弊端,服务器无法侦测客户机是否已经崩溃,因此有可能会有部分数据残留在管道内。
2、有名管道编程
有名管道的使用类似于创建一个文件,我们可以使用mkfifo()函数创建一个有名管道。
函数mkfifo()
所需头文件:#include<sys/types.h>
#include<sys/stat.h>
函数原型:int mkfifo(const char *pathname, mode_t mode)
函数参数:
pathname 要创建的有名管道的路径名与文件名
mode 创建的有名管道的文件权限码,通常用八进制数字表示
函数返回值:
成功:0
失败:-1
//Linux系统内,mkfifo同时也是一个用于创建有名管道的Shell命令。若不想调用该函数,则可以使用命令创建一个有名管道文件
示例1:编写程序,创建一个有名管道文件
#include<stdio.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<sys/types.h>
int main(int argc, const char *argv[])
{
if(argc<2)
{
printf("too few arguments\n");
exit(0);
}
if(mkfifo(argv[1],0664)<0)
{
perror("cannot create fifo");
exit(0);
}
return 0;
}
运行该程序,则可以创建一个有名管道文件。我们可以使用ls -l命令或stat命令查看该文件的属性
//当然也可以使用mkfifo命令创建有名管道文件
创建有名管道后,我们可以使用文件IO的方式操作管道。
若读取管道内的数据,则使用read()函数;
若想向管道内写入数据,则使用write()函数。
示例2:在示例1的基础上,编写程序,实现两个进程使用有名管道通信
注意:为了模拟“两个没有亲缘关系的进程”,我们将代码分成两部分,一部分读管道,一部分写管道。两端代码要分别使用两个终端同时运行。
//写管道代码如下
//文件fifo_write.c
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<string.h>
#include<sys/stat.h>
#include<unistd.h>
#include<sys/types.h>
#define MAX 256
int main(int argc, const char *argv[])
{
int fd;
char buffer[MAX]={0};
if(argc<2)
{
printf("too few arguments\n");
exit(0);
}
if((fd=open(argv[1],O_WRONLY))<0) //打开管道。因为需要写管道,所以使用O_WRONLY
{
perror("cannot open pipe");
exit(0);
}
printf("Please input string, if input 'quit' will stop:"); //输入"quit"程序停止
scanf("%[^\n]",buffer);
getchar();
while(strncmp(buffer,"quit",4)!=0)
{
write(fd,buffer,strlen(buffer)+1);
printf("Please input string, if input 'quit' will stop:");
scanf("%[^\n]",buffer);
getchar();
}
write(fd,buffer,strlen(buffer)+1); //将最后的"quit"写入管道中
close(fd); //关闭管道
return 0;
}
//读管道代码如下
//文件fifo_read.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<unistd.h>
#include<fcntl.h>
#define MAX 256
int main(int argc, const char *argv[])
{
int nread;
if(argc<2)
{
printf("too few arguments\n");
exit(0);
}
char readbuffer[MAX]={0};
int fd;
if((fd=open(argv[1],O_RDONLY))<0) //打开管道,因为需要读管道,所以使用O_RDONLY
{
perror("cannot open pipe");
exit(0);
}
while(1)
{
if((nread=read(fd,readbuffer,MAX))<=0) //读取出错 或 管道内已无数据
{
printf("read fifo error, will exit\n");
break;
}
if(strncmp(readbuffer,"quit",4)!=0)
{
printf("read string:%s\n",readbuffer);
bzero(readbuffer,MAX);
}
else //读到"quit"程序停止
{
printf("read 'quit', will exit\n");
break;
}
}
close(fd);//关闭管道
return 0;
}
也可以多个发送端多个接收端,但是收到的信息都是一样的。可以自己尝试
四、信号通信signal
1、信号通信简介
相信各位对“中断”都不陌生。信号是在软件层次上对中断机制的一种模拟,从原理上来说,进程接收信号并处理与处理器接收中断并处理是一样的。
信号通信是异步的(即非实时性的):一个进程不必等待信号的到达。事实上,进程也不知道什么时候信号会到达。
信号通信可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以通过信号知道用户空间进程发生了哪些事件。如果某个进程未处于运行态而接收到了一个信号,那么该信号会被内核保存起来,直至该进程恢复运行再将该信号传递给进程;如果一个信号被设置为阻塞,则该信号的传递会被延迟,直至其阻塞被取消才能被传递给进程。
通常情况下,以下情景会使用信号通信:
1.某些后台进程的通信。例如xinetd进程。
2.两个无亲缘关系的进程且无法使用有名管道
3.某个进程只能使用标准输入与标准输出(即无法读写管道)
信号通信最早来自对硬件中断的一种模拟,不过经POSIX的扩展后功能变得更加强大,不仅能够发送或接收信号,信号本身还可以附加信息。
信号的产生有硬件来源与软件来源。硬件来源常见的有按下键盘、定时器到时、硬件故障等;软件来源常见的有信号处理函数、非法操作等。我们可以使用kill -l命令查看所有信号与编号,其中1~31是传统UNIX支持的信号(非可靠信号,不支持嵌套),32~63是后来扩充的信号(可靠信号,支持嵌套)。
进程可以有三种方式来响应一个信号:
1.忽略信号:即对信号不做任何处理。但是SIGKILL(9)与SIGSTOP(19)信号不能被忽略。
2.捕捉信号:自定义信号处理函数,当接收到信号时执行相应的信号处理函数。
3.执行默认操作:Linux对每种信号都规定了默认操作,当接收到信号时进程执行信号的默认操作。
2、信号通信编程——信号的发送
1)发送信号:kill()函数与raise()函数
kill()函数与我们之前学习过的kill命令一样,都可以发送信号给一个进程或进程组(实际上,Shell命令的kill命令就是内核通过kill()函数实现的)。需要注意的是,kill()函数不仅可以发送终止进程的信号,它也可以发送其他信号。
raise()函数也可以发送一个信号,不过与kill()函数不同的是,raise()函数只能让进程向自身发送信号。
函数kill()
所需头文件:#include<sys/types.h>
#include<signal.h>
函数原型:int kill(pid_t pid, int sig)
函数参数:
pid:
正数 发送信号给进程标识符为pid的进程
0 信号被发送到所有和当前进程在同一个进程组的进程
-1 信号被发送给所有的有权给其发送信号的进程(除了1号init进程)
<-1 信号发送给进程组号为-pid的每个进程
sig 需要发送的信号。若为0则不会送出信号,但是系统会执行错误检查。通常使用0来检测某个进程是否正在运行
函数返回值:
成功:0
失败:-1
函数raise()
所需头文件:#include<signal.h>
函数原型:int raise(int sig)
函数参数:
sig 需要发送的信号。若为0则不会送出信号,但是系统会执行错误检查。通常使用0来检测某个进程是否正在运行
函数返回值:
成功:0
失败:-1
//函数raise()等价于kill(getpid(),sig)或pthread_kill(pthread_self(),sig)
示例:演示kill()函数与raise()函数。首先创建子进程,在子进程内调用raise()函数发送一个SIGSTOP信号使自身暂停;父进程中调用kill()函数向子进程发送信号SIGKILL
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<signal.h>
int main(int argc, const char *argv[])
{
pid_t pid;
if((pid=fork())<0)
{
perror("cannot fork");
exit(0);
}
else if(pid==0)//子进程
{
printf("这是子进程:%d, 等待信号。\n",getpid()); //主函数的sleep(5);
raise(SIGSTOP); //子进程被暂停
printf("子进程已被暂停\n"); //注意这句话并不会输出
exit(0);
}
else //父进程
{
int ret;
sleep(5); //让子进程先运行,等待时间
ret = waitpid(pid,NULL,WNOHANG); //wait();第一个参数:在else这里pid为子进程的pid;第二个参数:子进程退出时的状态;第三个参数:WNHANG:表示若指定的进程未结束,则立即返回0
if(ret==0) //如果子进程在运行
{
kill(pid,SIGKILL); //向子进程发送SIGKILL,杀死子进程
printf("父进程杀死了子进程。 %d\n",pid);
}
waitpid(pid,NULL,0);//回收子进程
exit(0);
}
return 0;
}
等待了父进程5s
示例2:在示例1的基础上,将父进程发送的信号换成SIGCONT,观察效果
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<signal.h>
int main(int argc, const char *argv[])
{
pid_t pid;
if((pid=fork())<0)
{
perror("cannot fork");
exit(0);
}
else if(pid==0)//子进程
{
printf("这是子进程: %d, 等待信号。\n",getpid());
raise(SIGSTOP);//子进程被暂停
printf("子进程已被暂停\n");//注意这句话会输出
exit(0);
}
else//父进程
{
int ret;
sleep(5);//让子进程先运行
ret = waitpid(pid,NULL,WNOHANG);
if(ret==0)//如果子进程在运行
{
kill(pid,SIGCONT);//向子进程发送SIGCONT,子进程恢复运行
}
waitpid(pid,NULL,0);//回收子进程
exit(0);
}
return 0;
}
3、信号通信编程——定时器信号
alarm()函数也称为闹钟函数,它可以在进程中设定一个定时器,当定时器计时结束时,它就会向进程发送SIGALRM信号,并且在终端输出"Alarm clock"表示计时结束。
注意:一个进程只能有一个闹钟时间,如果在调用alarm()函数前已经设定过闹钟时间,则旧的闹钟时间会被新的闹钟时间替代。
pause()函数用于将该进程挂起直至接收到某个信号为止。
函数alarm()
所需头文件:#include<unistd.h>
函数原型:unsigned int alarm(unsigned int second)
函数参数:
second 指定倒计时秒数,在second秒后发送SIGALRM信号
函数返回值:
成功:0(未设置过闹钟时间) 或 上个闹钟时间的剩余时间(设置过闹钟时间)
失败:-1
函数pause()
所需头文件:#include<unistd.h>
函数原型:int pause()
函数参数:无
函数返回值:-1,并且把errno设定为EINTR(仅会在接收到信号后返回)
示例:演示alarm()函数与pause()函数
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
alarm(5);//设定闹钟时间5s
pause();//将进程挂起,等待闹钟
printf("I should wake up\n");//注意此语句不会执行
return 0;
}
如果没有pause();挂起操作,程序的运行时间要超过alarm();所设定的时间,否则alarm();就不会返回"Alarm clock",就是因为已经提前结束了
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(int argc, char *argv[])
{
int i = 1;
alarm(5);
while(i <= 6)
{
printf("这是%d第次循环\n",i++);
sleep(1);
}
printf("I will exit\n");
return 0;
}
程序等待到了alarm();的结束信号。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(int argc, char *argv[])
{
int i = 1;
alarm(5);
while(i <= 3)
{
printf("这是%d第次循环\n",i++);
sleep(1);
}
printf("I will exit\n");
return 0;
}
这里就提前结束了程序,没有打印"Alarm clock"
执行程序,我们会发现printf()内的字符串不会被打印,而是会打印"Alarm clock"。这是因为SIGALRM信号默认的处理方式是终止程序,因此程序在printf()执行前就已经退出了。
4、信号通信编程——信号处理函数
在刚才的学习过程中,我们发现绝大多数的信号的默认处理都是终止进程。如果想要让进程接收信号后做出不同的响应,则需要设置信号处理函数。
信号处理函数主要有两个:signal()函数和sigaction()函数。signal()函数比较简单,只需指定信号类型与信号处理函数即可,但是它只能用于编号前31种信号处理,且不能通过信号传递信息。而sigaction()函数可以看做是signal()函数的升级版,功能比signal()函数更加健全强大。
函数signal()
所需头文件:#include<signal.h>
函数原型:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
函数参数:
signum 指定信号
handler 指定接收信号后的处理方式
SIG_IGN:忽略信号
SIG_DFL:采用默认方式处理信号
其他:自定义信号处理函数
函数返回值:
成功:以前的信号处理函数
失败:SIG_ERR
示例1:使用signal()函数捕捉信号,并执行相应的信号处理函数。其中SIGINT代表ctrl+c组合键,SIGQUIT代表ctrl+\组合键。
#include<stdio.h>
#include<stdlib.h>
#include<signal.h>
void handler(int sig_no) //自定义信号处理函数
{
if(sig_no == SIGINT)
{
printf("Got a signal: SIGINT(ctrl+c)\n");
}
else if(sig_no == SIGQUIT)
{
printf("Got a signal: SIGQUIT(ctrl+\\)\n");
}
}
int main()
{
signal(SIGINT,handler);
signal(SIGQUIT,handler);
printf("Waiting for signal SIGINT or SIGQUIT……\n");
pause(); //等待接收信号
return 0;
}
练习:若将程序内主函数的"signal(信号,handler);"改为"signal(信号,SIG_IGN);",则该程序会出现什么效果?
//sigaction为选学内容
函数sigaction()(选学)
所需头文件:#include<signal.h>
函数原型:int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
函数参数:
signum 指定信号,除了SIGKILL和SIGSTOP
act (若非NULL)指向sigaction结构体的指针(具体定义见下),包含对信号的处理。需要使用地址传递传参
oldact (若非NULL)同act,不过保存的是对原先信号的处理方式
函数返回值:
成功:0
失败:-1
/********************结构体struct sigaction简介*********************/
在sigaction()函数中,第二个和第三个参数需要的struct sigaction类型结构体如下:
struct sigaction
{
void (*sa_handler)(int);//信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void*);//不常见,当flags=SA_SIGINFO时生效,代替sa_handler
sigset_t sa_mask;//屏蔽信号
int sa_flags;//信号处理行为
void (*sa_restorer)(void);//不常见(基本已被废弃),当flags=SA_SIGINFO时才会使用
};
//注:在某些操作系统内,可能sa_handler和sa_sigaction两个成员合并,只保留一个。因此注意二者不要同时设置避免出现冲突
其中各结构体成员的具体含义如下:
1.sa_handler:
表示一个函数指针,指向信号处理函数。用法等同于signal()函数的第二个参数
2.sa_sigaction:
另一个函数指针,指向信号处理函数。由于该类型的函数指针有三个参数,因此功能更加强大,但是通常情况下不会使用。只有当第四个参数flags=SA_SIGINFO时才会使用,替代sa_handler;否则以sa_handler为准。
sa_sigaction需要的第二个参数siginfo_t的结构体类型定义如下:
siginfo_t
{
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused
hardware-generated signal
(unused on most architectures) */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
sigval_t si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count; POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
long si_band; /* Band event (was int in
glibc 2.3.2 and earlier) */
int si_fd; /* File descriptor */
short si_addr_lsb; /* Least significant bit of address
(since kernel 2.6.32) */
}
3.sa_mask:
指定在信号处理函数运行期间需要被屏蔽的信号,被屏蔽的信号在信号处理函数执行期间不会被响应。
4.sa_flags:
用于指定信号处理行为,有固定的取值。若需要多个取值则可以使用“按位或”连接。取值与对应的意义如下:
SA_NOCLDSTOP(不常用):
如果 signum 是 SIGCHLD,当子进程停止时 (即当它们接收到SIGSTOP、SIGTSTP、SIGTTIN 或 SIGTTOU) 或恢复 (即当它们接收到 SIGCONT) 时不接收通知。此标志仅在定义了 SIGCHLD 的处理程序时才有意义。
SA_NOCLDWAIT(Linux 2.6后新增)(不常用):
如果 signum 是 SIGCHLD,当子进程停止时不会将子进程设置成僵尸进程。此标志仅在定义了 SIGCHLD 的处理程序时 或 将信号的处理设置成SIG_DFL时才有意义。
SA_NODEFER:
当接收到此信号时,执行信号处理函数时不会屏蔽该信号,即在信号处理函数执行期间仍然能发出该信号。此标志仅在已经定义了信号处理函数时才有意义。
SA_ONSTACK(不常用):
在sigaltstack(2)提供的备用信号堆栈上调用信号处理程序。如果备用堆栈不可用,将使用默认堆栈。此标志仅在已经定义了信号处理函数时才有意义。
SA_RESETHAND:
调用信号处理程序后,重新将信号操作恢复到默认操作。此标志仅在已经定义了信号处理函数时才有意义。
SA_RESTART:
重新启动某些被信号中断的系统调用。此标志仅在已经定义了信号处理函数时才有意义。
SA_SIGINFO(Linux 2.2后新增)(不常用):
不使用sa_handler,而使用sa_sigaction。此标志仅在已经定义了信号处理函数时才有意义。
5.sa_restorer:被废弃
/********************结构体struct sigaction简介end******************/
示例2:使用sigaction()函数功能完成示例1
#include<stdio.h>
#include<stdlib.h>
#include<signal.h>
void handler(int sig_no)
{
if(sig_no == SIGINT)
{
printf("Got a signal: SIGINT(ctrl+c)\n");
}
else if(sig_no == SIGQUIT)
{
printf("Got a signal: SIGQUIT(ctrl+\\)\n");
}
}
int main()
{
struct sigaction action;
action.sa_handler = handler;
sigaction(SIGINT,&action,NULL);//注意sigaction函数第二个参数的使用方法
sigaction(SIGQUIT,&action,NULL);//注意sigaction函数第二个参数的使用方法
printf("Wating for signal SIGINT or SIGQUIT……\n");
pause();
return 0;
}
示例3:使用sigaction()函数,接收其他进程向本进程发送的SIGUSR1和SIGUSR2信号并获得打印相关信息。SIGUSR1和SIGUSR2信号是Linux系统预留给用户定义的信号,本身并无含义。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#define MAX 512
void sig_usr(int signum)
{
if(signum == SIGUSR1)
{
printf("SIGUSR1 received\n");
}
else if(signum == SIGUSR2)
{
printf("SIGUSR2 received\n");
}
}
int main()
{
char buf[MAX];
int n;
struct sigaction sa_usr;
sa_usr.sa_flags = 0;//可以将此处改成SA_RESTART
sa_usr.sa_handler = sig_usr;//设置信号处理函数
sigaction(SIGUSR1, &sa_usr, NULL);//接收信号SIGUSR1
sigaction(SIGUSR2, &sa_usr, NULL);//接收信号SIGUSR2
printf("My PID is %d\n", getpid());
while(1)
{
if((n = read(STDIN_FILENO, buf, MAX-1))<0)
{
perror("read is interrupted by signal");
}
else
{
buf[n] = '\0';
printf("%d bytes read: %s\n", n, buf);
}
}
return 0;
}
需要注意,我们应该在一个终端运行该程序,在另一个终端运行kill命令发送信号。kill命令为
kill -USR1 进程ID号(发送SIGUSR1信号) 或 kill -USR2 进程ID号(发送SIGUSR2信号)
若sa_flags设置为0,我们会发现程序会输出:
SIGUSR1 received
read is interrupted by signal: Interrupted system call
这是因为系统调用read()被刚才的信号打断,信号处理程序默认情况下是不会恢复系统调用的运行的。如果需要让系统调用read()恢复运行,可以将sa_usr.sa_flags设置为SA_RESTART。
练习:不使用系统的kill命令,自己编程实现向示例3程序发送SIGUSR1或SIGUSR2信号,其中发送的信号类型与目标进程ID通过命令行参数传递。
答案:
//运行程序:./kill SIGUSR1或SIGUSR2 目标进程ID
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<signal.h>
#include<string.h>
int main(int argc, const char *argv[])
{
pid_t pid;
if(argc<3)
{
printf("too few arguments\n");
printf("Usage: ./kill <SIGUSR1>or<SIGRUSR2> <PID>\n");
exit(0);
}
pid = atoi(argv[2]);
if(strcmp(argv[1],"SIGUSR1")==0)
{
kill(pid,SIGUSR1);
}
else if(strcmp(argv[1],"SIGUSR2")==0)
{
kill(pid,SIGUSR2);
}
else
{
printf("Input signal error!\n");
}
return 0;
}
共享内存、消息队列、信号量都属于SystemV型的进程间通信方法,三者在使用函数方面有相似之处,我们在学习过程中要仔细观察三者的相同点与不同点。
一、共享内存shared-memroy
1、共享内存简介
顾名思义,共享内存就是允许两个不相关的进程访问同一个内存。共享内存是进程间最为高效的一种通信方式,进程间可以直接读写内存而无需使用其他的手段。
在Linux系统内,内核专门预留了一块内存区域用于进程间交换信息。这段内存区可以由任何需要访问的进程将其映射到自己的私有地址空间,进程可以直接读写该区域而无需数据拷贝,从而大大提高了效率。所有进程都有权访问共享内存地址,就好像使用malloc()函数分配的内存一样。
但是,由于多个进程共享一段内存,因此一个进程改变共享内存内的数据可能会影响其他进程。由于共享内存本身并没有提供同步与互斥机制,因此需要依靠某种同步与互斥机制来保证数据的独立性,常见的手段是使用信号量来实现同步与互斥机制。
共享内存的使用分为两个步骤:第一步是创建共享内存,使用shmget()函数从内存中获取一块共享内存区域;第二步是映射共享内存,也就是使用shmat()把申请的共享内存区映射到具体的进程空间中。除此之外,还可以使用shmdt()函数撤销映射操作。
我们可以使用ipcs命令查看进程间通信的状态。
2、共享内存编程
使用共享内存编程通常需要调用shmget()、shmat()、shmdt()和shmctl()几个函数。
函数shmget()用于创建共享内存
函数shmget()
所需头文件:#include<sys/ipc.h>
#include<sys/shm.h>
函数原型:int shmget(key_t key, size_t size, int shmflg)
函数参数:
key 共享内存的键值,其他进程通过该值访问该共享内存,其中有个特殊值IPC_PRIVATE,表示创建当前进程的私有共享内存
size 申请的共享内存段的大小
shmflg 同open()函数的第三个参数,为共享内存设定权限,通常使用八进制表示。若共享内存不存在想创建一块全新的共享内存时,需要按位或IPC_CREAT
函数返回值:
成功:共享内存段的标识符(非负整数)
失败:-1
函数shmat()用于将共享内存映射到进程中
函数shmat()
所需头文件:#include<sys/types.h>
#include<sys/shm.h>
函数原型:void *shmat(int shmid, const void *shmaddr, int shmflg)
函数参数:
shmid 要映射的共享内存区标识符(即shmget()函数的返回值)
shmaddr 将共享内存映射到的指定内存地址,如果为NULL则会自动分配到一块合适的内存地址
shmflg SHM_RDONLY表示共享内存为只读,0(默认值)表示共享内存可读可写
函数返回值:
成功:被映射的内存地址
失败:-1
函数shmdt()用于将进程与共享内存分离。注意“分离”并不是删除共享内存,而是表示该进程不再使用该共享内存。
函数shmdt()
所需头文件:#include<sys/types.h>
#include<sys/shm.h>
函数原型:int shmdt(const void *shmaddr)
函数参数:
shmaddr 需要解除映射的共享内存地址
函数返回值:
成功:0
失败:-1
函数shmctl()用于控制共享内存,包括获得属性、删除共享内存等
函数shmctl()
所需头文件:#include<sys/ipc.h>
#include<sys/shm.h>
函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf)
函数参数:
shmid 共享内存区标识符(即shmget()函数的返回值)
cmd 需要对共享内存采取的操作。可取值有很多,常用的有:
IPC_STAT 将shmid_ds结构体中的数据设置为共享内存的当前关联值,即用shmid覆盖shmid_ds内的值
IPC_SET 如果进程权限允许,将共享内存的当前关联值设置为shmid_ds中给出的值
IPC_RMID 删除共享内存
buf 该参数是一个shmid_ds类型的结构体指针,使用时必须使用地址传递的方式。结构体成员很多,常用的有:
struct shmid_ds
{
uid_t shm_perm.uid; /* Effective UID of owner */
uid_t shm_perm.gid; /* Effective GID of owner */
mode_t shm_perm.mode; /* Permissions + SHM_DEST and SHM_LOCKED flags */
……
};
函数返回值:
成功:
IPC_INFO或SHM_INFO操作:内核内部记录的有关共享内存段的使用条目
SHM_STAT操作:shmid中指定的共享内存标识符
其他操作:0
失败:-1
/**********************函数ftok()******************/
在创建SystemV进程间通信(共享内存/消息队列/信号量)的函数中,都出现了key_t类型的key参数。该参数表示指定的键值,其他进程可以通过该键值访问该结构。不过有些情况下参数key的值是不能直接指定的(例如内核中已有该键值或键值非法),此时就需要使用ftok()函数生成一个符合要求的键值。
函数ftok()用于将路径名和当前进程标识符转换成符合SystemV的key值,该值是系统生成的,具有唯一性。因此在不能自定义指定参数key的时候,我们可以使用ftok()函数生成一个符合标准的key值。
函数ftok()
所需头文件:#include<sys/types.h>
#include<sys/ipc.h>
函数原型:key_t ftok(const char *pathname, int proj_id)
函数参数:
pathname 指定的目录
proj_id 指定的子序号,取值范围为0~255
函数返回值:
成功:生成的key值
失败:-1
使用ftok()生成键值的方式如下:
if((key=ftok(".",'a'))==-1)//第一个参数表示当前目录,第二个参数可以随意指定
{
perror("cannot ftok");
exit(0);
}
若ftok()执行成功,则变量key内存储的就是符合标准的key值。
/**********************函数ftok()end***************/
示例1:演示4个共享内存函数。
首先创建一块共享内存(在本例中设置为IPC_PRIVATE),之后创建子进程,在父子进程内分别将各自的进程映射到共享内存。
父进程等待用户输入,然后在共享内存内写入"WROTE"字符串连通用户输入数据共同写入共享内存;子进程待共享内存内出现"WROTE"数据后读出用户输入内容。
待数据传输结束后父子进程分别取消映射。最后删除共享内存。
在程序运行期间,多次调用"system("ipcs -m")"命令报告共享内存情况,注意每次报告的情况的异同。
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<string.h>
#define BUFFERSIZE 2048
int main()
{
pid_t pid;
int shmid;//共享内存标识符
char *shm_addr;
char flag[]="WROTE";
char buff[BUFFERSIZE];
//创建共享内存
if((shmid=shmget(IPC_PRIVATE,BUFFERSIZE,0664))<0)
{
perror("cannot shmget");
exit(0);
}
else
{
printf("Create sharedmemory: %d\n",shmid);
}
system("ipcs -m");//显示当前共享内存情况
pid = fork();//创建进程
if(pid == -1)
{
perror("cannot fork");
exit(0);
}
else if(pid == 0)//子进程
{
//映射共享内存
if((shm_addr=shmat(shmid,NULL,0)) == (void*)-1)//注意-1的写法
{
perror("Child Process: shmat");
exit(0);
}
else
{
//映射成功
printf("Child Process: Attach sharedmemory: %p\n",shm_addr);
}
system("ipcs -m");//显示当前共享内存情况
while(strncmp(shm_addr,flag,strlen(flag)))//等待传输数据
{
printf("Child Process is waiting for data……\n");
sleep(3);
}
strcpy(buff,shm_addr + strlen(flag));//获取数据,从WROTE后面开始读取
printf("Child Process read %s\n",buff);
//解除共享内存映射
if(shmdt(shm_addr)<0)
{
perror("Child Process: shmdt");
exit(0);
}
else
{
//解除映射成功
printf("Child Process Deattach sharedmemory\n");
}
system("ipcs -m");//显示当前共享内存情况
//删除共享内存
if(shmctl(shmid,IPC_RMID,NULL)==-1)
{
perror("Child Process: shmctl(IPC_RMID)\n");
exit(0);
}
else
{
printf("Delete sharedmemory\n");
system("ipcs -m");//显示当前共享内存情况
}
}
else//父进程
{
//映射共享内存
if((shm_addr=shmat(shmid,NULL,0)) == (void*)-1)//注意-1的写法
{
perror("Parent Process: shmat");
exit(0);
}
else
{
//映射成功
printf("Parent: Attach sharedmemory: %p\n",shm_addr);
}
sleep(1);//让子进程先运行
printf("\nInput string:");
fgets(buff,BUFFERSIZE,stdin);
strcpy(shm_addr,flag);//写入共享内存WROTE
strncpy(shm_addr + strlen(flag),buff,strlen(buff));//写入共享内存用户输入数据
//解除共享内存映射
if(shmdt(shm_addr)<0)
{
perror("Parent Process: shmdt");
exit(0);
}
else
{
//解除映射成功
printf("Parent Process Deattach sharedmemory\n");
}
system("ipcs -m");//显示当前共享内存情况
waitpid(pid,NULL,0);//等待回收子进程
printf("Finished\n");
}
return 0;
}
示例2:建立两个进程间的共享内存通信。首先创建两个文件分别执行两个不同的程序,文件shmread.c负责读取共享内存数据,文件shmwrite.c负责写入共享内存数据。为了方便操作,两个文件使用相同的结构体保存数据,结构体定义在头文件shmdata.h中。
在shmwrite.c内写入数据,让shmread.c读取,当写入"end"时程序结束。
由于共享内存通信需要互斥,我们在头文件内使用written变量控制共享内存的读写,非0值表示可读,0表示可写。
//文件shmdata.h
#ifndef _SHMDATA_H_HEADER
#define _SHMDATA_H_HEADER
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/ipc.h>
#define TEXT_SZ 2048
struct shared_use_st
{
int written;//作为一个标志,非0:表示可读,0表示可写
char text[TEXT_SZ];//记录写入和读取的文本
};
#endif
//文件shmread.c
#include "shmdata.h"
int main()
{
int running = 1;//程序是否继续运行的标志
void *shm = NULL;//分配的共享内存的原始首地址
struct shared_use_st *shared;
int shmid;//共享内存标识符
//创建共享内存
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
if(shmid == -1)
{
perror("shmget failed");
exit(0);
}
//将共享内存连接到当前进程的地址空间
shm = shmat(shmid, NULL, 0);
if(shm == (void*)-1)
{
perror("shmat failed");
exit(0);
}
printf("\nMemory attached at %p\n", shm);
//设置共享内存
shared = (struct shared_use_st*)shm;
shared->written = 0;
while(running)//读取共享内存中的数据
{
//没有进程向共享内存定数据有数据可读取
if(shared->written != 0)
{
printf("You wrote: %s\n", shared->text);
//读取完数据,设置written使共享内存段可写
shared->written = 0;
//输入了end,退出程序
if(strncmp(shared->text, "end", 3) == 0)
running = 0;
}
else//有其他进程在写数据,不能读取数据
sleep(1);
}
//把共享内存从当前进程中分离
if(shmdt(shm) == -1)
{
perror("shmdt failed");
exit(0);
}
//删除共享内存
if(shmctl(shmid, IPC_RMID, 0) == -1)
{
perror("shmctl(IPC_RMID) failed");
exit(0);
}
return 0;
}
//文件shmwrite.c
#include "shmdata.h"
int main()
{
int running = 1;
void *shm = NULL;
struct shared_use_st *shared = NULL;
char buffer[TEXT_SZ + 1];//用于保存输入的文本
int shmid;
//创建共享内存
shmid = shmget((key_t)1234, sizeof(struct shared_use_st), 0666|IPC_CREAT);
if(shmid == -1)
{
perror("shmget failed");
exit(0);
}
//将共享内存连接到当前进程的地址空间
shm = shmat(shmid, NULL, 0);
if(shm == (void*)-1)
{
perror("shmat failed");
exit(0);
}
printf("Memory attached at %p\n", shm);
//设置共享内存
shared = (struct shared_use_st*)shm;
while(running)//向共享内存中写数据
{
//数据还没有被读取,则等待数据被读取,不能向共享内存中写入文本
while(shared->written == 1)
{
sleep(1);
printf("Waiting...\n");
}
//向共享内存中写入数据
printf("Enter some text: ");
fgets(buffer,TEXT_SZ,stdin);
strcpy(shared->text, buffer);
//写完数据,设置written使共享内存段可读
shared->written = 1;
//输入了end,退出循环(程序)
if(strncmp(buffer, "end", 3) == 0)
running = 0;
}
//把共享内存从当前进程中分离
if(shmdt(shm) == -1)
{
perror("shmdt failed");
exit(0);
}
sleep(2);
return 0;
}
其实,示例2的程序实际上是有风险的,因为我们并未对这块共享内存实施任何的互斥操作(虽然使用了written变量但功能不足)。使用信号量实现共享内存的互斥操作见本文最后的程序。
二、消息队列messagequeue
1、消息队列简介
顾名思义,消息队列就是一些消息构成的列表,准确来说,消息队列是在消息的传输过程中保存消息的容器。如果从功效方面说,消息队列可以看做是消息构成的链表。用户可以在消息队列中添加和读取消息等。消息队列具有一定的管道的特点,但是消息队列可以实现消息的随机查询,比管道具有更加明显的优势。
消息队列存在于内核中,使用消息队列的“队列ID”来唯一标识。
使用消息队列有以下的优点:
1.解耦:
使用消息队列通信的每个成员不会受到其他成员的干扰,成员与成员间只使用消息队列互相通信,大大降低了成员间的耦合度。
2.提速:
使用消息队列后,消息的发送者无需监视消息消息的接收状态,这样就有更多的时间处理其他事务。
3.广播:
一个成员向消息队列提供了消息后,其他成员都可以看到,降低了消息广播的成本。
4.削峰:
若遇到瞬时消息量暴涨,消息队列可以起到一定的缓冲作用,使用消息队列的成员不会受到大量消息的干扰。
但是使用消息队列也是存在一定的弊端:
1.引入了额外的空间:
毫无疑问,消息队列是需要额外存储空间的。
2.无法实时通信:
在一个成员送出消息后,它无法保证也无法获知目标成员什么时候会接收到消息,也无法知道目标成员是否已经处理该消息。
消息队列不仅可以用于同一个系统的进程间通信,也可以应用于网络通信。例如访问网络数据库、大型服务器(主要用于削峰)、大型网站、在线聊天室、P2P数据传输等领域都有消息队列的应用。
2、消息队列编程
使用消息队列通常需要创建/打开消息队列、添加消息、读取消息、控制消息队列四种操作。
函数msgget()用于创建/打开消息队列
函数msgget()
所需头文件:#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
函数原型:int msgget(key_t key,int msgflg)
函数参数:
key 消息队列的键值,其他进程通过该值访问该消息队列,其中有个特殊值IPC_PRIVATE,表示创建当前进程的私有消息队列
msgflg 同open()函数的第三个参数,为消息队列设定权限,通常使用八进制表示。若消息队列不存在想创建一个全新的消息队列时,需要按位或IPC_CREAT
函数返回值:
成功:消息队列ID
失败:-1
函数msgsnd()用于向消息队列中添加消息,该函数可以将消息添加到消息队列的末尾
函数msgsnd()
所需头文件:#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
函数原型:int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)
函数参数:
msqid 消息队列标识符(即msgget()函数的返回值)
msgp 指向消息队列的结构体指针,必须使用地址传递的方式传参。该结构体的类型如下:
struct msgbuf
{
long mtype;//消息类型,必须大于0
char mtext[n];//消息正文
};
msgsz 消息正文的字节数,必须与第二个参数的消息正文数据长度一致
msgflg
IPC_NOWAIT 若消息无法立即发送则立即返回
0 若消息无法立即发送则阻塞等待消息发送成功
函数返回值:
成功:0
失败:-1
函数msgrcv()用于从消息队列中提取消息,它与msgsnd()经常一起使用。与管道不同的是,消息队列可以指定取走任意消息
函数msgrcv()
所需头文件:#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
函数原型:ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg)
函数参数:
msqid 消息队列标识符(即msgget()函数的返回值)
msgp 指向消息队列的结构体指针,必须使用地址传递的方式传参。该结构体的类型如下:
struct msgbuf
{
long mtype;//消息类型,必须大于0
char mtext[n];//消息正文
};
msgsz 消息正文的字节数,必须与第二个参数的消息正文数据长度一致
msgtyp
0 接收消息队列中第一个消息
大于0 接收消息队列中第一个值为msgtyp的消息
小于0 接收消息队列中具有小于或等于msgtyp绝对值的最小mtype值的第一条消息
msgflg
MSG_NOERROR 若返回的消息比msgsz字节多,则消息会截断到msgsz字节,且不通知消息发送进程
IPC_NOWAIT 若消息队列中无对应类型的消息接收则立即返回
0 阻塞等待直至接收到一条相应类型的消息为止
函数返回值:
成功:0
失败:-1
函数msgctl()用于消息队列控制,包括获得属性、删除消息队列等
函数msgctl()
所需头文件:#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
函数原型:int msgctl(int msqid, int cmd, struct msqid_ds *buf)
函数参数:
msqid 消息队列标识符(即msgget()函数的返回值)
cmd 需要对消息队列采取的操作。可取值有很多,常用的有:
IPC_STAT 读取消息队列的数据结构msqid_ds并将其存储在buf指定的地址中
IPC_SET 设置消息队列中的数据结构msqid_ds中的pic_perm元素的值。这个值来自buf参数
IPC_RMID 从内核中删除消息队列
buf 该参数是一个msqid_ds类型的结构体指针,使用时必须使用地址传递的方式。结构体成员很多,常用的有:
struct msqid_ds
{
uid_t msg_perm.uid; /* Effective UID of owner */
gid_t msg_perm.gid; /* Effective GID of owner */
……
};
函数返回值:
成功:0
失败:-1
示例:建立两个进程间的共享内存通信。文件msgsend.c负责读取用户输入信息并发送信息,若输入"quit"则程序停止;文件msgreceive.c负责接收消息队列的信息,若接收到"quit"则表示程序结束,删除消息队列。两个文件共用一个同文件msgdata.h。
//文件msgdata.h
#ifndef _MSGDATA_H_INCLUDE_
#define _MSGDATA_H_INCLUDE_
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<unistd.h>
#include<string.h>
#define BUFFERSIZE 512
struct message//需要发送/接收的消息的数据类型
{
long msg_type;
char msg_text[BUFFERSIZE];
};
#endif
//文件msgsend.c
#include"msgdata.h"
int main()
{
int qid;
key_t key = 1234;
struct message msg;
if((qid = msgget(key,IPC_CREAT|0664))==-1)//打开/创建消息队列
{
perror("cannot msgget");
exit(0);
}
printf("Open MessageQueue %d\n",qid);
while(1)
{
printf("Please input string, input 'quit' to stop:");
fgets(msg.msg_text,BUFFERSIZE,stdin);
msg.msg_type = getpid();//将自己的进程ID作为type发送给msgreceive
if((msgsnd(qid,&msg,strlen(msg.msg_text),0))==-1)//发送数据
{
perror("cannot msgsnd");
exit(0);
}
if(strncmp(msg.msg_text,"quit",4)==0)
{
break;
}
}
return 0;
}
//文件msgreceive.c
#include"msgdata.h"
int main()
{
int qid;
key_t key = 1234;
struct message msg;
if((qid = msgget(key,IPC_CREAT|0664))==-1)//打开/创建消息队列
{
perror("cannot msgget");
exit(0);
}
printf("Open MessageQueue %d\n",qid);
while(1)
{
bzero(msg.msg_text,BUFFERSIZE);
if(msgrcv(qid,(void*)&msg,BUFFERSIZE,0,0)==-1)//接收数据,第4个参数0表示接收第一个消息
{
perror("cannot msgrcv");
exit(0);
}
printf("receive message from %ld:%s\n",msg.msg_type,msg.msg_text);
if(strncmp(msg.msg_text,"quit",4)==0)
{
break;
}
}
if((msgctl(qid,IPC_RMID,NULL))<0)//如果接收到quit则删除消息队列
{
perror("cannot msgctl");
exit(0);
}
printf("Remove MessageQueue %d success\n",qid);
return 0;
}
三、信号量semaphore
1、信号量通信简介
在多进程/多线程系统内,多个进程/线程会同时运行,多个进程/线程可能会为了完成同一个任务共同协作,这时进程/线程间就出现了同步关系。同样,在多进程/多线程系统内,不同任务之间也可能会争夺有限的系统资源,多个进程/线程可能进入争夺状态,这时进程/线程间就出现了互斥关系。
任务之间的同步与互斥存在的根源主要是临界资源。临界资源是指在同一时刻只允许有限个(通常是一个)任务可以访问资源。例如各种硬件资源(CPU、内存空间、存储设备、打印机等外部设备等)和软件资源(共享内存段、共享变量等)。临界资源存放的区域称为临界区。
信号量是用来解决进程/线程间同步与互斥问题的一种通信机制,包括一个信号量变量,以及对该信号量进行的原子操作(PV操作)。
PV操作的具体定义如下:
P操作(通过):对信号量减1,若结果大于等于0,则进程继续,否则执行P操作的进程被阻塞等待释放
V操作(释放):对信号量加1,若结果小于等于0,则唤醒队列中一个因为P操作而阻塞的进程,否则不必唤醒进程
最简单的信号量只有0和1两个值,称为二值信号量;如果一个信号量有多个值(0~n)则称为计数信号量,计数信号量表示可用的资源数。
2、信号量编程
在Linux系统中,使用信号量通常需要创建信号量、初始化信号量、信号量PV操作以及信号量删除四种操作。
函数semget()用于创建一个(或多个)信号量
函数semget()
所需头文件:#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
函数原型:int semget(key_t key, int nsems, int semflg)
函数参数:
key 信号量的键值,其他进程通过该值访问该信号量,其中有个特殊值IPC_PRIVATE,表示创建当前进程的私有信号量
nsems 需要创建的信号量数目,通常为1。若创建多个信号量则称为信号量集
semflg 同open()函数的第三个参数,为信号量设定权限,通常使用八进制表示。若按位或IPC_CREAT表示创建一个全新的信号量,即使该信号量已经存在也不会报错;若按位或IPC_EXCL,则信号量存在时该函数会返回报错。
函数返回值:
成功:信号量的标识符(非负整数)
失败:-1
函数semctl()用于对信号量进行相应的控制,包括获取信息、设置属性、删除信号量(集)等操作
函数semctl()
所需头文件:#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
函数原型:int semctl(int semid, int semnum, int cmd, union semun arg)
函数参数:
semid 信号量标识符(即semget()函数的返回值)
semnum 信号量编号,通常存在多个信号量时才会使用。通常取值为0,即第一个信号量。
cmd 需要对信号量采取的操作。可取值有很多,常用的有:
IPC_STAT 读取消息队列的数据结构semid_ds并将其存储在第四个参数arg结构变量的buf指定的地址中
IPC_SETVAL 将信号量值设定为arg中的val值
IPC_GETVAL 获取当前信号量的值
IPC_RMID 从内核中删除信号量(集)
arg 是一个union semun结构的共用体,具体类型如下:
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) */
};
注意:某些系统内未给出union semun的定义,需要程序员自己定义该共用体。
其中buf参数是一个semid_ds类型的结构体指针,使用时必须使用地址传递的方式。结构体成员很多,常用的有:
struct semid_ds
{
uid_t sem_perm.uid; /* Effective UID of owner */
gid_t sem_perm.gid; /* Effective GID of owner */
……
};
函数返回值:
成功:
IPC_STAT、IPC_SETVAL或IPC_RMID操作:0
IPC_GETVAL操作:返回当前信号量的值
失败:-1
函数semop()用于对信号量进行PV操作
函数semop()
所需头文件:#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
函数原型:int semop(int semid, struct sembuf *sops, size_t nsops)
函数参数:
semid 信号量标识符(即semget()函数的返回值)
sops 是一个sembuf类型的结构体指针,使用时必须使用地址传递的方式。结构体类型如下:
struct sembuf
{
unsigned short sem_num;//信号量编号,若是单个信号量则取值0
short sem_op;//取值-1为P操作,取值1为V操作
short sem_flg;//通常取值SEM_UNDO,表示进程结束后系统自动释放该进程中未释放的信号量
};
nsops 需要操作的信号量数目,通常取值1(一个操作)
函数返回值:
成功:信号量的标识符
失败:-1
示例1:演示3个信号量处理函数。在该示例中,为了使用方便,我们将初始化信号量操作、删除信号量操作、PV操作分别封装成子函数。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
union semun //若该共用体未定义,则需要手动定义
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
int init_sem(int sem_id, int init_value)
{
union semun sem_union;
sem_union.val = init_value;
if(semctl(sem_id,0,SETVAL,sem_union)==-1)
{
perror("Initialize semaphore");
return -1;
}
return 0;
}
int del_sem(int sem_id)
{
union semun sem_union;
if(semctl(sem_id,0,IPC_RMID,sem_union)==-1)
{
perror("Delete semaphore");
return -1;
}
return 0;
}
int sem_p_operate(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num=0; //表示单个信号量
sem_b.sem_op=-1; //表示P操作
sem_b.sem_flg=SEM_UNDO; //表示系统会自动回收系统内残余的信号量
if(semop(sem_id,&sem_b,1)==-1)
{
perror("P opreate");
return -1;
}
return 0;
}
int sem_v_operate(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num=0; //表示单个信号量
sem_b.sem_op=1; //表示V操作
sem_b.sem_flg=SEM_UNDO; //表示系统会自动回收系统内残余的信号量
if(semop(sem_id,&sem_b,1)==-1)
{
perror("V opreate");
return -1;
}
return 0;
}
int main()
{
pid_t pid;
int sem_id;
sem_id = semget(1234,1,0664|IPC_CREAT); //创建1个信号量
init_sem(sem_id,0); //将信号量初值设定为0
pid = fork();
if(pid<0)
{
perror("cannot fork");
exit(0);
}
else if(pid==0) //子进程
{
printf("Child process %d will ENTER to wakeup Parent……\n",getpid());
getchar(); //输入回车,释放父进程
printf("Child %d will V-operate\n",getpid());
sem_v_operate(sem_id);
}
else //父进程
{
printf("Parent process %d will P-operate\n",getpid());
sem_p_operate(sem_id); //父进程被阻塞等待释放
printf("Parent process %d go on\n",getpid());
del_sem(sem_id);
}
return 0;
}
示例2:使用信号量完成公交车司机——公交车售票员模型
司机的流程如下:
P(S1)
启动车辆
正常行驶
到站停车
V(S2)
售票员的流程如下:
关车门
V(S1)
售票
P(S2)
开车门
上下乘客
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
int init_sem(int sem_id, int init_value)
{
union semun sem_union;
sem_union.val = init_value;
if(semctl(sem_id,0,SETVAL,sem_union)==-1)
{
perror("Initialize semaphore");
return -1;
}
return 0;
}
int del_sem(int sem_id)
{
union semun sem_union;
if(semctl(sem_id,0,IPC_RMID,sem_union)==-1)
{
perror("Delete semaphore");
return -1;
}
return 0;
}
int sem_p_operate(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num=0;
sem_b.sem_op=-1;
sem_b.sem_flg=SEM_UNDO;
if(semop(sem_id,&sem_b,1)==-1)
{
perror("P opreate");
return -1;
}
return 0;
}
int sem_v_operate(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num=0;
sem_b.sem_op=1;
sem_b.sem_flg=SEM_UNDO;
if(semop(sem_id,&sem_b,1)==-1)
{
perror("V opreate");
return -1;
}
return 0;
}
int main()
{
pid_t pid;
int sem_ids1,sem_ids2;
sem_ids1 = semget(1234,1,0664|IPC_CREAT);
sem_ids2 = semget(5678,1,0664|IPC_CREAT);
init_sem(sem_ids1,0);
init_sem(sem_ids2,0);
pid = fork();
if(pid<0)
{
perror("cannot fork");
exit(0);
}
else if(pid==0)//子进程表示司机进程
{
while(1)
{
sem_p_operate(sem_ids1);
printf("司机发现关闭车门\n");
printf("司机启动车辆\n");
sleep(1);
printf("司机驾驶车辆\n");
sleep(10);
printf("车辆到站,司机停车\n");
sem_v_operate(sem_ids2);
}
}
else//父进程表示售票员进程
{
while(1)
{
printf("售票员关闭车门\n");
sleep(1);
sem_v_operate(sem_ids1);
printf("售票员开始售票\n");
sleep(5);
printf("售票员售票完毕\n");
sem_p_operate(sem_ids2);
printf("车辆到站,售票员开启车门\n");
printf("乘客上下车\n");
sleep(1);
}
del_sem(sem_ids1);
del_sem(sem_ids2);
}
return 0;
}
示例3:使用信号量实现共享内存的互斥操作该程序由4个文件组成
shmdata.h其余三个.c文件需要的头文件
semPV.c信号量的操作
shmwrite.c向共享内存内写数据操作
shmread.c读取共享内存的数据操作
编译时shmwrite.c和shmread.c都需要与semPV.c一起编译,运行时,在两个不同的终端分别运行两个程序,先运行写程序,再运行读程序
//文件shmdata.h
#ifndef _SHMDATA_H_HEADER
#define _SHMDATA_H_HEADER
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/types.h>
#include <sys/ipc.h>
#define TEXT_SZ 2048
struct shared_use_st
{
pid_t pid;//写入数据的进程ID
char buffer[TEXT_SZ];//记录写入和读取的文本
};
#endif
//文件semPV.c
#include "shmdata.h"
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
int init_sem(int sem_id, int init_value)
{
union semun sem_union;
sem_union.val = init_value;
if(semctl(sem_id,0,SETVAL,sem_union)==-1)
{
perror("Initialize semaphore");
return -1;
}
return 0;
}
int del_sem(int sem_id)
{
union semun sem_union;
if(semctl(sem_id,0,IPC_RMID,sem_union)==-1)
{
perror("Delete semaphore");
return -1;
}
return 0;
}
int sem_p_operate(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num=0;
sem_b.sem_op=-1;
sem_b.sem_flg=SEM_UNDO;
if(semop(sem_id,&sem_b,1)==-1)
{
perror("P opreate");
return -1;
}
return 0;
}
int sem_v_operate(int sem_id)
{
struct sembuf sem_b;
sem_b.sem_num=0;
sem_b.sem_op=1;
sem_b.sem_flg=SEM_UNDO;
if(semop(sem_id,&sem_b,1)==-1)
{
perror("V opreate");
return -1;
}
return 0;
}
//文件shmwrite.c
#include "shmdata.h"
int main()
{
void *shared_memory = NULL;
struct shared_use_st *shm_buff_insert = NULL;
char buffer[BUFSIZ + 1];//用于保存输入的文本
int shmid,semid;
semid = semget(ftok(".",'a'), 1, 0666|IPC_CREAT);//创建信号量
init_sem(semid,1);//设置信号量的初值为1
//创建共享内存
shmid = shmget(ftok(".",'b'), sizeof(struct shared_use_st), 0666|IPC_CREAT);
if(shmid == -1)
{
perror("shmget failed");
exit(0);
}
//将共享内存连接到当前进程的地址空间
shared_memory = shmat(shmid, (void*)0, 0);
if(shared_memory == (void*)-1)
{
perror("shmat failed");
exit(0);
}
printf("Write Process ID is %d\n",getpid());
printf("Memory attached at %p\n", shared_memory);
//设置共享内存
shm_buff_insert = (struct shared_use_st*)shared_memory;
do
{
sem_p_operate(semid);//P操作
printf("Input string to shm(enter 'quit' to exit):");
if(fgets(shm_buff_insert->buffer,TEXT_SZ,stdin)==NULL)//读取用户输入
{
perror("cannot write shm");
sem_v_operate(semid);
break;
}
shm_buff_insert->pid = getpid();//将自己的进程ID写入共享内存
sem_v_operate(semid);//V操作
}while(strncmp(shm_buff_insert->buffer,"quit",4)!=0);
//把共享内存从当前进程中分离
if(shmdt(shared_memory) == -1)
{
perror("shmdt failed");
exit(0);
}
del_sem(semid);//删除信号量
return 0;
}
//文件shmread.c
#include "shmdata.h"
int main()
{
void *shared_memory = NULL;
struct shared_use_st *shm_buff_insert = NULL;
char buffer[BUFSIZ + 1];//用于保存输入的文本
int shmid,semid;
if((semid = semget(ftok(".",'a'), 1, 0666|IPC_CREAT))<0)//获取信号量
{
perror("cannot semget");
exit(0);
}
//创建共享内存
shmid = shmget(ftok(".",'b'), sizeof(struct shared_use_st), 0666|IPC_CREAT);
if(shmid == -1)
{
perror("shmget failed");
exit(0);
}
//将共享内存连接到当前进程的地址空间
shared_memory = shmat(shmid, (void*)0, 0);
if(shared_memory == (void*)-1)
{
perror("shmat failed");
exit(0);
}
printf("Read Process ID is %d\n",getpid());
printf("Memory attached at %p\n", shared_memory);
//设置共享内存
shm_buff_insert = (struct shared_use_st*)shared_memory;
while(1)
{
sem_p_operate(semid);//P操作
printf("Shared-Memory was written by process %d: %s",shm_buff_insert->pid,shm_buff_insert->buffer);
if(strncmp(shm_buff_insert->buffer,"quit",4)==0)
break;
shm_buff_insert->pid = 0;
bzero(shm_buff_insert->buffer,TEXT_SZ);
//sleep(10);
sem_v_operate(semid);//V操作
}
//把共享内存从当前进程中分离
if(shmdt(shared_memory) == -1)
{
perror("shmdt failed");
exit(0);
}
if(shmctl(shmid,IPC_RMID,NULL)==-1)//删除共享内存
{
perror("cannot shmctl(IPC_RMID)");
exit(0);
}
return 0;
}
我们可以在阅读程序的V操作之前添加一个延时,此时可以发现写程序暂时无法向共享内存中写入数据。