进程间通信(IPC)的五种方式:(管道、FIFO、共享内存、信号量、消息队列。
进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。
进程间通信的本质:
让两个不同的进程看到同一份资源(该资源通常由操作系统直接或间接提供)
进程间通信目的:
数据传输 : 一个进程需要将它的数据发送给另一个进程
资源共享 : 多个进程之间共享有同样的资源
通知事件 :一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如:进程终止时要通知父进程)
进程控制 : 有些进程希望完全控制另一个进程的运行(如:Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
一、pipe(无名管道)
管道,一般指匿名管道,是 UNIX 系统中 IPC最古老的形式。
通常把从一个进程链接到另一个进程的一个数据流成为一个“管道”。
创建无名管道(pipe函数):
#include <unistd.h>
int pipe(int fd[2]);
功能:创建无名管道。
参数:
pipefd : 为 int 型数组的首地址,其存放了管道的文件描述符 pipefd[0]、pipefd[1]。
当一个管道建立时,它会创建两个文件描述符 fd[0] 和 fd[1]。其中 fd[0] 固定用于读管道,
而 fd[1] 固定用于写管道。一般文件 I/O的函数都可以用来操作管道(lseek() 除外)。
返回值:
成功:0
失败:-1
建立一个管道时,会创建两个文件描述符,读端和写段,fd—>文件描述符,其中fd[0]表示读端,fd[1]表示写端 如下图:
若要数据流从父进程流向子进程,则关闭父进程的读端(fd[0])与子进程的写端(fd[1]);反之,则可以使数据流从子进程流向父进程。
子进程和父进程通信1
子进程通过无名管道给父进程传递一个字符串数据:
代码:
#include<stdio.h>
#include<unistd.h>
int main()
{
int fd[2]; // 两个文件描述符
pid_t pid;
char buf[20];
if(pipe(fd) < 0) // 创建管道
printf("Create Pipe Error!\n");
if((pid = fork()) < 0) // 创建子进程
printf("Fork Error!\n");
else if(pid > 0) // 父进程
{
close(fd[0]); // 关闭读端
write(fd[1], "hello world\n", 12);
}
else
{
close(fd[1]); // 关闭写端
read(fd[0], buf, 20);
printf("%s", buf);
}
return 0;
}
父进程向子进程通信2
代码2:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
//创建无名管道
int main()
{
int fd[2];// 两个文件描述符
int pid;
char buf[128];
// int pipe(int pipefd[2]);
if(pipe(fd) == -1)//创建无名管道
{
printf("创建管道失败!\n");
}
pid = fork();//创建子进程
if(pid<0)
{
printf("创建进程失败!\n");
}
else if(pid > 0)//父进程
{
sleep(3);
printf("父进程:this is father\n");
close(fd[0]);//关闭读端
//写数据
write(fd[1],"hello from father",strlen("hello form father"));
wait();
}
else
{
//子进程
printf("子进程:this is child\n");
close(fd[1]);//关闭写端
//读
read(fd[0],buf,128);
printf("来自父进程信息: %s\n",buf);
exit(0);
}
return 0;
}
总结
读管道:
Ø 管道中有数据,read返回实际读到的字节数。
Ø 管道中无数据:
u 管道写端被全部关闭,read返回0 (相当于读到文件结尾)
u 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)
写管道:
Ø 管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程终止)
Ø 管道读端没有全部关闭:
u 管道已满,write阻塞。
u 管道未满,write将数据写入,并返回实际写入的字节数。
二、FIFO(有名管道)
概述
管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了命名管道(FIFO),也叫有名管道、FIFO文件。
命名管道(FIFO)不同于无名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中,这样,即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。
命名管道(FIFO)和无名管道(pipe)有一些特点是相同的,不一样的地方在于:
-
FIFO 在文件系统中作为一个特殊的文件而存在,但 FIFO 中的内容却存放在内存中。
-
当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。
-
FIFO 有名字,不相关的进程可以通过打开命名管道进行通信。
命令创建有名管道(mkfifo filename)
命名管道可以从命令行上创建,使用下面的命令:
程序创建有名管道(mkfifo函数)
命名管道也可以从程序里创建,相关函数为:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
功能:
命名管道的创建。
参数:
pathname : 普通的路径名,也就是创建后 FIFO 的名字。
mode : 文件的权限,与打开普通文件的 open() 函数中的 mode 参数相同。(0666)
返回值:
成功:0 状态码
失败:如果文件已经存在,则会出错且返回 -1。
代码1-创建有名管道
#include <sys/types.h>
#include <sys/stat.h>
#include<stdio.h>
int main()
{
int ret=mkfifo("./file",0600);
if(ret==0)
{
printf("mkfifo susccess\n");
}
if(ret==-1)
{
printf("mkfifo failuer\n");
}
return 0;
}
有名管道读写操作
一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo。如:close、read、write、unlink等。
FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。
代码1-有名管道读操作
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
// int mkfifo(const char *pathname, mode_t mode);
int main()
{
char buf[30] = {0};
int nread = 0;
//创建fifo管道
if( (mkfifo("./file",0600) == -1) && errno!=EEXIST)
{
printf("mkfifo failuer\n");
perror("why");
}
//以只读的方式打开文件
int fd = open("./file",O_RDONLY);//O_RDONLY以只读的方式打开
printf("open success\n");
while(1)
{
//读取FIFO管道
nread = read(fd,buf,30);
printf("read %d byte from fifo,context:%s\n",nread,buf);
}
close(fd);
return 0;
}
代码2-有名管道写操作
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include<errno.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main()
{
int cnt = 0;
char *str = "message from fifo";
//以只写的方式打开文件
int fd = open("./file",O_WRONLY);//O_WRONLY(以只写的方式打开)
printf("write open success\n");
while(1)
{
//写文件
write(fd, str, strlen(str));
sleep(1);
if(cnt == 5){
break;
}
}
close(fd);
return 0;
}
有名管道注意事项
- 一个为只读而打开一个管道的进程会阻塞直到另外一个进程为只写打开该管道
- 一个为只写而打开一个管道的进程会阻塞直到另外一个进程为只读打开该管道
-
读管道:
Ø 管道中有数据,read返回实际读到的字节数。
Ø 管道中无数据:
u 管道写端被全部关闭,read返回0 (相当于读到文件结尾)
u 写端没有全部被关闭,read阻塞等待 -
写管道:
Ø 管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程终止)
Ø 管道读端没有全部关闭:
u 管道已满,write阻塞。
u 管道未满,write将数据写入,并返回实际写入的字节数。
三、消息队列
-
由于管道的通信方式效率很低,不适合进程间频繁的交换数据,因此引入了消息队列的进程间通信方式。
-
消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
特点
- 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
- 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
- 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
函数原型
创建或打开消息队列(msgget函数)
#include <sys/msg.h>
// 创建或打开消息队列:成功返回队列ID,失败返回-1
int msgget(key_t key, int flag);
功能:⽤来创建和访问⼀个消息队列
参数:
key—> 某个消息队列的名字
msgflg—>由九个权限标志构成,它们的⽤法和创建⽂件时使⽤的mode模式标志是⼀样的
返回值:成功返回⼀个⾮负整数,即该消息队列的标识码;失败返回-1
添加消息(msgsnd函数)
// 添加消息:成功返回0,失败返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
参数:
size_t size:表示发送消息大小;
int flag:0,当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列;IPC_NOWAIT,
当消息队列已满的时候,msgsnd函数不等待立即返回;IPC_NOERROR,若发送的消息大于size字节,
则把该消息截断,截断部分将被丢弃,且不通知发送进程。
读取消息(msgrcv函数)
// 读取消息:成功返回消息数据的长度,失败返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
参数:
size_t size:要接收消息的最大大小,不含消息类型占用的4个字节.
long type:
type==0,接收第一个消息
type>0,接收类型等于msgtyp的第一个消息
type<0:接收类型等于或者小于msgtyp绝对值的第一个消息
msgflg:
0: 阻塞式接收消息,没有该类型的消息msgrcv函数一直阻塞等待
IPC_NOWAIT:如果没有返回条件的消息调用立即返回,此时错误码为ENOMSG
IPC_EXCEPT:与msgtype配合使用返回队列中第一个类型不为msgtype的消息
IPC_NOERROR:如果队列中满足条件的消息内容大于所请求的size字节,则把该消息截断,截断部分将被丢弃
控制消息队列(msgctl函数)
// 控制消息队列:成功返回0,失败返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf)
功能 :消息队列的控制函数
参数 :
msqid—>由msgget函数返回的消息队列标识码
cmd—>是将要采取的动作,(有三个可取值)
返回值 :成功返回0,失败返回-1
获取key值(ftok()函数)
获取key 值
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
返回值:
当成功执行的时候,一个key_t值将会被返回,否则 -1 被返回。
参数:
pathname:就是你指定的文件名(已经存在的文件名),相对路径和绝对路径都可以;
一般使用当前目录,如:key = ftok(".", 1); 这样就是将pathname设为当前目录。
proj_id:是子序号,可以根据自己的约定,随意设置。但是只使用8bits(1-255)。
注:在一般的UNIX实现中,是将文件的索引节点号取出,前面加上子序号得到key_t的返回值。
如指定文件的索引节点号为65538,换算成16进制为0x010002,而你指定的ID值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002。
查询文件索引节点号的方法是: ls -i
当删除重建文件后,索引节点号由操作系统根据当时文件系统的使用情况分配,因此与原来不同,所以得到的索引节点号也不同。
代码1-消息队列发送
// 发送
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
// int msgget(key_t key, int msgflg);
// int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
// ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
//定义消息结构体
struct msgbuf {
long mtype; //类型 /* message type, must be > 0 */
char mtext[256]; //数据 /* message data */
};
int main()
{
//1、定义发送消息类型
struct msgbuf sendBuf = {888,"this is message from quen"};
// 1.1获取key值
key_t key;
key = ftok(".",'m');
printf("key=%x\n",key);
//2、创建消息体
int msgId = msgget(key, IPC_CREAT|0777);
if(msgId == -1 )
{
printf(" 消息体创建失败!\n");
}
// 3、添加发送消息
msgsnd(msgId,&sendBuf,strlen(sendBuf.mtext),0);
printf("发送完毕 send over\n");
// 4、等接收端回应
//定义读取消息类型
struct msgbuf readBuf;//定义读取消息类型
memset(&readBuf,0,sizeof(struct msgbuf));//初始化
//4.1读取消息 读取接收端发过来的消息
msgrcv(msgId, &readBuf,sizeof(readBuf.mtext),988,0);
printf("来自接收端:%s\n",readBuf.mtext);
//控制消息队列
msgctl(msgId,IPC_RMID,NULL);//移除这个消息队列
return 0;
}
代码2-消息队列接收
//消息体接收
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
//int msgget(key_t key, int msgflg);
//int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
//定义消息结构体
struct msgbuf {
long mtype; //类型 /* message type, must be > 0 */
char mtext[256]; //数据 /* message data */
};
int main()
{
//1、定义消息类型
struct msgbuf readBuf; //定义消息类型
memset(&readBuf,0,sizeof(struct msgbuf));//初始化
//1.1获取消息队列名字
key_t key;
key = ftok(".",'m');
printf("key=%x\n",key);
//2、创建队列
int msgId = msgget(key, IPC_CREAT|0777);
if(msgId == -1 )
{
printf("消息队列创建失败!\n");
}
// 3、读取消息队列
msgrcv(msgId, &readBuf,sizeof(readBuf.mtext),888,0);
printf("读取发送端信息:%s\n",readBuf.mtext);
//4、接收完 回应发送端
//4.1定义发送消息 类型
struct msgbuf sendBuf = {988,"thank you for reach"};
//4.2发送
msgsnd(msgId,&sendBuf,strlen(sendBuf.mtext),0);
// 控制消息队列
msgctl(msgId,IPC_RMID,NULL);//移除这个消息队列
return 0;
}
运行结果:
代码3-sever端
//msg_sever.c
#include<stdio.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<time.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
// 用于创建一个唯一的key
#define MSG_FILE "/etc/passwd"
// 消息结构
struct msg_form {
long mtype;
char mtext[256];
};
int main()
{
int msqid;
key_t key;
struct msg_form msg;
// 获取key值
if((key = ftok(MSG_FILE,'z')) < 0)
{
perror("ftok error");
exit(1);
}
// 打印key值
printf("Message Queue - Server key is: %d.\n", key);
// 创建消息队列
if ((msqid = msgget(key,IPC_CREAT|0777)) == -1)
{
perror("msgget error");
exit(1);
}
// 打印消息队列ID及进程ID
printf("My msqid is: %d.\n", msqid);
printf("My pid is: %d.\n", getpid());
// 循环读取消息
for(;;)
{
msgrcv(msqid, &msg, 256, 888, 0);// 返回类型为888的第一个消息
printf("Server: receive msg.mtext is: %s.\n", msg.mtext);
printf("Server: receive msg.mtype is: %d.\n", msg.mtype);
msg.mtype = 999; // 客户端接收的消息类型
sprintf(msg.mtext, "hello, I'm server %d", getpid());
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
}
return 0;
}
代码4-client端
//msg_client.c
#include<stdio.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<time.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
// 用于创建一个唯一的key
#define MSG_FILE "/etc/passwd"
// 消息结构
struct msg_form {
long mtype;
char mtext[256];
};
int main()
{
int msqid;
key_t key;
struct msg_form msg;
// 获取key值
if ((key = ftok(MSG_FILE, 'z')) < 0)
{
perror("ftok error");
exit(1);
}
// 打印key值
printf("Message Queue - Client key is: %d.\n", key);
// 打开消息队列
if ((msqid = msgget(key, IPC_CREAT|0777)) == -1)
{
perror("msgget error");
exit(1);
}
// 打印消息队列ID及进程ID
printf("My msqid is: %d.\n", msqid);
printf("My pid is: %d.\n", getpid());
// 添加消息,类型为888
msg.mtype = 888;
sprintf(msg.mtext, "hello, I'm client %d", getpid());
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
// 读取类型为777的消息
msgrcv(msqid, &msg, 256, 999, 0);
printf("Client: receive msg.mtext is: %s.\n", msg.mtext);
printf("Client: receive msg.mtype is: %d.\n", msg.mtype);
return 0;
}
运行结果:
四、共享内存
消息队列中消息的读取和写入的过程,都会发生用户态和内核态之间的消息拷贝过程,会造成开销大的缺点。而共享内存则是多个进程共同映射到一块物理内存上读写。
优点: 读取数据效率高
缺点: 没有相应的同步机制,需要通过外部的信号量来控制同步。
- 使用共享内存三步走:
step1:创建物理内存,shmget()
step2:将进程的虚拟地址链接上物理内存,shmat
step3:使用完释放内存,shmdt()
然后使用struct作为一个变量写入,注意只有在共享内存可写的时候写入。
函数原型:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
//创建共享内存—>shmget()函数
int shmget(key_t key, size_t size, int shmflg);//成功则返回共享内存ID,出错返回-1
参数:key通过ftok()函数得到;size为共享内存的大小,为页数的整数倍
Shmflg是一组标志,创建一个新的共享内存,
IPC_CREAT标志表示共享内存存在则打开,
IPC_CREAT|IPC_EXCL存在则打开,不存在则创建一个新的共享内存。
操作共享内存—>shmctl()函数
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);//成功返回,出错返回-1
参数:shm_id是shmget函数返回的共享内存标志符。
cmd表示要采取的操作,他可以取下面三个值:
(IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds给出的值。
IPC_RMID:删除共享内存段。)
*buf 是一个结构体指针,它指向共享内存模式和访问权限的结构。shmid_ds结构至少包括以下成员
struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
挂接操作---> shmat()函数
创建共享存储段之后,将进程连接带它的地址空间
void *shmat(int shm_id, const void *shm_addr, int shmflg);//成功返回指向共享内存段的指针,出错返回-1
参数:shm_id是由shmget函数返回的共享内存标志。
shm_addr指定共享内存连接到当前进程中的地址位置,通常为0,表示让系统来选择共享内存的地址。
shm_flg是一组标志位,通常为0;
分离操作--->该操作不从系统中删除标志符合其数据结构,要显示调用shmctl(带命令IPC_RMID)才能删除它。
int shmdt(const void *shmaddr);//成功返回,出错返回-1
参数:shmaddr参数是之前调用shmat时返回的值。
相关函数
创建共享内存——>shmget() 函数
int shmget(key_t key, size_t size, int shmflg);
//成功返回共享内存的ID,出错返回-1
(1)第一个参数key是长整型(唯一非零),系统建立IPC通讯 ( 消息队列、 信号量和 共享内存) 时必须指定一个ID值。
通常情况下,该id值通过ftok函数得到,由内核变成标识符,要想让两个进程看到同一个信号集,只需设置key值不变就可以。
(2)第二个参数size指定共享内存的大小,它的值一般为一页大小的整数倍
(未到一页,操作系统向上对齐到一页,但是用户实际能使用只有自己所申请的大小)。
(3)第三个参数shmflg是一组标志,创建一个新的共享内存,将shmflg 设置了IPC_CREAT标志后,共享内存存在就打开。
而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的共享内存,如果共享内存已存在,返回一个错误。
操作共享内存———>shmctl()函数
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
//成功返回0,出错返回-1
(1)第一个参数,shm_id是shmget函数返回的共享内存标识符。
(2)第二个参数,cmd是要采取的操作,它可以取下面的三个值 :
IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
IPC_RMID:删除共享内存段
(3)第三个参数,buf是一个结构指针,它指向共享内存模式和访问权限的结构。 shmid_ds结构至少包括以下成员
struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
挂接操作———>shmat()函数
创建共享存储段之后,将进程连接到它的地址空间
void *shmat(int shm_id, const void *shm_addr, int shmflg);
//成功返回指向共享存储段的指针,出错返回-1
(1)第一个参数,shm_id是由shmget函数返回的共享内存标识。
(2)第二个参数,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
(3)第三个参数,shm_flg是一组标志位,通常为0
分离操作———>shmdt()函数
该操作不从系统中删除标识符和其数据结构,要显示调用shmctl(带命令IPC_RMID)才能删除它
int shmdt(const void *shmaddr);
//成功返回0,出错返回-1
(1)addr参数是以前调用shmat时的返回值
代码1-发送数据
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
//int shmget(key_t key, size_t size, int shmflg);
int main()
{
int shmid;
char *shmaddr;
// 获取key值
key_t key;
key = ftok(".",1);
//1、创建共享内存
shmid = shmget(key,1024*4,IPC_CREAT|0666);
if(shmid == -1){
printf("shmget noOk\n");
exit(-1);
}
//2、共享内存-挂接操作
shmaddr = shmat(shmid,0,0);
printf("挂接完成-shmat ok\n");
strcpy(shmaddr,"hahahahaha");//复制
sleep(5);
//3、共享内存-分离
shmdt(shmaddr);
//操作共享内存
shmctl(shmid, IPC_RMID, 0);
printf("quit\n");
return 0;
}
代码2-接收数据
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
//int shmget(key_t key, size_t size, int shmflg);
int main()
{
int shmid;
char *shmaddr;
// 获取key值
key_t key;
key = ftok(".",1);
// 1、创建
shmid = shmget(key,1024*4,0);
if(shmid == -1){
printf("shmget noOk\n");
exit(-1);
}
// 2、挂接
shmaddr = shmat(shmid,0,0);
printf("挂接完成-shmat ok!\n");
printf("data: %s\n:",shmaddr);
// 3、分离
shmdt(shmaddr);
printf("quit\n");
return 0;
}
五、信号
当一个进程收到信号的时候它有三种处理方式:
- 默认(缺省)处理
默认处理, 而大多数信号的默认处理方式为 退出进程 - 忽略信号
收到的信号不做任何处理 - 捕获信号
收到信号后会去执行信号处理函数,而这个函数可由程序猿自己编写
信号处理(signal 函数)
Linux提供了一个函数来设置信号的处理方式 : signal 函数
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:信号处理函数
参数:signum:要处理的信号//不能是SIGKILL和SIGSTOP
handler:SIG_IGN:忽略该信号。
SIG_DFL:采用系统默认方式处理信号。
自定义的信号处理函数指针
返回值:成功:设置之前的信号处理方式;失败:SIG_ERR
信号处理(sigaction 函数)
检查或修改指定信号的设置(或同时执行这两种操作)
#include <signal.h>
int sigsction (int sig, cont struct sigaction* act, struct sigaction* oact)
参数:
sig:要操作的信号,即上文信号中的展示;
act:指定新的信号处理方式;(要设置的对信号的新处理方式(传入参数))
oact:输出先前的信号处理方式(不为NULL时)。(原来对信号的处理方式(传出参数))
函数中的act和oact是sigaction结构体类型指针,sigaction结构体内容如下:
struct sigaction {
void (*sa_handler)(int);//旧的信号处理函数指针
void (*sa_sigaction)(int, siginfo_t *, void *);//新的信号处理函数指针
sigset_t sa_mask;//信号阻塞集
int sa_flags;//信号处理的方式
void (*sa_restorer)(void);//已弃用
}
sa_handler 信号处理函数
sa_mask 在处理该信号时可以暂时将sa_mask 指定的信号集搁置
sa_flags 指定一组修改信号行为的标志。 它由以下零个或多个的按位或组成
SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号
sa_restorer 是一个替代的信号处理程序,当设置SA_SIGINFO时才会用它。
发送信号 (kill函数)
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
它的作用把信号sig发送给进程号为pid的进程,成功时返回0。
通过kill指令发送信号
入门级别:通过kill 操作signal函数
命令格式:kill[参数][进程号]
linux信号参数值有64个如下:kill -9 2239 代表终止22239这个进程。
测试示例:
#include <signal.h>
#include <stdio.h>
//typedef void (*sighandler_t)(int);
// sighandler_t signal(int signum, sighandler_t handler);
//自定义信号处理函数
void handler(int signum)
{
printf("get signum=%d\n",signum);
switch(signum){
case 2:
printf("SIGINT\n");
break;
case 9:
printf("SIGKILL\n");
break;
case 10:
printf("SIGUSR1\n");
break;
}
printf("never quit\n");
}
int main()
{
signal(SIGQUIT,SIG_DFL);//系统默认方式处理SIGINT信号(强制退出)
signal(SIGKILL,SIG_IGN);//忽略SIGKILL信号
signal(SIGUSR1,handler);//用自定义信号处理函数handler处理SIGUSR1信号
while(1);
return 0;
}
说明:运行程序执行代码,然后通过ps命令查找该程序进程号,接着根据kill命令输入参数。
也可以定义一个命令发送函数完成以上命令发送如下:
#include <signal.h>
#include <stdio.h>
#include <sys/types.h>
int main(int argc ,char **argv)
{
int signum;
int pid;//定义进程号
// char cmd[128]={0};//构造syste()命令所需参数
signum = atoi(argv[1]);//要处理的信号值
pid = atoi(argv[2]);//进程号
printf("num=%d,pid=%d\n",signum,pid);
//发送信号
// 第一种方式
kill(pid, signum);
// 第二种方式构造一个system()命令,通过system(xx)执行
//1.构造syste()命令参数
// sprintf(cmd,"kill -%d %d",signum,pid);
// //2.system()执行
// system(cmd);
printf("send signal ok");
return 0;
}
通过函数指令发送信号
高级板:
信号量
信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
struct semaphore
{
int value;
pointer_PCB queue;
}
进程互斥
-
由于各进程要求共享资源,⽽且有些资源需要互斥使⽤,因此各进程间竞争使⽤这些资源,进程的这种关系为进程的互斥。
-
系统中某些资源⼀次只允许⼀个进程使⽤,称这样的资源为临界资源或互斥资源。
-
在进程中涉及到互斥资源的程序段叫临界区
-
特性:
IPC资源必须删除,否则不会自动清除,除非重启,所以 System V IPC 资源的生命周期随内核。
信号量集函数
semget函数 创建
创建一个新信号量或取得一个已有信号量
int semget(key_t key, int nsems, int semflg)
功能 :⽤来创建和访问⼀个信号量集
参数 :
key—>信号集的名字;
->不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,
->程序先通过调用semget()函数并提供一个键,再由系统生成一个相应的信号标识符(semget()函数的返回值),
->只有semget()函数才直接使用信号量键,所有其他的信号量函数使用由semget()函数返回的信号量标识符。
->如果多个程序使用相同的key值,key将负责协调工作。
nsems->:信号集中信号量的个数,它的值几乎总是1
semflg->:由九个权限标志构成,它们的⽤法和创建⽂件时使⽤的mode模式标志是⼀样的;
->当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。
->设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。
->而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。
返回值 :成功返回⼀个⾮负整数,即该信号集的标识码;失败返回-1
shmctl函数 控制
控制信号量的相关信息
int semctl(int semid, int semnum, int command, ...);
功能 :⽤于控制信号量集
参数:
semid-> 由semget返回的信号集标识码
semnum->信号集中信号量的序号
command->将要采取的动作,通常是下面两个值中的其中一个:
SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。
最后⼀个参数根据命令不同⽽不同,如果有第四个参数,它通常是一个union semum结构,定义如下:
union semun
{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
前两个参数与前面一个函数中的一样,
返回值:成功返回0;失败返回-1
semop函数 操作
对信号量组进行操作,改变信号量的值
int semop(int semid, struct sembuf *sops, unsigned nsops);
功能 :⽤来创建和访问⼀个信号量集
参数:
semid—>是该信号量的标识码,也就是semget()返回的信号量标识符.
sops—>是个指向⼀个结构数值的指针,sembuf结构的定义如下:
struct sembuf
{
short sem_num; // 除非使用一组信号量,否则它为0
short sem_op; // 信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,一个是+1,即V(发送信号)操作。
short sem_flg; // 通常为SEM_UNDO,使操作系统跟踪信号, 并在进程没有释放该信号量而终止时,操作系统释放信号量
};
nsops—>信号量的个数
返回值 :成功返回0;失败返回-1
- 当semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1;如果是引用一个现有的集合,则将num_sems指定为 0 。
- struct sembuf 结构体:
struct sembuf
{
short sem_num; // 信号量组中对应的序号,0~sem_nums-1
short sem_op; // 信号量值在一次操作中的改变量
short sem_flg; // IPC_NOWAIT, SEM_UNDO
}
其中 sem_op 是一次操作中的信号量的改变量:
- 若sem_op > 0,表示进程释放相应的资源数,将 sem_op 的值加到信号量的值上。如果有进程正在休眠等待此信号量,则换行它们。
- 若sem_op < 0,请求 sem_op 的绝对值的资源。
- sem_flg 指定IPC_NOWAIT,则semop函数出错返回EAGAIN。
- sem_flg 没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:
- 当相应的资源数可以满足请求,此信号量的semncnt值减1,该信号量的值减去sem_op的绝对值。成功返回;
- 此信号量被删除,函数smeop出错返回EIDRM;
- 进程捕捉到信号,并从信号处理函数返回,此情况下将此信号量的semncnt值减1,函数semop出错返回EINTR
- 如果相应的资源数可以满足请求,则将该信号量的值减去sem_op的绝对值,函数成功返回。
- 当相应的资源数不能满足请求时,这个操作与sem_flg有关。
- 若sem_op == 0,进程阻塞直到信号量的相应值为0:
- sem_flg指定IPC_NOWAIT,则出错返回EAGAIN。
- sem_flg没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:
- 信号量值为0,将信号量的semzcnt的值减1,函数semop成功返回;
- 此信号量被删除,函数smeop出错返回EIDRM;
- 进程捕捉到信号,并从信号处理函数返回,在此情况将此信号量的semncnt值减1,函数semop出错返回EINTR
- 当信号量已经为0,函数立即返回。
- 如果信号量的值不为0,则依据sem_flg决定函数动作:
示例代码1:信号量通信
实验说明:在main函数中调用semget()来创建一个信号量,该函数将返回一个信号量标识符,保存于semid中,然后以后的函数就使用这个标识符来访问信号量。
对这个信号量进行初始化操作,(实现子进程放钥匙,父进程取钥匙,然后开锁,再然后父把钥匙返还)。
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
//在main函数中调用semget()来创建一个信号量,该函数将返回一个信号量标识符,
//保存于semid中,然后以后的函数就使用这个标识符来访问信号量。
//int semget(key_t key, int nsems, int semflg);
//int semctl(int semid, int semnum, int cmd, ...);
//信号量参数值
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) */
};
//执行动作(取钥匙)
void pGetKey(int id)
{
struct sembuf set;
set.sem_num = 0;
set.sem_op = -1;
set.sem_flg=SEM_UNDO;
// int semop(int semid, struct sembuf *sops, unsigned nsops);
semop(id, &set ,1);//操作信号量
printf("取钥匙!\n");
}
//执行动作(放钥匙)
void vPutBackKey(int id)
{
struct sembuf set;
set.sem_num = 0;
set.sem_op = 1;
set.sem_flg=SEM_UNDO;
semop(id, &set ,1);
printf("放钥匙进去!\n");
}
int main(int argc, char const *argv[])
{
int semid;
// 获取key值
key_t key;
key = ftok(".",2);
//1、创建信号量 //1 代表;信号量集合中有一个信号量
semid = semget(key, 1, IPC_CREAT|0666);//获取/创建信号量
union semun initsem;//定义信号量的参数
initsem.val = 0;//状态标记为0;
//2、操作信号量 (初始化 信号量) //操作第0个信号量
semctl(semid, 0, SETVAL, initsem);//初始化信号量
//SETVAL设置信号量的值,设置为inisem
//3、创建子进程
int pid = fork();
if(pid > 0)//父进程
{
//3.2父进程拿钥匙开锁
pGetKey(semid);//如果先运行父进程,父进程是无法取得钥匙的(因为子进还没往里放钥匙),就会一直阻塞这里,
//只有运行子进程把钥匙放进去后,父进程才能拿到钥匙。
//父进程取完钥匙后initsem.val状态标记的值由1变成0。(set.sem_op = -1;做了-1操作)
printf("this is father\n");
//3.3锁放回去
vPutBackKey(semid);//父进程用完钥匙后,又把钥匙放进去。
// initsem.val状态标记的值由0再次变成1。(set.sem_op = 1;做了+1操作)
//3.4销毁信号量
semctl(semid,0,IPC_RMID);
}
//3.1子进程放钥匙进去
else if(pid == 0)//子进程
{
printf("this is child\n");
//放一把钥匙后initsem.val状态标记的值由0变成1
vPutBackKey(semid);//执行这个程序后 initsem.val状态标记的值由0变成1;
}
else
{
printf("fork error\n");
}
return 0;
}
示例代码2:信号量通信
#include<stdio.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<time.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
// 联合体,用于semctl初始化
union semun
{
int val; //for SETVAL/
struct semid_ds *buf;
unsigned short *array;
};
// 初始化信号量
int init_sem(int sem_id, int value)
{
union semun tmp;
tmp.val = value;
if(semctl(sem_id, 0, SETVAL, tmp) == -1)
{
perror("Init Semaphore Error");
return -1;
}
return 0;
}
// P操作:
// 若信号量值为1,获取资源并将信号量值-1
// 若信号量值为0,进程挂起等待
int sem_p(int sem_id)
{
struct sembuf sbuf;
sbuf.sem_num = 0; //序号/
sbuf.sem_op = -1; //P操作/
sbuf.sem_flg = SEM_UNDO;
if(semop(sem_id, &sbuf, 1) == -1)
{
perror("P operation Error");
return -1;
}
return 0;
}
// V操作:
// 释放资源并将信号量值+1
// 如果有进程正在挂起等待,则唤醒它们
int sem_v(int sem_id)
{
struct sembuf sbuf;
sbuf.sem_num = 0; //序号/
sbuf.sem_op = 1; //V操作/
sbuf.sem_flg = SEM_UNDO;
if(semop(sem_id, &sbuf, 1) == -1)
{
perror("V operation Error");
return -1;
}
return 0;
}
// 删除信号量集
int del_sem(int sem_id)
{
union semun tmp;
if(semctl(sem_id, 0, IPC_RMID, tmp) == -1)
{
perror("Delete Semaphore Error");
return -1;
}
return 0;
}
int main()
{
int sem_id; // 信号量集ID
key_t key;
pid_t pid;
// 获取key值
if((key = ftok(".", 'z')) < 0)
{
perror("ftok error");
exit(1);
}
// 创建信号量集,其中只有一个信号量
if((sem_id = semget(key, 1, IPC_CREAT|0666)) == -1)
{
perror("semget error");
exit(1);
}
// 初始化:初值设为0资源被占用
init_sem(sem_id, 0);
if((pid = fork()) == -1)
perror("Fork Error");
else if(pid == 0) //子进程/
{
sleep(2);
printf("Process child: pid=%d\n", getpid());
sem_v(sem_id); //释放资源/
}
else //父进程/
{
sem_p(sem_id); //等待资源/
printf("Process father: pid=%d\n", getpid());
sem_v(sem_id); //释放资源/
del_sem(sem_id); //删除信号量集/
}
return 0;
}
上面的例子如果不加信号量,则父进程会先执行完毕。这里加了信号量让父进程等待子进程执行完以后再执行。
综合练习
下面这个例子,使用了【共享内存+信号量+消息队列】的组合来实现服务器进程与客户进程间的通信。
- 共享内存用来传递数据;
- 信号量用来同步;
- 消息队列用来 在客户端修改了共享内存后 通知服务器读取。
server.c
#include <stdio.h>
#include <memory> // shared memory
#include <semaphore> // semaphore
#include <queue> // message queue
// 消息队列结构
struct msg_form {
long mtype;
char mtext;
};
// 联合体,用于semctl初始化
union semun
{
int val; /for SETVAL/
struct semid_ds buf;
unsigned short *array;
};
// 初始化信号量
int init_sem(int sem_id, int value)
{
union semun tmp;
tmp.val = value;
if(semctl(sem_id, 0, SETVAL, tmp) == -1)
{
perror("Init Semaphore Error");
return -1;
}
return 0;
}
// P操作:
// 若信号量值为1,获取资源并将信号量值-1
// 若信号量值为0,进程挂起等待
int sem_p(int sem_id)
{
struct sembuf sbuf;
sbuf.sem_num = 0; /序号/
sbuf.sem_op = -1; /P操作/
sbuf.sem_flg = SEM_UNDO;
if(semop(sem_id, &sbuf, 1) == -1)
{
perror("P operation Error");
return -1;
}
return 0;
}
// V操作:
// 释放资源并将信号量值+1
// 如果有进程正在挂起等待,则唤醒它们
int sem_v(int sem_id)
{
struct sembuf sbuf;
sbuf.sem_num = 0; /序号/
sbuf.sem_op = 1; /V操作/
sbuf.sem_flg = SEM_UNDO;
if(semop(sem_id, &sbuf, 1) == -1)
{
perror("V operation Error");
return -1;
}
return 0;
}
// 删除信号量集
int del_sem(int sem_id)
{
union semun tmp;
if(semctl(sem_id, 0, IPC_RMID, tmp) == -1)
{
perror("Delete Semaphore Error");
return -1;
}
return 0;
}
// 创建一个信号量集
int creat_sem(key_t key)
{
int sem_id;
if((sem_id = semget(key, 1, IPC_CREAT|0666)) == -1)
{
perror("semget error");
exit(-1);
}
init_sem(sem_id, 1); /初值设为1资源未占用/
return sem_id;
}
int main()
{
key_t key;
int shmid, semid, msqid;
char shm;
char data[] = "this is server";
struct shmid_ds buf1; /用于删除共享内存/
struct msqid_ds buf2; /用于删除消息队列/
struct msg_form msg; /消息队列用于通知对方更新了共享内存/
// 获取key值
if((key = ftok(".", 'z')) < 0)
{
perror("ftok error");
exit(1);
}
// 创建共享内存
if((shmid = shmget(key, 1024, IPC_CREAT|0666)) == -1)
{
perror("Create Shared Memory Error");
exit(1);
}
// 连接共享内存
shm = (char*)shmat(shmid, 0, 0);
if((int)shm == -1)
{
perror("Attach Shared Memory Error");
exit(1);
}
// 创建消息队列
if ((msqid = msgget(key, IPC_CREAT|0777)) == -1)
{
perror("msgget error");
exit(1);
}
// 创建信号量
semid = creat_sem(key);
// 读数据
while(1)
{
msgrcv(msqid, &msg, 1, 888, 0); /读取类型为888的消息/
if(msg.mtext == 'q') /quit - 跳出循环/
break;
if(msg.mtext == 'r') /read - 读共享内存/
{
sem_p(semid);
printf("%s\n",shm);
sem_v(semid);
}
}
// 断开连接
shmdt(shm);
/删除共享内存、消息队列、信号量/
shmctl(shmid, IPC_RMID, &buf1);
msgctl(msqid, IPC_RMID, &buf2);
del_sem(semid);
return 0;
}
client.c
#include <stdio.h>
#include <memory> // shared memory
#include <semaphore> // semaphore
#include <queue> // message queue
// 消息队列结构
struct msg_form {
long mtype;
char mtext;
};
// 联合体,用于semctl初始化
union semun
{
int val; /for SETVAL/
struct semid_ds buf;
unsigned short *array;
};
// P操作:
// 若信号量值为1,获取资源并将信号量值-1
// 若信号量值为0,进程挂起等待
int sem_p(int sem_id)
{
struct sembuf sbuf;
sbuf.sem_num = 0; /序号/
sbuf.sem_op = -1; /P操作/
sbuf.sem_flg = SEM_UNDO;
if(semop(sem_id, &sbuf, 1) == -1)
{
perror("P operation Error");
return -1;
}
return 0;
}
// V操作:
// 释放资源并将信号量值+1
// 如果有进程正在挂起等待,则唤醒它们
int sem_v(int sem_id)
{
struct sembuf sbuf;
sbuf.sem_num = 0; /序号/
sbuf.sem_op = 1; /V操作/
sbuf.sem_flg = SEM_UNDO;
if(semop(sem_id, &sbuf, 1) == -1)
{
perror("V operation Error");
return -1;
}
return 0;
}
int main()
{
key_t key;
int shmid, semid, msqid;
char shm;
struct msg_form msg;
int flag = 1; /while循环条件/
// 获取key值
if((key = ftok(".", 'z')) < 0)
{
perror("ftok error");
exit(1);
}
// 获取共享内存
if((shmid = shmget(key, 1024, 0)) == -1)
{
perror("shmget error");
exit(1);
}
// 连接共享内存
shm = (char*)shmat(shmid, 0, 0);
if((int)shm == -1)
{
perror("Attach Shared Memory Error");
exit(1);
}
// 创建消息队列
if ((msqid = msgget(key, 0)) == -1)
{
perror("msgget error");
exit(1);
}
// 获取信号量
if((semid = semget(key, 0, 0)) == -1)
{
perror("semget error");
exit(1);
}
// 写数据
printf("\n");
printf("* IPC \n");
printf(" Input r to send data to server. \n");
printf(" Input q to quit. \n");
printf("\n");
while(flag)
{
char c;
printf("Please input command: ");
scanf("%c", &c);
switch(c)
{
case 'r':
printf("Data to send: ");
sem_p(semid); /访问资源/
scanf("%s", shm);
sem_v(semid); /释放资源/
/清空标准输入缓冲区/
while((c=getchar())!='\n' && c!=EOF);
msg.mtype = 888;
msg.mtext = 'r'; /发送消息通知服务器读数据/
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
break;
case 'q':
msg.mtype = 888;
msg.mtext = 'q';
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
flag = 0;
break;
default:
printf("Wrong input!\n");
/清空标准输入缓冲区*/
while((c=getchar())!='\n' && c!=EOF);
}
}
// 断开连接
shmdt(shm);
return 0;
}
注意:当scanf()输入字符或字符串时,缓冲区中遗留下了\n,所以每次输入操作后都需要清空标准输入的缓冲区。但是由于 gcc 编译器不支持fflush(stdin)(它只是标准C的扩展),所以我们使用了替代方案:
while((c=getchar())!='\n' && c!=EOF);
总结
五种通讯方式总结
1.管道:速度慢,容量有限,只有父子进程能通讯
2.FIFO:任何进程间都能通讯,但速度慢
3.消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
4.信号量:不能传递复杂消息,只能用来同步
5.共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存。