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

2023-11-10

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个成员都是套接字地址结构。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

  • Unix 中的访问时间是多少

    我想知道访问时间是多少 我在网上搜索但得到了相同的定义 读 被改变 我知道与touch我们可以改变它 谁能用一个例子来解释一下它是如何改变的 有没有办法在unix中获取创建日期 时间 stat结构 The stat 2 结构跟踪所有文件日期
  • 关闭 python 后让进程保持运行

    我希望在终止原始进程后继续运行进程 以下代码在终止原始进程后不会使其进程保持运行 args yes Popen args shell True stdout None stdin None stderror None 我已经尝试了我能想到的
  • 使用 wget 从 Google Scholar 搜索结果下载所有 pdf 文件

    我想写一个简单的网络蜘蛛或者只是使用wget从谷歌学术下载 pdf 结果 这实际上是获取研究论文的一种非常巧妙的方式 我已阅读 stackoverflow 上的以下页面 使用wget爬取网站并限制爬取链接总数 https stackover
  • BASH 中带有千位分隔符的数字格式

    我有一个号码12343423455 23353 我想用千位分隔符格式化数字 所以输出将是12 343 423 455 23353 printf 3f n 12345678 901 12 345 678 901
  • grep 查找 Unix 中的特殊字符

    我有一个日志文件 application log 其中可能包含以下多行普通和特殊字符字符串 Q 我想搜索包含这个特殊字符串的行号 grep Q application log 上述命令不返回任何结果 获取行号的正确语法是什么 Tell gr
  • shell脚本中是否有互斥/信号量机制?

    我正在 shell 脚本中寻找互斥 信号量 并发机制 考虑以下情况 除非 a 用户不关闭共享文件 否则 b 用户应该无法打开 更新它 我只是想知道如何在 shell 脚本中实现互斥量 信号量 临界区等 在 shell 脚本中实现锁定机制 文
  • 如果目录不存在,有没有办法让 mv 创建要移动到的目录?

    因此 如果我在主目录中并且想将 foo c 移动到 bar baz foo c 但这些目录不存在 是否有某种方法可以自动创建这些目录 以便你只需要输入 mv foo c bar baz 一切都会顺利吗 似乎您可以将 mv 别名为一个简单的
  • import java 导入错误:没有名为 java 的模块

    我似乎遇到了障碍 根本无法解决这个问题 任何人都可以帮我弄清楚为什么我无法导入 java 模块吗 Error Traceback most recent call last File datasource config py line 3
  • 从文件中删除包含非英语 (Ascii) 字符的行

    我有一个文本文件 其中包含来自不同语言的字符 例如 中文 拉丁文等 我想删除包含这些非英语字符的所有行 我想包含所有英文字符 a b 数字 0 9 和所有标点符号 我如何使用 awk 或 sed 等 unix 工具来完成此操作 Perl 支
  • 将 cron 作业配置为在 Jenkins 上每 15 分钟运行一次

    如何在 Jenkins 上每 15 分钟运行一次 cron 作业 这是我尝试过的 在 Jenkins 上 我使用以下 cron 语法设置了每 15 分钟运行一次的作业 14 但该作业每小时执行一次 而不是 15 分钟 我收到有关 cron
  • 如何在Linux中获取带有图标的活动应用程序

    我想找到一种方法获取活动应用程序的列表及其名称和图标 实际上 我正在使用此命令来获取所有活动进程 wmctrl lp 示例输出 0x03800002 0 3293 user notebook XdndCollectionWindowImp
  • 如何从C程序执行C程序的shell中更改环境变量?

    我想改变的值PATHC 程序中的变量 然后在运行该程序的 shell 中查看更改后的值 做这样的事情 include
  • 使用 unix ksh shell 脚本或 perl 脚本监视文件夹中的新文件并触发 perl 脚本

    我已经在谷歌搜索和溢出了一段时间 但找不到任何可用的东西 我需要一个脚本来监视公共文件夹并在创建新文件时触发 然后将文件移动到私有位置 我有一个 samba 共享文件夹 exam ple 在 UNIX 上映射到X 在窗户上 在某些操作中 t
  • 为什么 ls -l 中的“总计”加起来不等于列出的总文件大小? [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 为什么是total在输出中ls l打印为64并不是26078列出的所有文件的总数是多少 ls l test ls total 64 rw
  • 如何在 VIm 和终端中始终拥有相同的当前目录?

    我希望我的终端当前目录跟随我的 VIM 目录 Example 在终端中 gt pwd gt Users rege gt vim 然后在VIM中 cd Users rege project
  • 使用 awk 处理多个文件

    我必须使用 awk 处理大量 txt 文件 每个文件 1600 万行 我必须阅读例如十个文件 File 1 en sample 1 200 en n sample 2 10 en sample 3 10 File 2 en sample 1
  • 为什么我的信号处理程序只执行一次?

    我正在 UNIX 和 C 中处理信号 并遇到了这个问题 我正在尝试编写一个计数到 10 的程序 每秒一个数字 当用户尝试使用 SIGINT 如 CTRL C 中断它时 它会打印一条消息 告诉它无论如何都会继续计数 到目前为止 我得到了这个
  • 在 unix bash 脚本中可以嵌套 Here Document 吗?

    是否可以在另一个heredoc中编写一个heredoc ssh T q yxz server1 lt lt END TEXT ssh T q abc server2 lt lt SUB TEXT SUB TEXT END TEXT Yes
  • 如何从 HTTP URL 下载文件?

    我知道如何使用wget从FTP下载但我无法使用wget从以下链接下载 如果您将其复制并粘贴到浏览器中 它将开始下载 但我想将它直接下载到我们的服务器 这样我就不需要将它从我的桌面移动到服务器 我该怎么做 Thanks 这就是我所做的 wge
  • 如何在 *nix 中登录时运行脚本?

    我知道我曾经知道如何做到这一点 但是 如何在 unix 中登录时运行脚本 bash 可以 From 维基百科 Bash http en wikipedia org wiki Bash 28Unix shell 29 当 Bash 启动时 它

随机推荐

  • 【Qt】串口通讯

    Qt串口通信基础及名词说明 1 串口通信 1 波特率 2 数据位 3 停止位 4 奇偶校验位 2 名词介绍简单版 1 起始位 2 数据位 3 校验位 4 停止位 5 空闲位 2 Qt串口通信模块QtSerialPort简介 1 QSeria
  • Jmeter--记录一个使用CSV Data Set Config犯的低级错误

    关于Jmeter的这个元件CSV Data Set Config网上已有大量篇幅的文章去接受 并且介绍的都挺详细 这里就不再介绍 这里主要介绍第一次使用此元件时犯的错误 当脚本完成后 点击运行 但是在察看结果树中没有发现运行结果 可以说点击
  • SpringBoot 如何保证接口安全?老鸟们都是这么玩的!

    为什么要保证接口安全 对于互联网来说 只要你系统的接口暴露在外网 就避免不了接口安全问题 如果你的接口在外网裸奔 只要让黑客知道接口的地址和参数就可以调用 那简直就是灾难 举个例子 你的网站用户注册的时候 需要填写手机号 发送手机验证码 如
  • 23年找工作的心酸历程

    前几天在脉脉上看到一个热议话题 23年找工作的心酸历程 大家都知道近几年互联网大环境不好 找工作变得越来越卷了 就算是BAT这种大厂出来的 也不见得就有多好找工作 可想而知 如果你的背景和能力不是特别强 很有可能练简历关都过不了 特别是工作
  • SpringBoot 快速整合SpringDataJPA (基础篇)

    序言 SpringDataJPA秉承大道至简的设计理念 给我们的数据层开发带来的极大的便利 诸如基于注解就可完成实体 数据库的映射关系 提供自带的通用Repo接口 接口方法约定名称即可实现数据访问等特性都是值得称赞的功能 正文 Spring
  • Flutter踩坑之 Android license status unknown 解决方案

    问题引入 Mac上搭建Flutter开发环境文章中 在使用flutter doctor查看是否需要安装其它依赖项时 检测出三个问题 Doctor found issues in 3 categories 其中有关Android平台的报错如下
  • C++ sort()函数

    C 中的sort 函数是用于对容器 如数组 向量 链表等 中的元素进行排序的标准库函数 它使用了一种称为快速排序 quicksort 的排序算法 通常具有较好的性能 sort 函数位于
  • element 表格二次封装

  • Mac office 2016 word 出现隐藏模块中出现编译错误: link

    try to remove Library Group Containers UBF8T346G9 Office User Content Startup Word linkCreation dotm or whatever in that
  • Redis单机版全面讲解

    目录 常识 什么是redis redis为什么快 redis作为实例安装在系统中 redis数据类型 redis命令 String类型相关命令 list类型相关命令 hash类型相关命令 set类型相关命令 zset类型相关命令 redis
  • LAN8720A网络模块的使用问题

    一 LAN8720A模块驱动电路 最近在调试STM32F4驱动LAN8720A网络模块 在做方案前参考是正点原子的LAN8720A的驱动电路方案 但是从网上买回来的LAN8720A模块用正点原子的例程一直驱动不起来 在windows系统下一
  • vue提示插件[vscode]

    在VSCode Marketplace 搜素Vue 出现关于语法高亮的插件有 vue vue beautify vue color VueHelper vertur等等 比较了下载数量可以了解到 vetur 是目前比较好的语法高亮插件 我们
  • Eclipse使用(Java基础)&Spring boot学习(一)

    Eclipse安装 这个很简单 搜索一下Eclipse下载即可 我是在这里下的 然后选个开发环境 C 的话我会在Visual Studio下写 所以只装了Java 一路next就好 没有什么坑 Hello World Create a Ja
  • HADOOP介绍

    1 HADOOP背景介绍 1 1 什么是HADOOP HADOOP是apache旗下的一套开源软件平台 HADOOP提供的功能 利用服务器集群 根据用户的自定义业务逻辑 对海量数据进行分布式处理 HADOOP的核心组件有 HDFS 分布式文
  • 嵌入式操作系统风云录:历史演进与物联网未来.

    嵌入式操作系统风云录 历史演进与物联网未来 何小庆 著 图书在版编目 CIP 数据 嵌入式操作系统风云录 历史演进与物联网未来 何小庆著 北京 机械工业出版社 2016 10 ISBN 978 7 111 55085 3 嵌 何 实时操作系
  • Java项目结构概述

    文章目录 前言 一 项目结构介绍 1 单模块项目结构 2 多模块项目结构 3 分层结构 4 MVC项目结构 5 插件化结构项目 6 微服务架构结构 总结 前言 构建一个良好的Java项目结构是开发高质量 可扩展和易维护应用程序的重要基础 在
  • skimage图像的读取与保存

    首先 说明用opencv与skimage io imread读取和保存图片的区别 读取和保存后的都是numpy格式 但cv2的读取和存储格式是BGR 而skimage的读取和存储格式是RGB 1 读取图片 skimage读取图片 img s
  • The deduced formulas of Conv1d and ConvTranspose1d

    torch nn Conv1d in channels out channels kernel size stride 1 padding 0 dilation 1 groups 1 bias True In the simplest ca
  • 基于树莓派的python界面开发实例教程

    基于树莓派的python界面开发实例教程 环境测试 添加label实例 时钟程序 添加天气 环境测试 点击树莓派的开始菜单 找到programming Python3 IDLE 点击打开 打开后如下 在home pi下面建立home pi
  • UNIX网络编程卷一 学习笔记 第十七章 ioctl操作

    ioctl函数传统上一直作为那些不适合归入现有已定义的类别的系统接口 POSIX正在通过创建特定的包装函数来代替ioctl函数的某些功能 以取而代之的是那些已被POSIX标准化的函数 例如 Unix终端接口传统上使用ioctl函数访问 而P