UNIX网络编程卷一 学习笔记 第十七章 ioctl操作

ioctl函数传统上一直作为那些不适合归入现有已定义的类别的系统接口。POSIX正在通过创建特定的包装函数来代替ioctl函数的某些功能,以取而代之的是那些已被POSIX标准化的函数。例如,Unix终端接口传统上使用ioctl函数访问,而POSIX为终端创造了12个新函数,如tcgetattr函数获取终端属性、tcflush函数用于冲刷待处理输入和输出。类似地,POSIX替换了一个用于网络的ioctl函数请求:新的sockatmark函数取代ioctl函数的SIOCATMARK参数。但与网络编程相关且依赖于实现的特性保留的ioctl函数请求仍然很多,它们用于获取接口信息、访问路由表、访问ARP高速缓存等。

ioctl函数影响由fd参数引用的打开文件描述符:
在这里插入图片描述
第三个参数总是一个指针,其类型依赖于request参数。

4.4 BSD把第二个参数定义为unsigned long而非int,这不成问题,因为用作这个参数的常值由头文件定义,只要这些常值在作用域内(即调用了ioctl函数的程序include了unistd.h头文件),那么用的就是正确类型的常值。

一些实现把第三个参数指定为void *,而非ANSI C省略号记法。ANSI C中,省略号表示参数列表中的可变部分。

POSIX未对定义iotcl函数原型的头文件进行标准化,许多系统在unistd.h头文件中定义它,但传统的BSD系统在sys/ioctl.h头文件中定义它。

我们可以把和网络相关的请求分为6类:
1.套接字操作。

2.文件操作。

3.接口操作。

4.ARP高速缓存操作。

5.路由表操作。

6.流系统。

某些ioctl函数操作和某些fcntl函数操作功能重叠(如把套接字设置为非阻塞),而且某些操作可用ioctl函数以多种方式指定(如设置套接字的进程组属主)。

以下是request参数的取值及其对应的arg指针参数指向的类型:
在这里插入图片描述
明确用于套接字操作的request参数:
1.SIOCATMARK:如果套接字的读指针位于带外标记,就通过第三个参数指向的整数返回一个非0值,否则返回0值。POSIX用sockatmark函数替换本请求。

2.SIOCGPGRP:通过第三个参数指向的整数返回本套接字的进程ID或进程组ID,该ID指定对本套接字的SIGIO或SIGURG信号的接收进程。本请求和fcntl函数的F_GETOWN命令等效,且POSIX标准化的是fcntl操作。

3.SIOCSPGRP:把本套接字的进程ID或进程组ID设置成由第三个参数指向的整数,该ID指定本套接字的SIGIO或SIGURG信号的接收进程。本请求和fcntl函数的F_SETOWN命令等效,且POSIX标准化的是fcntl操作。

用于套接字操作,且可能还适用于某些特定类型文件的request参数:
1.FIONBIO:根据ioctl函数的第三个参数指向一个0值或非0值,可清除或设置本套接字的非阻塞式IO标志。本请求和O_NONBLOCK文件状态标志等效,可通过fcntl函数的F_SETFL命令清除或设置O_NONBLOCK标志。

2.FIOASYNC:根据ioctl函数的第三个参数指向一个0值或非0值,可清除或设置本套接字的信号驱动异步IO标志。本请求决定是否会接收针对本套接字的异步IO信号(SIGIO)。本请求和O_ASYNC文件状态标志等效,可通过fcntl函数的F_SETFL命令清除或设置O_ASYNC标志。

3.FIONREAD:通过由ioctl函数的第三个参数指向的整数返回当前在本套接字接收缓冲区中的字节数。本特性也适用于文件、管道、终端。

4.FIOSETOWN:对于套接字等价于SIOCSPGRP。

5.FIOGETOWN:对于套接字等价于SIOCGPGRP。

