1. UNIX域套接字
1.1 概念
这种形式的IPC可以在同一计算机系统上运行的两个进程之间传送打开文件描述符
-
UNIX域套接字用于在同一台计算机上运行的进程之间的通信。这里要和第十六章介绍的内容区别,第十六章介绍的是因特网域套接字,而这里介绍UNIX域套接字,即通信域不同。因特网域主要应用于不同主机进程间通信,UNIX域只能应用于同一台主机上进程间的通信。 -
虽然因特网域套接字也可用于同一计算机进程间通信,但是UNIX域套接字效率更高。因为UNIX域套接字不需要TCP/IP网络协议栈处理,UNIX域套接字仅仅是复制数据,不执行协议处理,不需要添加或删除网络报头,无需计算校验和,不产生顺序号,无需发送确认报文。 -
UNIX域套接字提供基于字节流和基于数据报的两种接口。UNIX域套接字的数据报服务是可靠的,即不会丢失报文,不会传递出错。 -
UNIX域套接字就像是套接字和管道的混合。可以使用因特网域套接字接口(十六章介绍的bind、connect等函数),或者使用socketpair函数来创建一对无命名的、相互连接的UNIX域套接字。
1.2 无命名UNIX域套接字(socketpair函数)
使用socketpair函数来创建一对无命名的、相互连接的UNIX域套接字。类比于pipe函数
因为套接字是匿名的,因此只能用于相关进程间通信。
int socketpair(int domain, int type, int protocol, int sv[2]);
- 参数domain:通信域,在Linux下只能为AF_LOCAL或者AF_UNIX
- 参数type:套接字类型,既可以是SOCK_STREAM,又可以是SOCK_DGRAM,当参数指定为SOCK_STREAM时,得到的结果称为流管道,它与一般管道的区别是流管道是全双工的,即两个描述符既可读又可写
- 参数protocol:协议类型,只能是0
- 参数cv[2]:保存分配的两个UNIX域套接字
由该函数创建的一对未命名的UNIX域套接字,可以起到全双工管道的作用:两端都对读和写开放。
其实有些系统使用UNIX域套接字实现匿名管道:关闭第一描述符的写端和第二描述符的读端。
1.3 命名UNIX域套接字
未命名套接字由于没有名字,因此只能用于有关进程间通信,不能用于无关进程间通信。
上一章中介绍了bind函数可以将一个地址绑定到一个因特网域套接字上。对于命名UNIX域套接字,也可以使用bind函数将其绑定到一个路径上,但是注意,命名UNIX域套接字绑定的地址格式不同于因特网域套接字。
命名UNIX域套接字的绑定的地址格式由sockaddr_un结构表示,该结构体指针在传递给bind函数时也要转换为sockaddr指针类型。
struct sockaddr_un
{
sa_family_t sun_family;
char sun_path[108];
};
可以看出,sockaddr_un结构的sun_path成员包含一个路径(文件)名。当我们bind绑定成功时,系统会用该路径名创建一个套接字类型文件。
2. 唯一连接
与因特网域套接字类似,服务器进程可以使用bind、listen、accept函数,为客户进程安排一个唯一UNIX域连接。客户进程使用connect与服务器进程连接。在服务器接受了connect请求后,在服务器进程和客户进程之间就存在了唯一连接。
具体的操作类似于因特网域套接字中TCP连接。
-
服务器进程指定一个UNIX域套接字,通过bind绑定sockaddr_un地址,然后listen使之可以接受连接请求。连接到来时accept函数返回一个新的套接字用于与客户进程通信。 -
客户进程通过一个UNIX域套接字connect至服务器进程,注意connect的目的sockaddr_un地址与服务器进程bind时的地址必须一致。注意,这里通过connect函数将该UNIX域套接字绑定到一个默认地址,但是并不会在文件系统创建套接字文件。
3. 传送文件描述符
在两个进程之间传送打开文件描述符的技术非常有用。我们可以设计一个open服务器,专门接收客户进程发来的请求,open服务器提供的唯一服务就是根据客户请求的文件路径和打开模式打开指定文件,并向客户进程发回文件描述符,客户进程进而执行各种I/O。(问题:如果客户进程没有某种文件访问权限但是服务器进程有,那么这种操作是否可以越过文件权限检查?)
当一个进程向另一个进程传送一个打开文件描述符时,会使得发送进程和接收进程共享同一文件表项。
在技术上,我们是将指向一个打开文件表项的指针从一个进程发送到另一个进程。该指针被分配存放在接收进程的第一个可用描述符中(即发送进程和接收进程中的描述符编号不一定相同)
当发送进程将描述符传送给接收进程后,通常会close关闭该描述符。发送进程关闭该描述符并不会真的关闭该文件或设备,其原因是该描述符被视为由接收进程打开(即使接收进程尚未收到该描述符)
3.1 通过recvmsg、sendmsg函数接收/发送文件描述符
我们可以通过recvmsg、sendmsg函数实现在UNIX域套接字间传送文件描述符,具体是使用这两个函数的msghdr参数中的msg_control辅助数据字段。
struct msghdr {
void *msg_name;
socklen_t msg_namelen;
struct iovec *msg_iov;
size_t msg_iovlen;
void *msg_control;
size_t msg_controllen;
int msg_flags;
};
其中msg_control字段指向辅助数据(即为cmsghdr结构体,控制信息头),msg_controllen字段表示控制信息字节数
struct cmsghdr
{
size_t cmsg_len;
int cmsg_level;
int cmsg_type;
...
};
为了发送文件描述符:
访问权只能通过UNIX域套接字传送,真正的控制信息(即描述符数据)跟在cmsg_type字段之后存储。
可以通过以下宏定义来访问/管理cmsghdr结构体
unsigned char *CMSG_DATA(struct cmsghdr *cmsg);
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh);
struct cmsghdr *CMSG_NXTHDR(struct msghdr *msgh, struct cmsghdr *cmsg);
size_t CMSG_LEN(size_t length);
可以通过辅助数据传送的其它控制信息:具体见UNIX网络编程14.6节
4. 补充:获得一个结构体中某一成员的偏移量
size_t offsetof(type, member);
type是结构体类型名,member是结构体成员名。返回该成员从结构体开始处的偏移量。
offsetof的实现是一个宏
struct MyC{
int a;
int b;
double c;
};
int main(int argc, char *argv[])
{
cout << offsetof(MyC,a)<< endl;
cout << offsetof(MyC,b)<< endl;
cout << offsetof(MyC,c)<< endl;
}
5. 补充:以一致方式处理命令行参数 getopt函数
通过getopt函数及几个全局变量帮助开发者以一致的方式处理命令行选项。类似于python的optparse模块
int getopt(int argc, char * const argv[],const char *opstions);
extern char *optarg;
extern int optind, opterr, optopt;
-
参数argc和argv分别代表参数个数和内容,跟main()函数的命令行参数是一样的。 -
参数options说明哪些选项有参数,哪些选项没参数。如果选项字符串里的字母后接着冒号“:”,则表示还有相关的参数。例如一条命令的用法说明如下: command [-i] [-u username] [-z] filename
则我们可以给getopt穿一个"iu:z"作为options字符串
函数getopt一般在循环体内,每次调用getopt处理一个命令行选项,循环直到getopt返回-1时退出。每次迭代中,getopt返回选项字符。如果处理的当前选项有参数,则全局变量optarg 会指向此额外参数。
-
如果遇到无效选项,getopt()默认打印一条出错消息。如果不希望getopt()打印出错信息,则只要将全域变量opterr设为0即可。 -
optind全局变量用来存放下一个要处理的字符串在argv数组里的下表。它从1开始,每次getopt一次,该全局变量+1。 -
当getopt出错,optopt全局变量会指向导致出错的选项字符串。
|