内核中用于数据接收的结构体struct msghdr

    技术2022-05-18  16

    我们从一个实际的数据包发送的例子入手,来看看其发送的具体流程,以及过程中涉及到的相关数据结构。在我们的虚拟机上发送icmp回显请求包,ping另一台主机172.16.48.1。我们使用系统调用sendto发送这个icmp包。

         ssize_t sendto(int s, const void *buf, size_t len, int flags,

                            const struct sockaddr *to, socklen_t tolen);

         系统调用sendto最终调用内核函数 asmlinkage long sys_sendto(int fd, void __user * buff, size_t len, unsigned flags, struct sockaddr __user *addr, int addr_len)

        sys_sendto构建一个结构体struct msghdr,用于接收来自应用层的数据包 ,下面是结构体struct msghdr的定义:

            struct msghdr {

                void            *msg_name;// 存数据包的目的地址,网络包指向sockaddr_in

                                                       //向内核发数据时,指向sockaddr_nl

                int             msg_namelen;// 地址长度

                struct iovec    *msg_iov;

                __kernel_size_t msg_iovlen;

                void            *msg_control;

                __kernel_size_t msg_controllen;

                unsigned        msg_flags;

            };

        这个结构体的内容可以分为四组。

        第一组是msg_name和msg_namelen,记录这个消息的名字,其实就是数据包的目的地址msg_name是指向一个结构体struct sockaddr的指针 。长度为16:

            struct sockaddr{

                sa_family_t sa_family;

                char        sa_addr[14];

            }

    所以,msg_namelen的长度为16。需要注意的是,结构体struct sockaddr只在进行参数传递时使用,无论是在用户态还是在内核态,我们都把其强制转化为结构体struct sockaddr_in:

            strcut sockaddr_in{

                sa_family_t         sin_family;

                unsigned short int sin_port;

                struct in_addr      sin_addr;

                unsigned char       __pad[__SOCK_SIZE__ - sizeof(short int) -

                                    sizeof(unsigned short int) - sizeof(struct in_addr)];

            };

            struct in_addr{

                __u32 s_addr;

            }

    __SOCK_SIZE__的值为16,所以,struct sockaddr中真正有用的数据只有8bytes。在我们的ping例子中,传入到内核的msghdr结构中:

        msg.msg_name = { sa_family_t = MY_AF_INET, sin_port = 0, sin_addr.s_addr = 172.16.48.1 }

        msg_msg_namelen = 16。

    请求回显icmp包没有目的端地址的端口号。

        第二组是msg_iov和msg_iovlen,记录这个消息的内容。msg_iov是一个指向结构体struct iovec的指针,实际上,确切地说,应该是一个结构体strcut iovec的数组 。下面是该结构体的定义:

        struct iovec{

            void __user     *iov_base;

            __kernel_size_t iov_len;

        };

        iov_base指向数据包缓冲区,即参数buff,iov_len是buff的长度。msghdr中允许一次传递多个buff,以数组的形式组织在 msg_iov中,msg_iovlen就记录数组的长度 (即有多少个buff)。在我们的ping程序的实例中:

        msg.msg_iov = { struct iovec = { iov_base = { icmp头+填充字符'E' }, iov_len = 40 } }

        msg.msg_len = 1

        第三组是msg_control和msg_controllen,它们可被用于发送任何的控制信息,在我们的例子中,没有控制信息要发送。暂时略过。

        第四组是msg_flags。其值即为传入的参数flags。raw协议不支持MSG_ OOB 向标志,即带外数据。 向内核发送msg 时使用msghdr,netlink socket使用自己的消息头nlmsghdr和自己的消息地址sockaddr_nl:

    struct sockaddr_nl { sa_family_t    nl_family; unsigned short nl_pad; __u32          nl_pid; __u32          nl_groups; }; struct nlmsghdr { __u32 nlmsg_len;   /* Length of message */ __u16 nlmsg_type; /* Message type*/ __u16 nlmsg_flags; /* Additional flags */ __u32 nlmsg_seq;   /* Sequence number */ __u32 nlmsg_pid;   /* Sending process PID */ };

    过程如下:

    struct msghdr msg; memset(&msg, 0, sizeof(msg));msg.msg_name = (void *)&(nladdr);msg.msg_namelen = sizeof(nladdr); {/*初始化一个strcut nlmsghdr结构存,nlmsghdr为netlink socket自己的消息头部,并使iov->iov_base指向在这个结构*/ char buffer[] = "An example message"; struct nlmsghdr nlhdr; nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_MSGSIZE)); strcpy(NLMSG_DATA (nlhdr),buffer);//将数据存放在消息头指向的数据地址 nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer)); nlhdr->nlmsg_pid = getpid(); /* self pid */ nlhdr->nlmsg_flags = 0; iov.iov_base = (void *)nlhdr; iov.iov_len = nlh->nlmsg_len; } msg.msg_iov = &iov; msg.msg_iovlen = 1; fd=socket(AF_NETLINK, SOCK_RAW, netlink_type); sendmsg(fd,&msg,0)

     

     

    证书可以作为通信中所接收的附属数据的一部分而接收。附属数据对于通常数据来说是补充或是从属。这就引出需要在这里强调的几点问题: 证书是作为附属数据的一部分而接收的。 附属数据必须是补充通常数据的(他不可以独立传送)。 附属数据也可以包含其他的信息,例如文件描述符。 附属数据可以同时包含多个附属项目(例如同时包含证书与文件描述符)。 证书是由Linux内核提供的。他们从来不由客户程序提供。如果是这样,客户端就会被允许 个标识。因为内核是可信任的,证书就可以被对证书感兴趣的进程所信任。 现在我们已经了解文件描述符是作为附属数据来传送和接收的。然而,在我们开始编写套接口代码来使用这些附属数据元素时,我们需要介绍一些新的编程概念。 简介I/O向量 在我们了解使用附属数据工作的复杂函数之前,我们应该熟悉被readv(2)与writev(2)系统调用所使用的I/O向量。我们不仅将会发现这些函数是十分有用的,而他们的工作方式也被引入了一些附属数据函数中。这会使得后面的理解更为容易。 I/O向量(struct iovec) readv(2)与writev(2)函数都使用一个I/O向量的概念。这是由所包含的文件定义的: #include sys/uio.h头文件定义了struct iovc,其定义如下: struct iovec {     ptr_t iov_base; /* Starting address */     size_t iov_len; /* Length in bytes */ }; struct iovec定义了一个向量元素。通常,这个结构用作一个多元素的数组。对于每一个传输的元素,指针成员iov_base指向一个缓冲区,这个缓冲区是存放 的是readv所接收的数据或是writev将要发送的数据。成员iov_len在各种情况下分别确定了接收的最大长度以及实际写入的长度。 readv(2)与writev(2)函数 这些函数是作为read与write函数的衍生函数而被人所知的。他们以这样的方式进行设计是因为他们可以在一个原子操作中读取或是写入多个缓冲区。这些函数的原型如下: #include int readv(int fd, const struct iovec *vector, int count); int writev(int fd, const struct iovec *vector, int count); 这些函数需要三个参数: 要在其上进行读或是写的文件描述符fd 读或写所用的I/O向量(vector) 要使用的向量元素个数(count) 这些函数的返回值是readv所读取的字节数或是writev所写入的字节数。如果有错误发生,就会返回-1,而errno存有错误代码。注意,也其他I/O函数类似,可以返回错误码EINTR来表明他被一个信号所中断。 使用writev的例子 下面的程序代码展示了如何使用writev函数将三个独立的C字符串作为一次写操作写入标准输出。 /* * writev.c * * Short writev(2) demo: */ #include int main(int argc,char **argv) {     static char part2[] = "THIS IS FROM WRITEV";     static char part3[]    = "]/n";     static char part1[] = "[";     struct iovec iov[3];     iov[0].iov_base = part1;     iov[0].iov_len = strlen(part1);     iov[1].iov_base = part2;     iov[1].iov_len = strlen(part2);     iov[2].iov_base = part3;     iov[2].iov_len = strlen(part3);     writev(1,iov,3);     return 0; } 编译运行程序: $ make writev gcc -g -c -D_GNU_SOURCE -Wall -Wreturn-type writev.c gcc writev.o -o writev $ ./writev [THIS IS FROM WRITEV] $ 当程序运行时,我们可以看到无论所引用的缓冲区是如何分散,所有的缓冲区都会被输出形成最终的字符串。 也许我们希望多花一些时间来修改这个程序并做各种测试,但是要注意一定要将iov[[]数组分配得足够大。 sendmsg(2)与recvmsg(2)函数 这些函数为程序提供了一些其他的套接口I/O接口所不具备的高级特性。下面的内容我们将会先来看一下sendmsg来介绍这些主题。然后将会完整的介绍recvmsg函数,因为他们的函数接口是相似的。接下来,将会描述msghdr的完整结构。 sendmsg(2)函数 现在是时候进入这个大同盟了。从概念上说,sendmsg函数是所有写入函数的基础,而他是从属于套接口的。下面的列表以复杂增加的顺序列出了所有与入函数。在每一个层次上,同时列出了所增加的特性。 函数        增加的特性 write        最简单的套接口写入函数 send        增加了flags标记 sendto        增加了套接口地址与套接口长度参数 writev        没有标记与套接口地址,但是具有分散写入的能力 sendmsg        增加标记,套接口地址与长度,分散写入以及附属数据的能力 sendmsg(2)函数原型如下: #include #include int sendmsg(int s, const struct msghdr *msg, unsigned int flags); 函数参数描述如下: 要在其上发送消息的套接口s 信息头结构指针msg,这会控制函数调用的功能 可选的标记位参数flags。这与send或是sendto函数调用的标记参数相同。 函数的返回值为实际发送的字节数。否则,返回-1表明发生了错误,而errno表明错误原因。 recvmsg(2)函数 recvmsg是与sendmsg函数相对的函数。这个函数原型如下: #include #include int recvmsg(int s, struct msghdr *msg, unsigned int flags); 函数参数如下: 要在其上接收信息的套接口s 信息头结构指针msg,这会控制函数调用的操作。 可选标记位参数flags。这与recv或是recvfrom函数调用的标记参数相同。 这个函数的返回值为实际接收的字节数。否则,返回-1表明发生了错误,而errno表明错误原因。 理解struct msghdr 当我第一次看到他时,他看上去似乎是一个需要创建的巨大的结构。但是不要怕。其结构定义如下: 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_name与msg_namelen。 I/O向量引用msg_iov与msg_iovlen。 附属数据缓冲区成员msg_control与msg_controllen。 接收信息标记位msg_flags。 在我们将这个结构分为上面的几类以后,结构看起来就不那样巨大了。 成员msg_name与msg_namelen 这些成员只有当我们的套接口是一个数据报套接口时才需要。msg_name成员指向我们要发送或是接收信息的套接口地址。成员msg_namelen指明了这个套接口地址的长度。 当调用recvmsg时,msg_name会指向一个将要接收的地址的接收区域。当调用sendmsg时,这会指向一个数据报将要发送到的目的地址。 注意,msg_name定义为一个(void *)数据类型。我们并不需要将我们的套接口地址转换为(struct sockaddr *)。 成员msg_iov与msg_iovlen 这些成员指定了我们的I/O向量数组的位置以及他包含多少项。msg_iov成员指向一个struct iovec数组。我们将会回忆起I/O向量指向我们的缓冲区。成员msg_iov指明了在我们的I/O向量数组中有多少元素。 成员msg_control与msg_controllen 这些成员指向了我们附属数据缓冲区并且表明了缓冲区大小。msg_control指向附属数据缓冲区,而msg_controllen指明了缓冲区大小。 成员msg_flags 当使用recvmsg时,这个成员用于接收特定的标记位(他并不用于sendmsg)。在这个位置可以接收的标记位如下表所示: 标记位        描述 MSG_EOR        当接收到记录结尾时会设置这一位。这通常对于SOCK_SEQPACKET套接口类型十分有用。 MSG_TRUNC    这个标记位表明数据的结尾被截短,因为接收缓冲区太小不足以接收全部的数据。 MSG_CTRUNC    这个标记位表明某些控制数据(附属数据)被截短,因为缓冲区太小。 MSG_OOB        这个标记位表明接收了带外数据。 MSG_ERRQUEUE    这个标记位表明没有接收到数据,但是返回一个扩展错误。 我们可以在recvmsg(2)与sendmsg(2)的man手册页中查看更多的信息。 附属数据结构与宏 recvmsg与sendmsg函数允许程序发送或是接收附属数据。然而,这些额外的信息受限于一定的格式规则。这一节将会介绍控制信息头与程序将会用来管理这些信息的宏。 简介struct cmsghdr结构 附 属信息可以包括0,1,或是更多的单独附属数据对象。在每一个对象之前都有一个struct cmsghdr结构。头部之后是填充字节,然后是对象本身。最后,附属数据对象之后,下一个cmsghdr之前也许要有更多的填充字节。在这一章,我们将 要关注的附属数据对象是文件描述符与证书结构。 下图显示了一个包含附属数据的缓冲区是如何组织的。 我们需要注意以下几点: cmsg_len与CMSG_LEN()宏值所显示的长度相同。 CMSG_SPACE()宏可以计算一个附属数据对象的所必需的空白。 msg_controllen是CMSG_SPACE()长度之后,并且为每一个附属数据对象进行计算。 控制信息头部本身由下面的C结构定义: struct cmsghdr {     socklen_t cmsg_len;     int       cmsg_level;     int       cmsg_type; /* u_char     cmsg_data[]; */ }; 其成员描述如下: 成员        描述 cmsg_len    附属数据的字节计数,这包含结构头的尺寸。这个值是由CMSG_LEN()宏计算的。 cmsg_level    这个值表明了原始的协议级别(例如,SOL_SOCKET)。 cmsg_type    这个值表明了控制信息类型(例如,SCM_RIGHTS)。 cmsg_data    这个成员并不实际存在。他用来指明实际的额外附属数据所在的位置。 这一章所用的例子程序只使用SOL_SOCKET的cmsg_level值。这一章我们感兴趣的控制信息类型如下(cmsg_level=SOL_SOCKET): cmsg_level        描述 SCM_RIGHTS        附属数据对象是一个文件描述符 SCM_CREDENTIALS        附属数据对象是一个包含证书信息的结构 简介cmsg(3)宏 由于附属数据结构的复杂性,Linux系统提供了一系列的C宏来简化我们的工作。另外,这些宏可以在不同的UNIX平台之间进行移植,并且采取了一些措施来防止将来的改变。这些宏是由cmsg(3)的man手册页来进行描述的,其概要如下: #include struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh); struct cmsghdr *CMSG_NXTHDR(struct msghdr *msgh, struct cmsghdr *cmsg); size_t CMSG_ALIGN(size_t length); size_t CMSG_SPACE(size_t length); size_t CMSG_LEN(size_t length); void *CMSG_DATA(struct cmsghdr *cmsg); CMSG_LEN()宏 这个宏接受我们希望放置在附属数据缓冲区中的对象尺寸作为输入参数。如果我们回顾一个我们前面的介绍,我们就会发现这个宏会计算cmsghdr头结构加上所需要的填充字符的字节长度。这个值用来设置cmsghdr对象的cmsg_len成员。 下面的例子演示了如果附属数据是一个文件描述符,我们应如何来计算cmsg_len成员的值: int fd;   /* File descriptor */ printf("cmsg_len = %d/n",CMSG_LEN(sizeof fd)); CMSG_SPACE()宏 这个宏用来计算附属数据以及其头部所需的总空白。尽管CMSG_LEN()宏计算了一个相似的长度,CMSG_LEN()值并不包括可能的结尾的填充字符。CMSG_SPACE()宏对于确定所需的缓冲区尺寸是十分有用的,如下面的示例代码所示: int fd; /* File Descriptor */ char abuf[CMSG_SPACE(sizeof fd)]; 这个例子在abuf[]中声明了足够的缓冲区空间来存放头部,填充字节以及附属数据本身,和最后的填充字节。如果在缓冲区中有多个附属数据对象,一定要同时添加多个CMSG_SPACE()宏调用来得到所需的总空间。 CMSG_DATA()宏 这个宏接受一个指向cmsghdr结构的指针。返回的指针值指向跟随在头部以及填充字节之后的附属数据的第一个字节(如果存在)。如果指针mptr指向一个描述文件描述符的可用的附属数据信息头部,这个文件描述符可以用下面的代码来得到: struct cmsgptr *mptr; int fd; /* File Descriptor */ . . . fd = *(int *)CMSG_DATA(mptr); CMSG_ALIGN()宏 这是一个Linux扩展宏,而不是Posix.1g标准的一部分。指定一个字节长度作为输入,这个宏会计算一个新的长度,这个新长度包括为了维护对齐所需要的额外的填充字节。 CMSG_FIRSTHDR()宏 这 个宏用于返回一个指向附属数据缓冲区内的第一个附属对象的struct cmsghdr指针。输入值为是指向struct msghdr结构的指针(不要与struct cmsghdr相混淆)。这个宏会估计msghdr的成员msg_control与msg_controllen来确定在缓冲区中是否存在附属对象。然 后,他会计算返回的指针。 如果不存在附属数据对象则返回的指针值为NULL。否则,这个指针会指向存在的第一个struct cmsghdr。这个宏用在一个for循环的开始处,来开始在附属数据对象中遍历。 CMSG_NXTHDR()宏 这个用于返回下一个附属数据对象的struct cmsghdr指针。这个宏会接受两个输入参数: 指向struct msghdr结构的指针 指向当前struct cmsghdr的指针 如果没有下一个附属数据对象,这个宏就会返回NULL。 遍历附属数据 当接收到一个附属数据时,我们可以使用CMSG_FIRSTHDR()与CMSG_NXTHDR()宏来在附属数据对象中进行遍历。下面的示例代码显示了for循环的通常格式以及宏的相应用法: struct msghdr msgh;     /* Message Hdr */ struct cmsghdr *cmsg;0   /*Ptr to ancillary hdr */ int *fd_ptr;            /* Ptr to file descript.*/ int received_fd;        /* The file descriptor */ for ( cmsg=CMSG_FIRSTHDR(&msgh); cmsg!=NULL; cmsg=CMSG_NXTHDR(&msgh,cmsg) ) {     if ( cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS ) {         fd_ptr = (int *) CMSG_DATA(cmsg);         received_fd = *fd_ptr;         break;     } } if ( cmsg == NULL ) {     /* Error: No file descriptor recv'd */ } 创建附属数据 要发送一个文件描述符的进程必须使用正确的格式化数据来创建一个附属数据缓冲区。下面的代码展示的通常的创建过程: struct msghdr msg;            /* Message header */ struct cmsghdr *cmsg; /* Ptr to ancillary hdr */ int fd;              /* File descriptor to send */ char buf[CMSG_SPACE(sizeof fd)]; /* Anc. buf */ int *fd_ptr;           /* Ptr to file descriptor */ msg.msg_control = buf; msg.msg_controllen = sizeof buf; cmsg = CMSG_FIRSTHDR(&msg); cmsg->cmsg_level = SOL_SOCKET; cmsg->cmsg_type = SCM_RIGHTS; cmsg->cmsg_len = CMSG_LEN(sizeof fd); /* Initialize the payload: */ fd_ptr = (int *)CMSG_DATA(cmsg); *fd_ptr = fd; /* * Sum of the length of all control * messages in the buffer: */ msg.msg_controllen = cmsg->cmsg_len;

    最新回复(0)