需要处理网络接口的许多程序首先要从内核获取配置在系统中的所有接口,本任务由SIOCGIFCONF请求完成,它使用ifconf结构,ifconf结构中包含ifreq结构,以下是这两个结构的定义:
在这里插入图片描述
以SIOCGIFCONF为参数调用ioctl前我们要先分配一个缓冲区和一个ifconf结构,然后初始化ifconf结构,下图是初始化ifconf结构的结果,其中假设缓冲区大小为1024字节,ioctl函数的第三个参数指向该结构:
在这里插入图片描述
假设内核返回2个ifreq结构,在ioctl函数返回时该ifconf结构的值如下:
在这里插入图片描述
如上图,阴影区域是被ioctl函数修改过的部分,缓冲区中填入了2个ifreq结构,ifconf结构的ifc_len成员也被更新,以指示存放在缓冲区中的信息量,上图假设每个ifreq结构占32字节。

ifreq结构中含有一个联合,而#define隐藏了这些字段是联合的成员这一事实。有些系统往ifr_ifru联合中增添了许多依赖于实现的成员。

我们开发一个名为get_ifi_info的函数,它返回一个结构链表,其中每个结构对应当前处于up状态的接口,我们将使用ioctl函数的SIOCGIFCONF参数实现这个函数。

FreeBSD提供了一个实现类似功能的函数,名为getifaddrs。

但搜索FreeBSD 4.8的整个源代码树(指在软件开发中,代码文件和目录组织结构形成的一种层次化的树状结构),发现有12个程序使用SIOCGIFCONF参数调用ioctl函数以确定存在的接口。

为开发get_ifi_info函数,我们先编写一个名为unpifi.h的头文件,其中定义一些结构:

/* Our own header for the programs that need interface configuration info.
   Include this file, instead of "unp.h". */

#ifndef __unp_ifi_h
#define __unp_ifi_h

#include "unp.h"
#include <net/if.h>

#define IFI_NAME 16    /* same as IFNAMSIZ in <net/if.h> */
// 以太网硬件地址(MAC地址)长度为6字节,此处定义为8字节是为了兼容未来的8字节唯一标识符(EUI-64)
#define IFI_HADDR 8    /* allow for 64-bit EUI-64 in future */

struct ifi_info {
    char ifi_name[IFI_NAME];    /* inter face name, null-terminated */
    short ifi_index;    /* interface index */
    short ifi_mtu;    /* interface MTU */
    u_char ifi_haddr[IFI_HADDR];    /* hardware address(如以太网地址) */
    u_short ifi_hlen;    /* # bytes in hardware address: 0, 6, 8 */
    /* IFF_xxx constants from <net/if.h>(用来判断接口是否支持广播或多播、是否是一个点到点接口等) */
    short ifi_flags;    
    short ifi_myflags;    /* our own IFI_xxx flags */
    struct sockaddr *ifi_addr;    /* primary address */
    struct sockaddr *ifi_brdaddr;    /* broadcast address */
    struct sockaddr *ifi_dstaddr;    /* destination address(点到点链路的目的地址) */
    struct ifi_info *ifi_next;    /* next of these structures */
};

// 是否返回接口别名地址
#define IFI_ALIAS 1    /* ifi_addr is an alias */

/* function prototypes */
// 此函数获取一个ifi_info结构组成的链表
struct ifi_info *get_ifi_info(int, int);
struct ifi_info *Get_ifi_info(int, int);
// 用于存放ifi_info结构和其中所含套接字地址结构的内存空间都是动态获取的,此函数用来释放动态获取的内存空间
void free_ifi_info(struct ifi_info *);

#endif /* __unp_ifi_h */

在给出get_ini_info函数的实现前,先给出一个调用该函数并输出所有信息的程序,该程序是ifconfig程序的微型版本:

#include "unpifi.h"

