LinuxIPC问题
IPC
管道
单向通信通道。只适用于父子进程间通信,在进程间实现双向数据传输必须创建两个管道。
pipe
#include <unistd.h>
int pipe(int pipefd[2]);
其中pipefd
是用于通信的一对文件描述符,pipefd[0]
用于读,pipefd[1]
用于写。
程序示例:
- 用pipe函数创建两个管道:pipe1和pipe2
- 调用fork创建子进程
- 父进程用pipe1写数据( 关闭pipe1的读端口),pipe2读数据(关闭pipe2的写端口)
- 子进程用pipe1读数据( 关闭pipe1的写端口),pipe2写数据(关闭pipe2的读端口)
- 父子进程各自使用未关闭的端口进行通信
#include <unistd.h>
#include <iostream>
using namespace std;
int main(int argc,char **argv)
{
int pipe1[2],pipe2[2];
char pstr[]="parent data";
char cstr[]="child data";
char buf[100];
if(pipe(pipe1)<0||pipe(pipe2)<0)
cout<<"pipe error"<<endl;
pid_t pid=fork();
if(pid>0)
{
//父进程,用管道1写数据,管道2读数据
close(pipe1[0]);//关闭pipe1读端口
close(pipe2[1]);//关闭pipe2写端口
write(pipe1[1],pstr,sizeof(pstr));
if(read(pipe2[0],buf,100)>0)
cout<<"parent received:"<<buf<<endl;
}
else if(pid==0)
{
//子进程用管道1读数据,管道2写数据
close(pipe1[1]);//关闭pipe1写端口
close(pipe2[0]);//关闭pipe2读端口
if(read(pipe1[0],buf,100)>0)
cout<<"child received:"<<buf<<endl;
write(pipe2[1],cstr,sizeof(cstr));
exit(0);
}
else
cout<<"fork error"<<endl;
return 0;
}
命名管道
- 命名管道与一个路径名相关联,以文件形式存在于文件系统中
- 命名管道的文件名只是便于其他进程引用该管道,文件名所对应的文件中没有数据(只能以阻塞模式使用)
- 命名管道可以在无父子关系的进程间通信
关于第二点,补充一下,对于命令管道来说,其和普通管道的IO操作基本是一样的,主要区别在于对于命令管道,必须使用一个open
函数来显式建立连接到管道的通道。一般来说它总是处于阻塞状态,也就是说,如果打开时设置只读,那么读取的进程会一直阻塞,直到其它进程 打开这个管道并向其中写入数据。同理,当一个进程写入而没有进程读取时也会被阻塞。也可以open
时调用O_NONBLOCK
关闭默认的阻塞动作。
mkfifo
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
pathname
-管道名称,绝对路径名,mode
-文件的权限(详情见另一篇博文)。
下面是一个命名管道的例子,发现这个管道只能使用一次,如果第二次使用,就会出错。
- 写进程使用mkfifo创建命名管道
- 写进程调用open以写阻塞方式打开管道
- 读进程调用open以读阻塞方式打开管道
- 写进程调用write写入数据
- 读进程调用read读出数据
可以看到,当客户端读了之后,服务器才结束。
//
// Created by prime on 17-6-20.
//
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <iostream>
#include <cstring>
using namespace std;
#define path "/home/prime/ClionProjects/tcp/tmp"
int main()
{
char s_data[]={"server:hello!"};
if(mkfifo(path,S_IRWXU)==-1)
{
cout<<"error!";
exit(0);
}
int fd=open(path,O_WRONLY);
if(fd==-1)
{
cout<<"open fail!";
return 0;
}
ssize_t num=write(fd,s_data,strlen(s_data));
return 0;
}
//
// Created by prime on 17-6-20.
//
#include <unistd.h>
#include <fcntl.h>
#include <iostream>
using namespace std;
#define path "/home/prime/ClionProjects/tcp/tmp"
int main()
{
int fd=open(path,O_RDONLY);
char buff[1024];
if(fd==-1)
{
cerr<<"open fail";
return 0;
}
ssize_t num=read(fd,buff,1024);
cout<<"read "<<num<<" bytes\n";
write(1,buff,num);
return 0;
}
unix域socket
- UNIX域协议不是真正的网络协议
- UNIX域协议提供同一台机器的进程间通信
- UNIX域socket是双向通道
- UINIX域socket分为命名和非命名两种,分别和命名管道和非命名管道类似
命名unix域socket
struct socketaddr_un{
short int sun_family; //AF_UNIX
char sun_path[104]; //文件名的绝对路径
};
/*UNIX域协议使用路径名标识服务器和客户端
服务器调用函数bind绑定一个UNIX域socket时以该路径名创建一个文件
*/
特点
- 服务器可以接收多个客户端连接请求
- 客户端调用函数connect与服务器连接
- connect使用的socket应该是已打开的UNIX域socket
- 客户端必须拥有打开socket地址所指文件权限
- 监听socket的连接队列满时connect立刻返回错误
具体编程流程如下~
服务器端
- 服务器调用socket创建UNIX域socket
- 服务器调用bind绑定UNIX域socket和指定地址
- 服务器调用listen转化为侦听socket
- 服务器调用accept接收客户端连接
//
// Created by prime on 17-6-20.
//
#include <iostream>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
using namespace std;
#define UNIX_SOCKET "/home/prime/ClionProjects/tcp/unix_socket"
int main(int argc,char *argv)
{
int sockfd=socket(AF_UNIX,SOCK_STREAM,0);
//服务器调用bind绑定UNIX域socket和指定的地址
struct sockaddr_un addr;
bzero(&addr,sizeof(addr));
unlink(UNIX_SOCKET);
addr.sun_family=AF_UNIX;
sprintf(addr.sun_path,"%s",UNIX_SOCKET);
bind(sockfd,(struct sockaddr *)&addr,sizeof(addr));
//服务器调用listen转化为侦听socket
listen(sockfd,5);
//服务器调用accept接收客户端连接
while(1)
{
int new_fd=accept(sockfd,NULL,NULL);
if(new_fd==-1)
{
cout<<"accept error"<<endl;
continue;
}
int n;
do
{
char buf[512];
n=recv(new_fd,buf,512,0);
if(n>0)
{
buf[n]=0;
cout<<"recv:"<<buf<<endl;
n=send(new_fd,buf,n,0);
}
}while(n>0);
close(new_fd);
}
close(sockfd);
return 0;
}
客户端
- 客户端创建UNIX域socket(同服务器)
- 客户端调用connect连接服务器
- 客户端和服务器利用UNIX域socket进行通信
//
// Created by prime on 17-6-20.
//
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <sys/un.h>
using namespace std;
#define UNIX_SOCKET "/home/prime/ClionProjects/tcp/unix_socket"
int main(int argc,char **argv)
{
//客户端创建UNIX域socket(同服务器)
int sockfd=socket(AF_UNIX,SOCK_STREAM,0);
//客户端调用connect连接服务器
struct sockaddr_un addr;
char path[104]=UNIX_SOCKET;
int len;
bzero(&addr,sizeof(addr));
addr.sun_family=AF_UNIX;
sprintf(addr.sun_path,"%s",UNIX_SOCKET);
len=strlen(addr.sun_path)+sizeof(addr.sun_family);
if(connect(sockfd,(struct sockaddr *)&addr,len)==-1)
{
cout<<"connect error"<<endl;
return 1;
}
do
{
char buf[512];
int n;
cout<<">";
fgets(buf,512,stdin);
if(send(sockfd,buf,strlen(buf),0)==-1)
{
cout<<"send error"<<endl;
break;
}
if((n=recv(sockfd,buf,512,0))<=0)
{
cout<<"recv error"<<endl;
break;
}
else
{
buf[n]=0;
cout<<"recv:"<<buf<<endl;
}
}while(1);
close(sockfd);
return 0;
}
非命名域socket
#include <sys/types.h>
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int sv[2]);
创建两个UNIX域socket,并连接在一起。
参数如下:
family
-必须是AF_UNIX
type
-SOCK_STREAM
或SOCK_DGRAM
protocol
-0
sv
-存储已创建的socket(描述符)
特点
- socket是无名的
- socket是全双工的
- 通信前不需要连接
- 通常在父子进程间通信使用
socketpair
下面程序在子进程中用fd[1]
,父进程中用fd[0]
,子进程实现了字符变大写后返回给父进程的功能。
//
// Created by prime on 17-6-20.
//
#include <iostream>
#include <sys/socket.h>
#include <unistd.h>
using namespace std;
int main(int argc,char **argv)
{
int fd[2];
char buff[64];
socketpair(AF_UNIX,SOCK_STREAM,0,fd);
pid_t pid=fork();
if(pid==0)
{
close(fd[0]);
recv(fd[1],buff,1,0);
buff[0]=toupper(buff[0]);
send(fd[1],buff,1,0);
exit(0);
} else if(pid>0)
{
close(fd[1]);
send(fd[0],"a",1,0);
recv(fd[0],buff,1,0);
cout<<"res:"<<buff[0];
exit(0);
}
return 0;
}
消息队列
消息队列就是一个消息的链表。对消息队列有写权限的进程可以向中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读走消息。
struct msgbuf {
long mtype; //消息类型
char mtext[20]; //消息数据
};
这个结构只是一个模板,可以自定义自己的消息结构。
待续~
内存映像文件
不同进程通过映射同一个普通文件实现共享内存 ,提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作。
特点:
- 共享内存的一种实现;
- 提供了不同于一般对普通文件的访问方式;
- 最高效的IPC手段之一;
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
/*fd为即将映射到进程空间的文件描述字,一般由open()返回
length是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起
prot 参数指定共享内存的访问权限。
offset参数一般设为0,表示从文件头开始映射。
参数addr指定文件应被映射到进程空间的起始地址
返回值为最后文件映射到进程空间的地址
*/
int munmap(void *addr, size_t length);
/*该调用在进程地址空间中解除一个映射关系
addr是调用mmap()时返回的地址,
len是映射区的大小。
当映射关系解除后,对原来映射地址的访问将导致段错误发生。
*/
#include <sys/mman.h>
int msync(void *addr, size_t length, int flags);
/*该调用实现磁盘上文件内容与共享内存区的内容同步
addr是调用mmap()时返回的地址
len是映射区的大小
flags为同步标志
一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。
*/
共享内存
进程间需要共享的数据被放在一个叫做IPC共享内存区域的地方,所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中去。
特点:
- 共享内存的一种实现;
- 最高效的IPC手段之一;
- 通过
shmget
获得或创建一个IPC共享内存区域时在特殊文件系统shm
中,创建并打开一个同名文件。
创建共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
/*三个参数key、nbytes和flags的含义与消息队列中的系统调用msgget类似。
key取值IPC_PRIVATE时,新创建的共享内存段的关键字由系统分配。
shmget创建共享内存段成功时,初始化相应的控制信息,返回该共享段的描述字ID。
*/
映射到虚拟空间
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
/*shmat将标识字为shmid的共享内存段映射到由shmaddr参数指定的进程虚拟地址空间。
如果不关心映射内存的地址,则可以置shmaddr为0,让系统选择一个可用地址。
shmat调用成功后返回共享内存段在进程虚拟地址空间的首地址。
*/
int shmdt(const void *shmaddr);
/*shmaddr是相应shmdt调用的返回值。
shmdt调用成功时,内存段的访问计数减1,返回值为0。
*/
共享内存段控制
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
/*shmid为标识符
cmd为控制字
SHM_LOCK:将共享段锁定在内存,禁止换出(超级用户才具有此权限);
SHM_UNLOCK:与LOCK相反(超级用户才具有此权限);
IPC_RMID、ICP_STAT和IPC_SET:类似于msgctl中的定义,其中IPC_RMID标志所对应的存储段为“可释放”。
sbuf为指向共享内存段控制结构指针。
*/
信号量(信号灯)
信号灯与其他IPC方式不大相同,它主要提供对进程间共享资源访问控制机制。相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志。除了用于访问控制外,还可用于进程同步。
大体上,信号量分为两种:
- 二值信号灯
最简单的信号灯形式,信号灯的值只能取0或1,类似于互斥锁。 - 计数信号灯
信号灯的值可以取任意非负值(当然受内核本身的约束)。
创建信号量
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
/*key为信号灯组关键字
当key为IPC_PRIVATE时,信号灯组关键字由系统选择
nsems为信号灯个数
flags为操作标志
flags决定信号灯组的创建方式和权限,其取值和含义与msgget中的flags类似
semget调用成功时,初始化相应的控制块信息,返回信号灯组标识数。
*/