开门见山,哲学三问!Unix域套接字是什么?为什么会存在Unix域套接字?如何用Unix域套接字?
- Unix域套接字是什么,为什么会有Unix用于套接字?
Linux系统中不同进程进行通信的手段很多,套接字通信就是其中一种,传统我们所说的套接字是网络套接字,是实现不同主机之间的进程间通信的,需要有五元组,打包拆包、计算校验和、维护序号和应答等数据及操作保证数据的可靠传输。但是我们想用套接字实现同一主机不同进程之间的通信,操作系统下进程间通信本来就很可靠,不需要以上一系列数据及操作也可以实现数据的可靠传输,况且太多的校验会影响传输效率。所以Unix域套接字实现了这一功能!在数据交互上只负责copy数据,不需要执行协议处理,没有网络报头,校验和,不发送确认等,而且提供数据报和数据流两种接口。数据报服务是可靠的,既不会丢失报文也不会传输错误。Unix域套接字有两种,匿名Unix域套接字和命名的Unix域套接字
- 匿名Unix域套接字
匿名Unix域套接字,和管道想似以套接字对的形式创建,不同于管道的是,这对套接字都对读写开放,是全双工,可以使用他们面向网络的域套接字接口或者使用socketpair函数创建一对无命名,相互连接的Unix域套接字。需要注意的是,匿名Unix套接字对,虽然都对读写开放,但是,通过fd[0]写入数据,再通过fd[0]读数据,会阻塞,只能通过fd[1]来读,当fd[1]中没写入数据时,通过fd[0]进行读的话,也会阻塞。
创建匿名Unix域套接字
#include <sys/types.h>
#include <sys/socket.h>
int socketpair(int d, int type, int protocol, int sv[2]);
d:套接口的域,一般为AF_UNIX和网络套接字中我们熟悉的AF_INET是都是socket域。
type:套接口类型,Unix域套接字也支持流协议(SOCK_STREAM)和数据报协议SOCK_DGRAM。
protocol:使用的协议,对于socketpair函数,protocol参数必须为0。(目前网上没找到原因,可能是规定吧!)
sv[2]:指向存储文件描述符的指针:就是进行通信的套接字对。
匿名Unix域套接字搭配其他进程间通信方式使用
以消息队列为例,将消息队列的ID与套接字队中任意一个绑定在一块,将其中一个套接字注册到IO复用中进行可读事件检测,当消息队列中有来自其他进程的数据时,将消息队列中的数据读出,写入套接字缓冲区,这时就会触发IO复用的可读事件。然后就可以接收到其他进程的数据了。这样和单纯使用消息队列的区别就在于可以通过IO复用机制监测普通进程件的通信事件!下面是想法的一个实现,可以参考。
Main.h
#pragma once
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<iostream>
#include<signal.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<unistd.h>
#include<fcntl.h>
#include<pthread.h>
using namespace std ;
#define BUF_SIZE (1024*4)
#define EPOLL_MAX 1024
#define NUM 4
#define KEY 0x123
//将消息队列的id和unix域套接字关联起来
struct node {
//unix 域套接字
int fd ;
//消息队列的id号
int qid ;
} ;
//消息数据
struct Msg{
long type ;
char text[BUF_SIZE] ;
} ;
namespace MsgId {
static int msgid[NUM] ;
}
namespace EpFd {
static int epfd ;
}
//初始化epoll,返回epoll句柄
int initEpollFd() ;
//添加fd到epoll树上
int addFd(int epfd, int fd) ;
//接收到消息队列的消息后写到套接字缓冲区中
void* recvData(void* data) ;
int runEpoll(int epfd) ;
void sig_handle(int signo) ;
Main.cpp
#include"Main.h"
int main() {
signal(SIGINT, sig_handle) ;
pthread_t tid[NUM] ;
struct node data[NUM] ;
int qid[NUM] ;
initEpollFd() ;
int fd[2] ;
int epfd = initEpollFd() ;
EpFd:: epfd = epfd ;
if(epfd < 0) {
cout << __FILE__ << __LINE__ << endl ;
exit(1) ;
}
for(int i = 0; i < NUM; i++) {
//创建消息队列
if((qid[i] = msgget(KEY+i, IPC_CREAT|IPC_EXCL|0666))<0) {
cout << __FILE__ << __LINE__ << endl ;
exit(1) ;
}
//创建unix域套接字
if(socketpair(AF_UNIX, SOCK_DGRAM, 0, fd) < 0) {
cout << __FILE__ << __LINE__ << endl ;
exit(1) ;
}
MsgId::msgid[i] = qid[i] ;
//将套接字和消息队列的id进行关联
data[i].fd = fd[1];
data[i].qid = qid[i] ;
int ret = addFd(epfd, fd[0]) ;
if(ret < 0) {
return 0;
}
//将套接字加入epoll中
pthread_create(&tid[i], NULL, recvData, &data[i]) ;
}
runEpoll(epfd) ;
return 0;
}
//启动epoll进行监听
int runEpoll(int epfd) {
char buf[BUF_SIZE] ;
struct epoll_event es[EPOLL_MAX] ;
while(1) {
int ret = epoll_wait(epfd, es, NUM, -1) ;
if(ret < 0) {
cout << __FILE__ << __LINE__ <<endl;
exit(1) ;
}
cout << ret << endl ;
for(int i=0; i< NUM; i++) {
int fds = es[i].data.fd ;
//若为可读事件,就将读取的数据打印到屏幕上
if(es[i].events&EPOLLIN) {
if(read(fds, buf, sizeof(buf)) < 0) {
cout << __FILE__ <<__LINE__ <<endl ;
exit(1) ;
}
cout << "get id from :"<< MsgId::msgid[i] << " data" << buf <<endl ;
}
}
}
}
//线程等待数据的到来
void* recvData(void* args) {
node data = *(node*)args ;
Msg msg ;
for( ; ;) {
int ret = msgrcv(data.qid, (void*)&msg, sizeof(msg)-sizeof(long), 0, MSG_NOERROR) ;
if(ret < 0) {
printf("%s %d\n", __FILE__, __LINE__) ;
return 0 ;
}
if(write(data.fd, msg.text, sizeof(msg.text)) < 0) {
printf("%s %d\n", __FILE__, __LINE__) ;
return 0 ;
}
cout << msg.text <<endl ;
}
}
//向epoll树上添加fd
int addFd(int epfd, int fd) {
struct epoll_event ev ;
ev.events = EPOLLIN|EPOLLET ;
ev.data.fd = fd ;
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) ;
if(ret < 0) {
cout << __FILE__ << __LINE__ << endl ;
exit(1) ;
}
return 1 ;
}
//初始化epoll句柄
int initEpollFd() {
int epfd = epoll_create(EPOLL_MAX) ;
if(epfd < 0) {
cout << __FILE__ << __LINE__ <<endl ;
return -1 ;
}
struct epoll_event ev ;
//检测可读事件
ev.events = EPOLLIN ;
return epfd ;
}
//释放掉所有资源
void sig_handle(int signo) {
//接收到中断信号,将所有资源释放掉
if(signo == SIGINT) {
for(int i= 0; i<NUM ;i++) {
msgctl(MsgId::msgid[i], IPC_RMID, 0) ;
}
close(EpFd::epfd) ;
}
}
test.h
#pragma once
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<thread>
#include<iostream>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<unistd.h>
#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/stat.h>
#include<signal.h>
using namespace std ;
#define BUF_SIZE (1024*4)
//消息数据
struct Msg{
long type ;
char text[BUF_SIZE] ;
} ;
namespace MsgId {
static int msgid ;
}
void sig_handle(int signo) ;
#include"test.h"
#include<stdlib.h>
#define KEY 0x123
int main(int argc, char** argv) {
signal(SIGINT, sig_handle);
if(argc != 2) {
printf("%s %d\n", __FILE__, __LINE__) ;
return 1 ;
}
key_t key = strtol(argv[1],NULL, 0) ;
//获取消息队列的句柄
int msgid = msgget(key, 0) ;
if(msgid < 0) {
printf("%s %d\n", __FILE__, __LINE__) ;
exit(1) ;
}
Msg msg ;
while(1) {
cout <<"请输入要发送的消息:" ;
cin >> msg.text ;
//获取pid,在接收消息时可以用到
int ret = msgsnd(msgid, (void*)&msg, sizeof(msg)-sizeof(long), 0) ;
if(ret < 0) {
printf("%s %d\n", __FILE__, __LINE__) ;
exit(1) ;
}
}
return 0;
}
void sig_handle(int signo) {
if(signo == SIGINT) {
msgctl(MsgId::msgid, IPC_RMID, 0) ;
}
printf("已中断\n") ;
}
先执行Main,然后执行test并指定消息队列key值
运行结果 :
以上是一个简单应用,当我们想要使用IO复用机制检测各种进程间通信的事件时,可以将它们的ID与Unix域套接字绑定起来。然后将套接字注册到IO事件检测表(epoll、poll,或者select中)中就行!
以上是一个简单的应用,好玩的还在后面呢!
- 命名Unix域套接字
具体用法详述
- 命名Unix套接字
socketpair可以创建一对相互连接的套接字,但是每个套接字没有名字,在无关进程中也是无法使用它们。所以要想和其他进程进行通信,就需要定一个众所周知的名字。即类似于消息队列那样的操作,创建一个特殊的文件作为地址,让其他进程通过连接地址找到服务进程,Linux中通过以下结构维护地址:
struct sockaddr_un {
sa_family_t sun_family ;
char sun_path[108] ;
};
sun_path包括一个路径名,一般为绝对路径,当我们将一个地址绑定到一个Unix域套接字时,系统会用该路径名创建一个S_IFSOCK类型的文件。该文件向客户进程告示套接字的名称。该文件不能被打开,也不能有应用程序用于通信。如果我们试图绑定同一地址,该文件已经存在,那么绑定请求就会失败,当关闭套接字时并不会自动删除该文件,所以必须确保在程序退出前,对文件执行连接解除操作。
绑定简单例子
#include <stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<sys/un.h>
#include<stddef.h>
int main()
{
int fd ,size ;
struct sockaddr_un un ;
un.sun_family = AF_UNIX ;
//设置一个路径名
strncpy(un.sun_path, "test",sizeof(un.sun_path)-1) ;
if((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
printf("socket failed") ;
return 1;
}
if(bind(fd, (struct sockaddr*)&un, sizeof(struct sockaddr_un)) < 0) {
printf("bind failed!");
return 1 ;
}
printf("unix demain bound!\n");
//推出解除链接
unlink("test.socket") ;
return 0 ;
}
创建唯一连接
确定了地址的创建方式,和网络套接字一样,服务进程可以通过bind、listen、accept和客户进程建立唯一连接。客户进程通过使用connect向服务进程发送连接请求。
下面是一个使用Unix域中的流socket的客户端和服务器程序:
server
#include <stdio.h>
#include<iostream>
#include<stdlib.h>
#include<unistd.h>
#include<sys/un.h>
#include<sys/socket.h>
#define BUFSIZE 1024
#define SV_SOCK_PATH "/tmp/uu"
int main()
{
struct sockaddr_un addr ;
int sfd ,cfd ;
char buf[BUFSIZE] ;
bzero(buf, 1024) ;
sfd = socket(AF_UNIX, SOCK_STREAM, 0) ;
if(sfd == -1) {
std::cout <<"socket err" <<std::endl ;
exit(1) ;
}
//防止文件已存在导致绑定失败
if(remove(SV_SOCK_PATH) == -1&& errno != ENOENT) {
std::cout<< "remove" <<std::endl ;
exit(1) ;
}
memset(&addr, 0, sizeof(addr)) ;
addr.sun_family = AF_UNIX ;
//设置成员sun_path
strncpy(addr.sun_path, SV_SOCK_PATH, sizeof(addr.sun_path)-1) ;
//绑定地址
if(bind(sfd, (struct sockaddr*)&addr,size) == -1) {
std::cout <<"bind error!"<<std::endl ;
exit(1) ;
}
if(listen(sfd, 10) == -1) {
std::cout <<"listen error!"<<std::endl ;
exit(1);
}
ssize_t num ;
for(;;){
cfd = accept(sfd, NULL, NULL) ;
if(cfd == -1){
std::cout << "accept error!"<<std::endl ;
exit(1) ;
}
while((num = read(cfd, buf, BUFSIZE)) > 0)
write(STDOUT_FILENO, buf, BUFSIZE);
}
close(cfd) ;
return 0;
}
client
#include <iostream>
#include<sys/un.h>
#include<string.h>
#include<sys/socket.h>
#include<unistd.h>
#define SIZE 1024
#define SV_SOCK_PATH "/tmp/uu"
int main()
{
struct sockaddr_un addr ;
int sfd ;
ssize_t num ;
char buf[SIZE] ;
bzero(buf, SIZE) ;
sfd = socket(AF_UNIX, SOCK_STREAM, 0) ;
memset(buf, 0, sizeof(buf)) ;
memset(&addr, 0, sizeof(addr)) ;
addr.sun_family = AF_UNIX ;
strncpy(addr.sun_path, SV_SOCK_PATH, sizeof(addr.sun_path)-1) ;
int size = offsetof(struct sockaddr_un, sun_path)+strlen(addr.sun_path) ;
if(connect(sfd, (struct sockaddr*)&addr, size)) {
std::cout <<"connnect error!" << std::endl ;
exit(1) ;
}
while((num = read(STDIN_FILENO, buf, SIZE)) >0) {
write(sfd, buf, num) ;
}
if(num == -1) {
std::cout <<"write error!" << std ::endl ;
exit(1) ;
}
return 0;
}
发送打开的描述符
发送文件描述符有什么意义呢?
对于这一个问题,我只能说,根据个人见解存在即合理,这一操作可以实现使得发送文件描述符的进程被接收描述符进程监视起来,发送进程对文件的操作对于接收进程来说都是透明的!
思考一个问题 ,在A进程中,打开一个文件,通过消息队列发给进程B(假定能接收成功),那这个文件描述符在B进程中是否会有效呢?!对,会失效!我们都知道在进程空间的所有信息由内核控制的结构体task_struct来维护,每个进程都有相应的task_struct,task_struct里面有一个成员 struct files_struct *files,称为用户打开文件表,记录当前进程文件描述符的打开情况,打开文件的记录也在这个结构中记录,进程调用open返回的文件描述符由struct file类型的成员决定, 每个进程的struct files_struct如图所示:
struct files_struct {
atomic_t count; /* 共享该表的进程数 */
rwlock_t file_lock; /* 保护以下的所有域,以免在tsk->alloc_lock中的嵌套*/
int max_fds; /*当前文件对象的最大数*/
int max_fdset; /*当前文件描述符的最大数*/
int next_fd; /*已分配的文件描述符加1*/
struct file ** fd; /* 指向文件对象指针数组的指针 */
fd_set *close_on_exec; /*指向执行exec( )时需要关闭的文件描述符*/
fd_set *open_fds; /*指向打开文件描述符的指针*/
fd_set close_on_exec_init;/* 执行exec( )时需要关闭的文件描述符的初 值集合*/
fd_set open_fds_init; /*文件描述符的初值集合*/
struct file * fd_array[32];/* 文件对象指针的初始化数组*/
};
struct file
{
struct list_head f_list; /*所有打开的文件形成一个链表*/
struct dentry *f_dentry; /*指向相关目录项的指针*/
struct vfsmount *f_vfsmnt; /*指向VFS安装点的指针*/
struct file_operations *f_op; /*指向文件操作表的指针*/
mode_t f_mode; /*文件的打开模式*/
loff_t f_pos; /*文件的当前位置*/
unsigned short f_flags; /////////////////////////////////////////////打开文件时所指定的标志fd///////////////////////////////////////////////////////
unsigned short f_count; /*使用该结构的进程数*/
unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;
/*预读标志、要预读的最多页面数、上次预读后的文件指针、预读的字节数以及
预读的页面数*/
int f_owner; /* 通过信号进行异步I/O数据的传送*/
unsigned int f_uid, f_gid; /*用户的UID和GID*/
int f_error; /*网络写操作的错误码*/
unsigned long f_version; /*版本号*/
void *private_data; /* tty驱动程序所需 */
};
所以说,tast_struct中的file_struct不一样,A进程打开的文件描述符发给B进程后怎么能指向同一个文件表呢?普通的IPC实现进程之间传递文件fd会导致fd失效!
但是Unix域套接字却可以实现不同进程间描述符的传送!如何实现?就和父进程fork出子进程,父子进程共享文件表项的原理基本一致!如图:
下面的例子是子进程继承了父进程的文件描述符,父进程和子进程通过fd共享文件表读取同一文件:
#include <stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
int main()
{
char buf[10] ;
int fd = open("hello",O_RDWR) ;
//创建新进程
if(fork() == 0) {
while(read(fd, buf, sizeof(buf))>0)
printf("\n子进程%d: %s\n", getpid(), buf);
}
while(read(fd, buf, sizeof(buf))>0)
printf("\n父进程%d:%s\n",getpid(), buf);
return 0;
}
文件内容为:
bcdefghjklpourwbnmz
运行结果:
子进程先被系统调度读取文件移动文件指针,父进程被调度后,继续移动子进程移动了的文件指针读取数据,说明父子进程的文件fd共享同一文件表!
同样,在非亲缘关系进程之间传送文件描述符也是同样的道理,发送fd就相当于发送进程向接收进程发送了一个与其进程中打开的fd所指向的文件表项相同的指针,这个指针被接收进程接收到后,存于接收进程第一个可用的描述符表项中。当发送进程将描述符发送给接收进程后,通常关闭该文件描述符,发送进程关闭该描述符并不会真的关闭该文件或者设备,原因是描述符将视为由接收进程打开(即使接收进程尚未接收到该描述符),即描述符的发送导致他的访问统计数加1。
为了用Unix域套接字交换文件描述符,调用sendmsg和recvmsg,这两个函数中都有一个指向msghdr的结构的指针,该结构包含了所有关于要发送或者要接收的消息的信息,定义如下:
#include<sys/socket.h>
struct msghdr {
void * msg_name ; / * 消息的协议地址 * / 协议地址和套接口信息,在非连接的UDP中,发
送者要指定对方地址端口,接受方用于的到数据来源,
如果不需要的话可以设置为NULL(在TCP或者连接的UDP中,一般设置为NULL)
socklen_t msg_namelen ; / * 地址的长度 * /
//上面两种一般和用于在网络连接上发送数据报,其中目的地址可以有数据报来指定
struct iovec * msg_iov ; / * 多io缓冲区的地址 * //定义套接字要发送的数据
int msg_iovlen ; / * 缓冲区的个数 * / //上面缓冲区的个数
void * msg_control ; 辅助数据, cmsghdr结构体的地址
socklen_t msg_controllen ; //cmsghdr字段包含的字节数
int msg_flags ; / * 接收消息的标识 * /
} ;
科普一下关于struct iovec结构的用法:
#include <iostream>
#include<sys/uio.h>
#include<string.h>
int main()
{
struct iovec iov[3] ;
const char* p1 = "I'" ;
const char* p2 = "m " ;
const char* p3 = "programing!\n";
iov[0].iov_base =(void*) p1 ;
iov[0].iov_len = strlen(p1) ;
iov[1].iov_base =(void*) p2 ;
iov[1].iov_len = strlen(p2) ;
iov[2].iov_base =(void*) p3 ;
iov[2].iov_len = strlen(p3) ;
writev(1, iov, 3) ;
return 0;
}
前两者主要用于网络连接上发送数据报。
结构中msg_control指向msg_controllen结构。
struct cmsghdr{
socetlen_t cmsg_len ;//大小有CMSG_LEN宏决定,一般传一个int然后sizeof(int)+sizoeof(struct cmsghdr) ;然后根据结构体对其要求向上取整
int cmsg_type ;//一般设置为SCM_RIGHTS说明访问权
int cmsg_level ;//一般设置成SOL_SOCKET,说明被设置选项是在socket级别上的
}
为了传送文件描述符,将cmsg_len设置为cmsghdr结构的长度加一个整型的长度(描述符的长度),cmsg_level字段设置为SOL_SOCKET,cmsg_type字段设置为SCM_RIGHTS(SCM为套接字级控制消息),用以表明传送访问权。访问权仅能通过Unix域套接字传送,描述符紧随cmsg_type字段之后存储,用CMSG_DATA宏获得该整型量的指针。
参考(可以说是照抄)APUE写的send_fd
#include<sys/un.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<string.h>
#define CONTROLLEN CMSG_LEN(sizeof(int))
static struct cmsghdr* cmptr = NULL ;
//send_fd 先发送2字节0,然后发送实际描述符
//发送文件描述符
//通过Unix域套接字传递文件描述符,sendmsg调用被
//用来传送协议数据以及描述符
int send_fd(int fd, int fd_to_send) {
struct iovec iov[1] ;
struct msghdr msg ;
char buf[2] ;
//向往缓冲区填上两字节数据,描述符随其后发送
iov[0].iov_base = buf ;
iov[0].iov_len = 2 ;
msg.msg_iov = iov ;
msg.msg_iovlen = 1 ;
msg.msg_name = NULL ;
msg.msg_namelen = 0 ;
if(fd_to_send < 0) {
msg.msg_control = NULL ;
msg.msg_controllen = 0 ;
buf[1] = -fd_to_send ;
if(buf[1] == 0) {
buf[1] = 1 ;
}
}
else {
if(cmptr == NULL && (cmptr = (struct cmsghdr*)malloc(CONTROLLEN)) == NULL) {
return -1 ;
}
cmptr->cmsg_level = SOL_SOCKET ;
cmptr->cmsg_type = SCM_RIGHTS ;
cmptr->cmsg_len = CONTROLLEN ;
msg.msg_control = cmptr ;
msg.msg_controllen = CONTROLLEN ;
//返回一个指针指向与cmsghdr相关联的数据
//这里将要发送的描述符和这个地址绑定
*(int*)CMSG_DATA(cmptr) = fd_to_send ;
buf[1] = 0 ;
}
buf[0] = 0;
if(sendmsg(fd, &msg, 0) != 2) {
return -1 ;
}
return 0 ;
}
recv_fd
#include<sys/socket.h>
#include<stdio.h>
#include<sys/un.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<string.h>
#include<stdlib.h>
//recv_fd 读取套接字中所有字节直至遇到NULL字符
//null字符之前所有的字符都传送给调用者的userfunc
//该程序总是准备接收一个描述符
//在每次调用之前设置msg_control和msg_controllen
//在msg_controllen返回的是非0时才确实接收到描述符
#define CONTROLLEN CMSG_LEN(sizeof(int))
static struct cmsghdr *cmptr = NULL ;
int recv_fd(int fd, ssize_t(*userfunc)(int, const void *t, size_t)) {
int newFd , nr ,status ;
char* ptr ;
char buf[1024] ;
struct iovec iov[1] ;
struct msghdr msg ;
status = 1 ;
for(;;) {
iov[0].iov_base = buf ;
iov[0].iov_len =sizeof(buf) ;
msg.msg_iov = iov ;
msg.msg_iovlen = 1 ;
msg.msg_name = NULL ;
msg.msg_namelen = 0 ;
if(cmptr == NULL && (cmptr = (struct cmsghdr*)malloc(CONTROLLEN)) == NULL) {
return -1 ;
}
msg.msg_control = cmptr ;
msg.msg_controllen = CONTROLLEN ;
if((nr = recvmsg(fd, &msg, 0)) < 0) {
return -1 ;
}
else if(nr == 0){
return -1 ;
}
for(ptr = buf; ptr < &buf[nr]; ) {
if(*ptr++ == 0) {
if(ptr != &buf[nr-1]) {
printf("出现错误\n") ;
return -1 ;
}
status = * ptr&0xFF ;
if(status == 0) {
if(msg.msg_controllen < CONTROLLEN) {
printf("出现错误!\n") ;
}
newFd = *(int*)CMSG_DATA(cmptr) ;
}
else {
newFd = -status ;
}
nr = -2 ;
}
}
if(nr > 0 && (*userfunc)(2, buf, nr) != nr) {
return -1 ;
}
if(status >= 0) {
return newFd ;
}
}
}
当发送完描述符后,那么问题来了,接收到描述符的那一个进程如何知道是哪个进程那个用户给他发送的文件描述符呢?
要想确认进程身份,则需要发送文件描述符的进程来主动告诉目标进程。Linux中维护了一个结构体,用来表述发送进程的身份。定义如下:
struct ucred{
pid_t pid ;
uid_t uid ;
gid_t gid ;
}
见名知意,这里各个字段不做解释。
在发送进程发送时,包含上自己的证书才能让接收进程确定各个进程的身份。
更新过后的send_fd
#if defined(SCM_CREDS)
#define CREDSTRUCT cmsgcred
#define SCM_CREDTYPE SCM_CREDS
#elif defined(SCM_CREDENTIALS)
#define CREDSTRUCT ucred
#define SCM_CREDTYPE SCM_CREDENTIALS
#else
#error passing credentials is unsupported!
#endif
#define RIGHTSLEN CMSG_LEN(sizeof(int))
#define CREDSLEN CMSG_LEN(sizeof(struct CREDSTRUCT))
#define CONTROLLEN (RIGHTSLEN + CREDSLEN)
static struct cmsghdr *cmptr = NULL;
int
send_fd(int fd, int fd_to_send)
{
struct CREDSTRUCT *credp;
struct cmsghdr *cmp;
struct iovec iov[1];
struct msghdr msg;
char buf[2]; /* send_fd/recv_ufd 2-byte protocol */
iov[0].iov_base = buf;
iov[0].iov_len = 2;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_flags = 0;
if (fd_to_send < 0) {
msg.msg_control = NULL;
msg.msg_controllen = 0;
buf[1] = -fd_to_send;
if (buf[1] == 0)
buf[1] = 1;
} else {
if (cmptr == NULL && (cmptr = malloc(CONTROLLEN)) == NULL)
return(-1);
msg.msg_control = cmptr;
msg.msg_controllen = CONTROLLEN;
cmp = cmptr;
cmp->cmsg_level = SOL_SOCKET;
cmp->cmsg_type = SCM_RIGHTS;
cmp->cmsg_len = RIGHTSLEN;
*(int *)CMSG_DATA(cmp) = fd_to_send;
cmp = CMSG_NXTHDR(&msg, cmp);
cmp->cmsg_level = SOL_SOCKET;
cmp->cmsg_type = SCM_CREDTYPE;
cmp->cmsg_len = CREDSLEN;
credp = (struct CREDSTRUCT *)CMSG_DATA(cmp);
#if defined(SCM_CREDENTIALS)
credp->uid = geteuid();
credp->gid = getegid();
credp->pid = getpid();
#endif
buf[1] = 0;
}
buf[0] = 0;
if (sendmsg(fd, &msg, 0) != 2)
return(-1);
return(0);
}
recv_fd
.......apue头文件省略
#if defined(SCM_CREDS)
#define CREDSTRUCT cmsgcred
#define CR_UID cmcred_uid
#define SCM_CREDTYPE SCM_CREDS
#elif defined(SCM_CREDENTIALS)
#define CREDSTRUCT ucred
#define CR_UID uid
#define CREDOPT SO_PASSCRED
#define SCM_CREDTYPE SCM_CREDENTIALS
#else
#error passing credentials is unsupported!
#endif
#define RIGHTSLEN CMSG_LEN(sizeof(int))
#define CREDSLEN CMSG_LEN(sizeof(struct CREDSTRUCT))
#define CONTROLLEN (RIGHTSLEN + CREDSLEN)
static struct cmsghdr *cmptr = NULL;
int
recv_ufd(int fd, uid_t *uidptr,
ssize_t (*userfunc)(int, const void *, size_t))
{
struct cmsghdr *cmp;
struct CREDSTRUCT *credp;
char *ptr;
char buf[MAXLINE];
struct iovec iov[1];
struct msghdr msg;
int nr;
int newfd = -1;
int status = -1;
#if defined(CREDOPT)
const int on = 1;
if (setsockopt(fd, SOL_SOCKET, CREDOPT, &on, sizeof(int)) < 0) {
err_ret("setsockopt error");
return(-1);
}
#endif
for ( ; ; ) {
iov[0].iov_base = buf;
iov[0].iov_len = sizeof(buf);
msg.msg_iov = iov;
msg.msg_iovlen = 1;
msg.msg_name = NULL;
msg.msg_namelen = 0;
if (cmptr == NULL && (cmptr = malloc(CONTROLLEN)) == NULL)
return(-1);
msg.msg_control = cmptr;
msg.msg_controllen = CONTROLLEN;
if ((nr = recvmsg(fd, &msg, 0)) < 0) {
err_ret("recvmsg error");
return(-1);
} else if (nr == 0) {
err_ret("connection closed by server");
return(-1);
}
for (ptr = buf; ptr < &buf[nr]; ) {
if (*ptr++ == 0) {
if (ptr != &buf[nr-1])
err_dump("message format error");
status = *ptr & 0xFF;
if (status == 0) {
if (msg.msg_controllen != CONTROLLEN)
err_dump("status = 0 but no fd");
// CMSG_FIRSTHDR返回一个指针,指向与msghdr相关联的第一个结构体
//CMSG_NXTHDR返回一个指针指向与msghdr相关联的下一个结构体
for (cmp = CMSG_FIRSTHDR(&msg); cmp != NULL; cmp = CMSG_NXTHDR(&msg, cmp)) {
if (cmp->cmsg_level != SOL_SOCKET)
continue;
switch (cmp->cmsg_type) {
case SCM_RIGHTS:
newfd = *(int *)CMSG_DATA(cmp);
break;
case SCM_CREDTYPE:
//返回一个指针指向域cmghdr相关联的数据
credp = (struct CREDSTRUCT *)CMSG_DATA(cmp);
*uidptr = credp->CR_UID;
}
}
} else {
newfd = -status;
}
nr -= 2;
}
}
if (nr > 0 && (*userfunc)(STDERR_FILENO, buf, nr) != nr)
return(-1);
if (status >= 0)
return(newfd);
}
}
下面是进程间的通信,传送普通数据和套接字的一个简单例子:
recvProcess
#include <unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/un.h>
int recvfd(int);
void client_socketun(){
int sockfd, newfd, len, nr;
sockaddr_un un;
char buf[20]={0};
un.sun_family = AF_UNIX;
strcpy(un.sun_path, "/tmp/s");
len = sizeof(un.sun_family) + strlen(un.sun_path);
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
if(connect(sockfd, (sockaddr *)&un, len) == -1){//连接socket
printf("connect error");
return ;
}
// read(sockfd, buf, 20);
recv(sockfd, buf, 20, 0);//接收普通数据
printf("接收到普通数据:%s\n", buf) ;
if((newfd = recvfd(sockfd)) == -1){//接收msg数据
printf("rec error");
}
else{
printf("通过新的文件描述符读取文件数据:\n") ;
while(1){
if((nr = read(newfd, buf, 10)) == -1){
perror("read error");
}
if(nr == 0) {
break ;
}
else{
printf("%s", buf);
fflush(stdout);
}
}
}
close(sockfd);
}
int recvfd(int sockfd){
int newfd, nr;
struct cmsghdr *cmptr = nullptr;//OUT
int cmsghdrlen = CMSG_LEN(sizeof(int));
char buf[2];
struct iovec iov[1];
struct msghdr msg;
iov[0].iov_base = buf;
iov[0].iov_len = 2;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
msg.msg_name = NULL;
msg.msg_namelen = 0;
cmptr = (cmsghdr *)malloc(cmsghdrlen);
msg.msg_control = cmptr;
msg.msg_controllen = cmsghdrlen;
if((nr = recvmsg(sockfd, &msg, 0)) < 0){
perror("recvmsg error\n");
}else{
newfd = *(int *)CMSG_DATA(cmptr);
fflush(stdout);
return newfd;
}
return -1;
}
int main(){
client_socketun();
return 0;
}
sendProcess
#include <unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/un.h>
void socketSendfd(int, int);
void namedSocket() {
int sockfd, size;
sockaddr_un un;
char buf[20] = {"123"};
un.sun_family = AF_UNIX;
strcpy(un.sun_path, "/tmp/s");
if ((sockfd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
printf("socket error");
size = sizeof(un.sun_family) + strlen(un.sun_path);
// print(size);
if (bind(sockfd, (sockaddr *) &un, size) < 0) {//绑定
printf("bind error");
return;
}
// print("bound OK");
if (listen(sockfd, 5) == -1) {//监听
printf("listen error");
return;
}
sockaddr_un client_addr;//OUT,接收客户端un信息
socklen_t len;
int clifd;
if ((clifd = accept(sockfd, (sockaddr *) &client_addr, &len)) == -1) {
printf("accept error");
return;
}
send(clifd, buf, 10, 0);//传送普通数据
int fd = open("hello", O_CREAT | O_RDWR, 0666);
char data[10] ;
read(fd, data, sizeof(data)) ;
printf("读取文件的数据:%s\n",data) ;
socketSendfd(clifd, fd);//传送文件描述符
close(fd);
close(sockfd);
unlink(un.sun_path);//解除链接操作
}
void socketSendfd(int clifd, int fdToSend){
struct iovec iov[1];
struct msghdr msg;//②msghdr结构体
struct cmsghdr *cmptr = nullptr;//③cmsghdr结构体
int cmsghdrlen = CMSG_LEN(sizeof(int));//CMSG_LEN()返回cmsghdr结构的cmsg_len成员的值,考虑到任何必要的对齐。它取出参数的长度。这是一个常量表达式。
char buf[2] = {0};
iov[0].iov_base = buf;
iov[0].iov_len = 2;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
msg.msg_name = NULL;
msg.msg_namelen = 0;
cmptr = (cmsghdr *)malloc(cmsghdrlen);
cmptr->cmsg_level = SOL_SOCKET;
cmptr->cmsg_type = SCM_RIGHTS;//SCM_RIGHTS表明在传送访问权,访问权仅能通过UNIX域套接字传送。
cmptr->cmsg_len = cmsghdrlen;
msg.msg_control = cmptr;
msg.msg_controllen = cmsghdrlen;
*(int *)CMSG_DATA(cmptr) = fdToSend;//CMSG_DATA()返回cmsghdr的数据部分指针。
if(sendmsg(clifd, &msg, 0) == -1){
perror("send error");
}
}
int main() {
namedSocket();
return 0;
}
hello源文件内容:
bdfsfcdefghjklpourfsfsfsfwbnmz
运行结果:
发送端发送时指针做了移动,在接收端文件指针继续接着发送端进程移动的位置移动!证明两个进程中的描述符共同指向同意文件表项。成功实现无关进程之间传送文件描述符的目的!
其实可以讲匿名Unix套接字和有名套接字结合起来使用通过使用有名Unix域套接字发送匿名套接字!