int main(int argc, char **argv) {
    struct ifi_info *ifi, *ifihead;
    struct sockaddr *sa;
    u_char *ptr;
    int i, family, doaliases;

    if (argc != 3) {
        err_quit("usage: prifinfo <inet4|inet6> <doaliases>");
    }

    if (strcmp(argv[1], "inet4") == 0) {
        family = AF_INET;
    } else if (strcmp(argv[1], "inet6") == 0) {
        family = AF_INET6;
    } else {
        err_quit("invalid <address-family>");
    }
    doaliases = atoi(argv[2]);

    for (ifihead = ifi = Get_ifi_info(family, doaliases); ifi != NULL; ifi = ifi->ifi_next) {
        printf("%s: ", ifi->ifi_name);
		if (ifi->ifi_index != 0) {
		    printf("(%d) ", ifi->ifi_index);
		}
		printf("<");
		if (ifi->ifi_flags & IFF_UP) {
		    printf("UP ");
		}
		if (ifi->ifi_flags & IFF_BROADCAST) {
		    printf("BCAST ");
		}
		if (ifi->ifi_flags & IFF_MULTICAST) {
		    printf("MCAST ");
		}
		if (ifi->ifi_flags & IFF_LOOPBACK) {
		    printf("LOOP ");
		}
		if (ifi->ifi_flags & IFF_POINTOPOINT) {
		    printf("P2P ");
		}
		printf(">\n");
	
	    // 如果无法得到硬件地址,则ifi_hlen成员值为0
		if ((i = ifi->ifi_hlen) > 0) {
		    ptr = ifi->ifi_haddr;
		    do {
		        // 将硬件地址显示为16进制数形式,每个十六进制数用冒号分隔
		        // 硬件地址格式为00:14:22:01:23:45,即用冒号分开每个字节的十六进制表示
		        printf("%s%x", (i == ifi->ifi_hlen) ? "  " : ":", *ptr++);
		    } while (--i > 0);
		    printf("\n");
		}
		if (ifi->ifi_mtu != 0) {
		    printf("  MTU: %d\n", ifi->ifi_mtu);
		}
	
		if ((sa = ifi->ifi_addr) != NULL) {
		    printf("  IP addr: %s\n", Sock_ntop_host(sa, sizeof(*sa)));
		}
		if ((sa = ifi->ifi_brdaddr) != NULL) {
		    printf("  broadcast addr: %s\n", Sock_ntop_host(sa, sizeof(*sa)));
		}
		if ((sa = ifi->ifi_dstaddr) != NULL) {
		    printf("  destination addr: %s\n", Sock_ntop_host(sa, sizeof(*sa)));
		}
		free_ifi_info(ifihead);
		exit(0);
    }
}

在主机macosx上执行以上代码:
在这里插入图片描述
上图中,第一个命令行参数inet4指定IPv4地址,第二个命令行参数0指定不返回地址别名。在MacOS X系统上,我们用这种方法无法得到以太网接口的硬件地址。

多宿主机的传统定义是具有多个接口的主机,如两个以太网链路或一个以太网链路加一个点到点链路。每个接口必须有唯一的一个IPv4地址。计量一个主机的接口数是否大于1个以确定它是否是多宿主机时,环回接口不计在内。

按照约定,地址127.0.0.1赋予环回接口,任何发送到这个IP地址的分组在内部被环送回来作为IP模块的输入,因此这个分组不会出现在网络上。我们在同一主机上测试客户和服务器程序时常用该地址。该地址为人所知的名字是INADDR_LOOPBACK。

网络127.0.0.1/8上任何地址都可以赋予环回接口,但127.0.0.1是最常用的,往往由系统自动配置。

路由器按照定义是多宿的,因为它把到达某个接口的分组转发到另一个接口。而多宿主机不必是一个路由器,除非它转发分组。一个多宿主机不应仅仅因为拥有多个接口而认定是一个路由器,除非它被配置成作为路由器(通常由系统管理员开启某个配置选项)。

但多宿这一说法已变得更一般化,包括两种情形:
1.拥有多个接口的主机是多宿的,每个接口必须有各自的IP地址,未指定网络地址的接口允许出现在点到点链路上。这是传统的定义。

2.较新的主机具备把多个IP地址赋予单个物理接口的能力。除第一个IP地址(主地址)外的每个额外IP地址称为该接口的一个别名地址或逻辑接口地址。通常别名地址与主地址共享同一个子网地址,但主机ID不同(即IP地址中的主机号部分不同)。但别名地址也可能具有完全不同于主地址的网络地址或子网地址。

可见多宿主机的定义是具有多个IP层可见接口(除了环回接口)的主机,不关心这些接口是物理的还是逻辑的。

网桥是一种用于连接多个局域网(LAN)的设备(数据链路层设备),它基于MAC地址来转发数据帧。网桥的主要功能是将数据帧从一个接口转发到另一个接口,以便在不同的局域网之间实现通信。以太网交换机在数据链路层上工作,它可以被视为一种高级形式的网桥,它在功能上类似于网桥,但通常具有更多的接口和更高的性能。

给予网络负荷极高的某个服务器主机到同一个以太网交换机的多个物理连接,并把这些连接汇聚成一个更高带宽的逻辑连接是常见的,但这样的主机不能因为拥有多个物理接口而被认为是多宿的,因为在IP层看来只有一个逻辑接口。

在另一个上下文中,有多个连接通往因特网的网络也称为多宿的,例如有些网点有两个而非一个通往因特网的连接,以此提供因特网接入的备份能力。SCTP协议能通过多宿网点获益。

如果给以太网接口en1增设3个别名地址(它们的主机ID分别为79、80、81),并且把第二个命令行参数改为1,会得到以下结果:
在这里插入图片描述
在第十八章中,我们会给出另一个get_ifi_info函数的实现,该实现可以很容易地获取硬件地址,在FreeBSD上运行第十八章中的该程序,可得到以下结果:
在这里插入图片描述
上图中我们指示程序输出别名地址,发现以太网接口de1定义了一个主机ID为93的别名。

以下是以SIOCGIFCONF为参数调用ioctl版本的get_ifi_info函数,它从内核获取接口配置:

#include "unpifi.h"

struct ifi_info *get_ifi_info(int family, int doaliases) {
    struct ifi_info *ifi, *ifihead, **ifipnext;
    int sockfd, len, lastlen, flags, myflags, idx = 0, hlen = 0;
    char *ptr, *buf, lastname[IFNAMSIZ], *cptr, *haddr, *sdlname;
    struct ifconf ifc;
    struct ifreq *ifr, ifrcopy;
    struct sockaddr_in *sinptr;
    struct sockaddr_in6 *sin6ptr;

    // 创建一个用于ioctl函数的UDP套接字,也可用TCP套接字
    sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

    lastlen = 0;
    len = 100 * sizeof(struct ifreq);    /* initial buffer size guess */
    // 以下获取SIOCGIFCONF请求的结果时使用的是循环,这是因为,对于SIOCGIFOCNF请求
    // 有些实现在缓冲区大小不足以存放结果时不返回错误,而是截断并返回成功
    // 这意味着要知道缓冲区是否足够大的唯一方法是:发出请求,记下返回的长度
    // 然后用更大的缓冲区发出请求,比较返回的长度和刚记下的长度,如果相同,我们的缓冲区才足够大
    // 源自Berkeley的实现在缓冲区太小时不返回错误,结果会被截断成缓冲区的可用大小
    // 即使返回的长度小于缓冲区大小时,我们也不能肯定成功
    // 因为源自Berkeley的实现在剩下的空间装不下另一个结构时返回的长度会小于缓冲区长度
    // Solaris 2.5在返回长度将大于等于缓冲区长度时返回EINVAL错误。
    for (; ; ) {
        buf = Malloc(len);
		ifc.ifc_len = len;
		ifc.ifc_buf = buf;
		
		if (ioctl(sockfd, SIOCGIFCONF, &ifc) < 0) {
		    // 此处是Solaris 2.5判断空间是否足够的过程
		    if (errno != EINVAL || lastlen != 0) {
		        err_sys("ioctl error");
		    }
		} else {
		    // 此处是空间不足时不返回错误的实现判断空间是否足够的过程
		    if (ifc.ifc_len == lastlen) {
		        break;    /* success, len has not changed */
		    }
		    lastlen = ifc.ifc_len;
		}
		len += 10 * sizeof(struct ifreq);    /* increment */
		free(buf);
    }
    ifihead = NULL;
    ifipnext = &ifihead;
    lastname[0] = 0;
    sdlname = NULL;

    for (ptr = buf; ptr < buf + ifc.ifc_len; ) {
        ifr = (struct ifreq *)ptr;
        
// 处理为套接字地址结构提供长度字段的较新系统
#ifdef HAVE_SOCKADDR_SA_LEN
        len = max(sizeof(struct sockaddr), ifr->ifr_addr.sa_len);
// 处理不提供套接字地址结构长度字段的较老系统
// 图17-2指出ifreq结构中包含的套接字地址结构是一个通用套接字地址结构
// 但在较新系统中它可以是任何类型的套接字地址结构
// 事实上,4.4 BSD还会为每个接口返回一个数据链路套接字地址结构
#else
        switch (ifr->ifr_addr.sa_family) {
// ifreq结构中的联合把返回地址定义为通用的16字节sockaddr结构
// 它对于IPv4的16字节sockaddr_in结构是够了
// 对于IPv6的24字节sockaddr_in6结构却太小
// 因此,如果套接字地址结构中有长度成员,就使用该成员,即上面使用max函数的语句
#ifdef IPV6
        case AF_INET6:
	    	len = sizeof(struct sockaddr_in6);
	    break;
#endif
        case AF_INET:
		default:
		    len = sizeof(struct sockaddr);
		    break;
		}
#endif /* HAVE_SOCKADDR_SA_LEN */
        ptr += sizeof(ifr->ifr_name) + len;    /* for next one in buffer */

#ifdef HAVE_SOCKADDR_DL_STRUCT
        /* assumes that AF_LINK precedes AF_INET or AF_INET6 */
		if (ifr->ifr_addr.sa_family == AF_LINK) {
		    struct sockaddr_dl *sdl = (struct sockaddr_dl *)&ifr->ifr_addr;
		    sdlname = ifr->ifr_name;
		    idx = sdl->sdl_index;
		    haddr = sdl->sdl_data + sdl->sdl_nlen;
		    hlen = sdl->sdl_alen;
		}
#endif

        // AF_LINK会在此处被跳过
        if (ifr->ifr_addr.sa_family != family) {
		    continue;    /* ignore if not desired address family */
		}
	
	    // 处理别名地址
		myflags = 0;
		// Solaris用于别名地址的接口名中有一个冒号
		// 而4.4 BSD不在名字上区分别名地址和主地址
		if ((cptr = strchr(ifr->ifr_name, ':')) != NULL) {
		    *cptr = 0;    /* replace colon with null */
		}
		// 把最近处理过的接口名存入lastname
		// 在与当前遍历到的接口名字比较时,若有冒号则只比较到冒号
		if (strncmp(lastname, ifr->ifr_name, IFNAMSIZ) == 0) {
		    if (doaliases == 0) {
		        continue;    /* already processed this interface */
		    }
		    myflags = IFI_ALIAS;
		}
		memcpy(lastname, ifr->ifr_name, IFNAMSIZ);
	
	    // 获取接口标志
	    // 不能用当前遍历到的ifreq结构调用ioctl
	    // 因为接口标志和IP地址在ifreq结构中是同一个联合的不同成员,用当前的ifreq结构会被覆盖
		ifrcopy = *ifr;    // 这里ifrcopy结构中的接口名字段就被赋值为当前遍历到的接口名
		                   // 之后其他ioctl函数的接口操作请求都对应该接口名关联的接口
		Ioctl(sockfd, SIOCGIFFLAGS, &ifrcopy);
		flags = ifrcopy.ifr_flags;
		if ((flags & IFF_UP) == 0) {
		    continue;    /* ignore if interface not up */
		}
		
		// 动态分配一个ifi_info结构,并将它加到链表末尾
		ifi = Calloc(1, sizeof(struct ifi_info));
        *ifipnext = ifi;    /* prev points to this new one */
		ifipnext = &ifi->ifi_next;    /* pointer to next one goes here */
	        
	    // 把接口标志复制到当前ifi_info结构中
		ifi->ifi_flags = flags;    /* IFF_xxx values */
		ifi->ifi_myflags = myflags;    /* IFI_xxx values */

// 把MTU复制到当前ifi_info结构中
#if defined(SIOCGIFMTU) && defined(HAVESTRUCT_IFREQ_IFR_MTU)
        Ioctl(sockfd, SIOCGIFMTU, &ifrcopy);
		ifi->ifi_mtu = ifrcopy.ifr_mtu;
#else
        ifi->ifi_mtu = 0;
#endif

        // 把接口名复制到当前ifi_info结构中
        memcpy(ifi->ifi_name, ifr->ifr_name, IFI_NAME);
        ifi->ifi_name[IFI_NAME - 1] = '\0';
		/* If the sockaddr_dl is from a different interface, ignore it */
		// 此处说明,我们已经获取过该接口的数据链路地址结构了
		// 即我们假设一个接口的数据链路地址结构先于IPv4或IPv6地址结构返回
		if (sdlname == NULL || strcmp(sdlname, ifr->ifr_name) != 0) {
		    idx = hlen = 0;
		}
		// 把接口索引和硬件地址长度复制到当前ifi_info结构中
		ifi->ifi_index = idx;
		ifi->ifi_hlen = hlen;
		if (ifi->ifi_hlen > IFI_HADDR) {
		    ifi->ifi_hlen = IFI_HADDR;
		}
		// 如果有硬件地址,把接口硬件地址复制到当前ifi_info结构中
		if (hlen) {
		    memcpy(ifi->ifi_haddr, haddr, ifi->ifi_hlen);
		}
		
		switch (ifr->ifr_addr.sa_family) {
		case AF_INET:
		    // 把由最初的SIOCGIFCONF请求返回的IP复制到当前ifi_info结构
            sinptr = (struct sockaddr_in *)&ifr->ifr_addr;
		    ifi->ifi_addr = Calloc(1, sizeof(struct sockaddr_in));
		    memcpy(ifi->ifi_addr, sinptr, sizeof(struct sockaddr_in));

// 如果当前接口支持广播,用SIOCGIFBRDADDR为参数调用ioctl获取它的广播地址
#ifdef SIOCGIFBRDADDR
            if (flags & IFF_BROADCAST) {
		        Ioctl(sockfd, SIOCGIFBRDADDR, &ifrcopy);
				sinptr = (struct sockaddr_in *)&ifrcopy.ifr_broadaddr;
				ifi->ifi_brdaddr = Calloc(1, sizeof(struct sockaddr_in));
				memcpy(ifi->ifi_brdaddr, sinptr, sizeof(struct sockaddr_in));
		    }
#endif

#ifdef SIOCGIFDSTADDR
            if (flags & IFF_POINTOPOINT) {
	        	Ioctl(sockfd, SIOCGIFDSTADDR, &ifrcopy);
				sinptr = (struct sockaddr_in *)&ifrcopy.ifr_dstaddr;
				ifi->ifi_dstaddr = Calloc(1, sizeof(struct sockaddr_in));
				memcpy(ifi->ifi_dstaddr, sinptr, sizeof(struct sockaddr_in));
		    }
#endif
            break;

        // IPv6与IPv4情况类似,但IPv6不支持广播
		case AF_INET6:
		    sin6ptr = (struct sockaddr_in6 *)&ifr->ifr_addr;
		    ifi->ifi_addr = Calloc(1, sizeof(struct sockaddr_in6));
		    memcpy(ifi->ifi_addr, sin6ptr, sizeof(struct sockaddr_in6));

#ifdef SIOCGIFDSTADDR
            if (flags & IFF_POINTOPOINT) {
		        Ioctl(sockfd, SIOCGIFDSTADDR, &ifrcopy);
				sin6ptr = (struct sockaddr_in6 *)&ifrcopy.ifr_dstaddr;
				ifi->ifi_dstaddr = Calloc(1, sizeof(struct sockaddr_in6));
				memcpy(ifi->ifi_dstaddr, sin6ptr, sizeof(struct sockaddr_in6));
		    }
#endif
            break;

		default:
	 	    break;
		}
    }
    free(buf);
    return ifihead;    /* pointer to first structure in linked list */
}

void free_ifi_info(struct ifi_info *ifihead) {
    struct ifi_info *ifi, *ifinext;

    for (ifi = ifihead; ifi != NULL; ifi = ifinext) {
        if (ifi->ifi_addr != NULL) {
		    free(ifi->ifi_addr);
		}
		if (ifi->ifi_brdaddr != NULL) {
		    free(ifi->ifi_brdaddr);
		}
		if (ifi->ifi_dstaddr != NULL) {
		    free(ifi->ifi_dstaddr);
		}
		ifinext = ifi->ifi_next;    /* can't fetch ifi_next after free */
		free(ifi);    /* the ifi_info{} itself */
    }
}

有些实现提供了用于返回接口数目的名为SIOCGIFNUM的请求,它使应用能在发出SIOCGIFCONF请求前分配足够的缓冲区,但这个新请求尚未被广泛实现。

随着Web的增长,为SIOCGIFCONF请求返回的结果预分配一个固定长度的缓冲区这一做法也成了问题,因为大的Web服务器把越来越多的别名地址赋予单个接口,例如,Solaris 2.5对于每个接口可赋予的别名地址数限制为256,Solaris 2.6则把该限制增加到8192。使用大量别名地址的网站已经发现使用固定大小缓冲区获取接口信息的程序开始工作失常。尽管Solaris在缓冲区太小时返回错误,但很多程序只是分配固定大小的缓冲区,发出ioctl请求,却不处理可能返回的错误,导致进程可能意外死亡。

ioctl函数的SIOCGIFOCNF请求为每个已配置接口返回其名字及一个套接字地址结构,我们接着可以发出其他接口类请求设置或获取每个接口的其他特征,这些请求的get版本(SIOCGxxx)通常由netstat程序发出,set版本(SIOCSxxx)通常由ifconfig程序发出。任何用户都能获取接口信息,但设置接口信息需要超级用户权限。

这些接口操作请求接受或返回一个ifreq结构中的信息,而这个结构的地址则作为ioctl函数的第三个参数指定。接口总是以其名字标识,在ifreq结构的ifr_name成员中指定,如le0、lo0、ppp0等。

这些接口操作请求中许多使用套接字地址结构在应用进程和内核间指定或返回具体接口的IP地址或地址掩码,对于IPv4,这个地址或掩码存放在一个网际网套接字地址结构的sin_addr成员中;对于IPv6,它存放在IPv6套接字地址结构的sin6_addr成员中。

ioctl函数的通用接口请求,许多实现中还加入了其他请求:
1.SIOCGIFADDR:在ifr_addr成员中返回单播地址。

2.SIOCSIFADDR:用ifr_addr成员设置接口地址,这个接口的初始化函数也被调用。

3.SIOCGIFFLAGS:在ifr_flags成员中返回接口标志,这些标志的名字格式为IFF_xxx,它们定义在头文件net/if.h中。这些标志指示接口是否处于在工状态(IFF_UP)、是否是一个点到点接口(IFF_POINTOPOINT)、是否支持广播(IFF_BROADCAST)等。

4.SIOCSIFFLAGS:用ifr_flags成员设置接口标志。

5.SIOCGIFDSTADDR:在ifr_dstaddr成员中返回点到点地址。

6.SIOCSIFDSTADDR:用ifr_dstaddr成员设置点到点地址。

7.SIOCGIFBRDADDR:在ifr_broadaddr成员中返回广播地址。应用必须先获取接口标志,然后发出正确请求:广播接口发出本请求,点对点接口应发出SIOCGIFDSTADDR请求。

8.SIOCSIFBRDADDR:用ifr_broadaddr成员设置广播地址。

9.SIOCGIFNETMASK:在ifr_addr成员中返回子网掩码。

10.SIOCSIFNETMASK:用ifr_addr成员设置子网掩码。

11.SIOCGIFMETRIC:在ifr_metric成员中返回接口测度。接口测度由内核为每个接口维护,使用它的是路由守护进程,接口测度被routed加到跳数上(使得某接口更不被看好,RIP使用跳数(Hop Count)作为度量(Metric)来衡量到达目的网络的距离)。

12.SIOCSIFMETRIC:用ifr_metric成员设置接口的路由测度。

ARP高速缓存也通过ioctl函数操纵,但使用路由域套接字的系统往往改用路由套接字访问ARP高速缓存。ioctl函数的ARP高速缓存请求使用arpreq结构,它定义在头文件net/if_arp.h中:
在这里插入图片描述
ioctl函数的ARP高速缓存请求:
1.SIOCSARP:把一个新表项加入ARP高速缓存,或修改其中已经存在的一个表项。arp_pa成员是一个含有IP地址的网际套接字地址结构;arp_ha成员是一个通用套接字地址结构,arp_ha.sa_family值为AF_UNSPEC,arp_ha.sa_data中含有硬件地址(如6字节以太网地址)。应用可指定ATF_PERM和ATF_PUBL标志,但另外两个标志由内核设置。

2.SIOCDARP:从ARP高速缓存中删除一个表项。调用者指定要删除表项的网际网地址。

3.SIOCGARP:从ARP高速缓存中获取一个表项。调用者指定网际网地址,相应的硬件地址和标志一起返回。

只有超级用户才能增加或删除ARP高速缓存表项。以上3个请求通常由arp程序发出。

一些较新系统不支持与ARP相关的ioctl函数请求,而改用路由套接字执行这些ARP操作。

ioctl函数没办法列出ARP高速缓存中的所有表项,当指定-a选项执行arp命令(列出ARP高速缓存中所有表项)时,大多版本的arp程序通过读取内核的内存(通过/dev/kmem文件,在许多UNIX系统中,该文件被用于访问系统内核的虚拟地址空间)获得ARP高速缓存的当前内容。

使用get_ifi_info函数返回一个主机的所有IP地址,然后对每个IP地址发出一个SIOCGARP请求获取它的硬件地址:

#include "unpifi.h"
#include <net/if_arp.h>

int main(int argc, char **argv) {
    int sockfd;
    struct ifi_info *ifi;
    unsigned char *ptr;
    struct arpreq arpreq;
    struct sockaddr_in *sin;

    sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
    // 调用get_ifi_info获取本机所有IPv4地址
    for (ifi = get_ifi_info(AF_INET, 0); ifi != NULL; ifi = ifi->ifi_next) {
        printf("%s: ", Sock_ntop(ifi->ifi_addr, sizeof(struct sockaddr_in)));

		sin = (struct sockaddr_in *)&arpreq.arp_pa;
		memcpy(sin, ifi->ifi_addr, sizeof(struct sockaddr_in));
	
		if (ioctl(sockfd, SIOCGARP, &arpreq) < 0) {
		    err_ret("ioctl SIOCGARP");
		    continue;
		}
	
		ptr = &arpreq.arp_ha.sa_data[0];
		printf("%x:%x:%x:%x:%x:%x\n", *ptr, *(ptr + 1), *(ptr + 2), *(ptr + 3),
		    *(ptr + 4), *(ptr + 5));
    }
    exit(0);
}

在hpux主机上运行以上程序:
在这里插入图片描述
有些系统提供用于操纵路由表的ioctl函数请求,这些请求的第3个ioctl函数的参数是指向rtentry结构的指针,该结构定义在net/route.h头文件中,这些请求通常由route程序发出,只有超级用户才能发出这些请求。在支持路由套接字的系统中,这些请求改由路由套接字执行。

操纵路由表的ioctl函数请求:
1.SIOCADDRT:往路由表中增加一个表项。

2.SIOCDELRT:从路由表中删除一个表项。

ioctl函数不能列出路由表中所有表项,这个操作通常由netstat程序在指定-r选项时执行,netstat程序通过读取内核的内存(/dev/kmem)获得整个路由表。

上面说过,由SIOCGIFBRDADDR请求返回的广播地址是通过ifreq结构的ifr_broadaddr成员返回的,但TCPv2一书的173页中,我们注意到该广播地址是在ifr_dstaddr成员中返回的,这里没有问题,因为union的前3个成员都是套接字地址结构。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值