Unix网络编程(10):ioctl
$ioctl$ 函数传统上一直作为那些不适合归入其他精细定义类别的特性的系统接口。POSIX
致力于摆脱处于标准化过程的特定功能的 $ioctl$ 接口,办法是为它们创造一些特殊的函数以取代 $ioctl$ 请求,办法是为它们创造一些特殊的函数以取代 $ioctl$ 请求。网络层程序经常在程序启动执行后使用 $ioctl$ 获取所在主机全部网络接口的信息,包括:接口地址、是否支持广播、是否支持多播等。
1. ioctl
函数
#include <unistd.h>
// 成功返回 $0$ ,出错返回-1
int ioctl(int fd, int request, ... /* void *arg */);
第三个参数是一个指针,类型依赖于 $request$ 参数。我们可以把和网络相关的请求分为 $6$ 类:
类别 | $request$ | 说明 | 数据类型 |
---|---|---|---|
套接字 | $SIOCATMARK$ | 是否位于带外标记 | $int$ |
$SIOCSPGRP$ | 设置套接字的进程ID 或进程组ID |
$int$ | |
$SIOCGPGRP$ | 获取套接字的进程ID 或进程组ID |
$int$ | |
文件 | $FIONBIO$ | 设置/清除非阻塞式I/O 标志 |
$int$ |
$FIOASYNC$ | 设置/清除信号驱动异步I/O 标志 |
$int$ | |
$FIONREAD$ | 获取接收缓冲区的字节数 | $int$ | |
$FIOSETOWN$ | 设置文件的进程ID 或进程组ID |
$int$ | |
$FIOGETOWN$ | 获取文件的进程ID 或进程组ID |
$int$ | |
接口 | $SIOCGIFCONF$ | 获取所有接口的列表 | $struct$ $ifconf$ |
$SIOCSIFADDR$ | 设置接口地址 | $struct$ $ifreq$ | |
$SIOCGIFADDR$ | 获取接口地址 | $struct$ $ifreq$ | |
$SIOCGIFADDR$ | 获取接口地址 | $struct$ $ifreq$ | |
$SIOCSIFFLAGS$ | 设置接口标志 | $struct$ $ifreq$ | |
$SIOCGIFFLAGS$ | 获取接口标志 | $struct$ $ifreq$ | |
$SIOCSIFDSTADDR$ | 设置点到点地址 | $struct$ $ifreq$ | |
$SIOCGIFDSTADDR$ | 获取点到点标志 | $struct$ $ifreq$ | |
$SIOCGIFBRDADDR$ | 获取广播地址 | $struct$ $ifreq$ | |
$SIOCSIFBRDADDR$ | 设置广播地址 | $struct$ $ifreq$ | |
$SIOCGIFNETMASK$ | 获取子网掩码 | $struct$ $ifreq$ | |
$SIOCSIFNETMASK$ | 设置子网掩码 | $struct$ $ifreq$ | |
$SIOCGIFMETRIC$ | 获取接口的测度 | $struct$ $ifreq$ | |
$SIOCSIFMETRIC$ | 设置接口的测度 | $struct$ $ifreq$ | |
$SIOCGIFMTU$ | 获取接口 $MTU$ | $struct$ $ifreq$ | |
ARP |
$SIOCSARP$ | 创建/修改ARP 表项 |
$struct$ $arpreq$ |
$SIOCGARP$ | 获取ARP 表项 |
$struct$ $arpreq$ | |
$SIOCDARP$ | 删除ARP 表项 |
$struct$ $arpreq$ | |
路由 | $SIOCADDRT$ | 增加路径 | $struct$ $rtentry$ |
$SIOCDELRT$ | 删除路径 | $struct$ $rtentry$ | |
流 | $I_-xxx$ | 以后说明 |
2. 套接字操作
- $SIOCATMARK$ :如果本套接字的读指针当前位于带外标记,那就通过第三个参数指向的整数值返回一个非 $0$ 值;否则返回一个 $0$ 值;
- $SIOCGPGRP$ :通过第三个参数指向的整数返回本套接字的进程
ID
或进程组ID
,该ID
指定针对本套接字的 $SIGIO$ 或 $SIGURG$ 信号的接收过程。本请求和 $fcntl$ 的 $F_-GETOWN$ 命令等效; - $SIOCSPGRP$ :把本套接字的进程
ID
或进程组ID
设置成由第三个参数指向的整数,该ID
指定针对本套接字的 $SIGIO$ 或 $SIGURG$ 信号的接收过程。本请求和 $fcntl$ 的 $F_-SETOWN$ 命令等效。
3. 文件操作
- $FIONBIO$ :根据 $ioctl$ 的第三个参数指向一个 $0$ 值或非 $0$ 值,可清除或设置本套接字的非阻塞式
I/O
标志。本请求和 $O_-NONBLOCK$ 文件状态标志等效,而可以通过 $fcntl$ 的 $F_-SETFK$ 命令清除或设置该标志; - $FIOASYNC$ :根据 $ioctl$ 的第三个参数指向一个 $0$ 值或非 $0$ 值,可清除或设置针对本套接字的信号驱动异步
I/O
标志,它决定是否收取针对本套接字的异步I/O
信号 ( $SIGIO$ )。本请求和 $O_-ASYNC$ 文件状态标志等效,可以通过 $fcntl$ 清除或设置该标志; - $FIONREAD$ :通过由 $ioctl$ 的第三个参数指向的整数返回当前在本套接字接收缓冲区中的字节数。本特性同样适用于文件、管道和终端;
- $FIOSETOWN$ / $FIOGETOWN$ :对于套接字和 $SIOCGPGRP$ 等效。
4. 接口配置
处理网络接口的许多程序沿用的初始步骤之一就是从内核获取配置在系统中的所有接口。本任务由 $SIOCGIFCONF$ 请求完成,使用 $ifconf$ 结构,$ifconf$ 又使用 $ifreq$ 结构。
struct ifconf {
lint ifc_len; // 缓冲区大小
union {
caddr_t ifcu_buf; // 用户空间到内核空间的输入
struct ifreq *ifcu_req; // 内核空间到用户空间的返回
} ifc_ifcu;
};
#define ifc_buf ifc_ifcu.ifcu_buf // 缓冲区地址
#define ifc_req ifc_ifcu.ifcu_req // 返回的数组结构
#define IFNAMSIZ 16
struct ifreq {
char ifr_name[IFNAMSIZ]; // 接口名
union {
struct sockaddr ifru_addr;
struct sockaddr ifru_dstaddr;
struct sockaddr ifru_broadaddr;
short ifru_flags;
int ifru_metric;
caddr_t ifru_data;
} ifr_ifru;
};
#define ifr_addr if_ifru.ifru_addr // 地址
#define ifr_dstaddr ifr_ifru.ifru_dstaddr // p2p连接的对端
#define ifr_broadaddr ifr_ifru.ifru_broadaddr // 广播地址
#define ifr_flags ifr_ifru.ifru_flags // 标志
#define ifr_metric ifr_ifru.ifru_metric // 测度
#define ifr_data ifr_ifru.ifru_data // 接口使用数据
在调用 $ioctl$ 之前需要先分配一个缓冲区和一个 $ifconf$ 结构,然后初始化后者,$ioctl$ 的第三个参数指向 $ifconf$ 结构。假设内核返回 $2$ 个 $ifreq$ 结构,在 $ioctl$ 返回时通过同一个 $ifconf$ 结构返回,并修改 $ifc_-len$ 和 $ifcu_-req$ 字段,$ifreq$ 结构会被填入缓冲区中。
5. get_ifi_info
函数
很多程序需要直到系统中的所有接口偶,于是可以开发一个名为 $get_-ifi_-info$ 的函数,它返回一个结构链表,其中每个结构对应一个当前处于 $up$ 状态的接口。首先实现 $unpifi.h$ 头文件。
// Out own header for the programs that need interface configuration info.
#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>
#define IFI_HADDR 8 // allow for 64-bit EUI-64 in future
struct ifi_info {
char ifi_name[IFI_NAME]; // interface name, null-terminated
short ifi_index; // interface index
short iff_mtu; // interface MTU
u_char ifi_haddr[IFI_HADDR]; // hadrware address
u_short ifi_hlen; // bytes in hardware address
short ifi_flags; // IFF_xxx constants from <net/if.h>
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
struct ifi_info *get_ifi_info(int, int);
struct ifi_info *Get_ifi_info(int, int);
void free_ifi_info(struct ifi_info *);
#endif // __unp_ifi_h
本函数返回一个本结构的链表,结构中包括了典型的应用程序可能关注的信息:接口名、接口索引、$MTU$ 、硬件地址、接口标志、接口地址、广播地址、点到点链路的目的地址。$free_-ifi_-info$ 负责释放空间。
#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");
if ((i = ifi->ifi_hlen) > 0) {
ptr = ifi->ifi_haddr;
do {
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);
}
第一命令行参数指定IPv4
/IPv6
地址,第二个命令行参数 $0$ 指定不返回地址别名。
#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;
sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
lastlen = 0;
len = 100 * sizeof(struct ifreq); // initial buffer size guess
for (;;) {
buf = Malloc(len);
ifc.ifc_len = len;
ifc.ifc_buf = buf;
if (ioctl(sockfd, SIOCGIFCONF, &ifc) < 0) {
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);
#else
switch(ifr->ifr_addr.sa_family) {
#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
// assume that AF_LINK precedes AF_INET or AF_INET6
if (ifr->ifr_addr.sa_family == AF_LINK) {
struct sockaddr_dl *sal = (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
if (ifr->ifr_addr.sa_family != family)
continue; // ignore if not desired address family
myflags = 0;
if ((cptr = strchr(ifr->ifr_name, ':')) != NULL)
*cptr = 0; // replace colon with null
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);
ifrcopy = *ifr;
Ioctl(sockfd, SIOCGIFFLAGS, &ifrcopy);
flags = ifrcopy.ifr_flags;
if ((flags & IFF_UP) == 0)
continue; // ignore if interface not up
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->ifi_flags = flags; // IFF_xxx values
ifi->ifi_myflags = myflags; // IFI_xxx values
#if defined(SIOCGIFMTU) && defined(HAVE_STRUCT_IFREQ_IFR_MTU)
Ioctl(sockfd, SIOCGIFMTU, &ifrcopy);
ifi->ifi_mtu = ifrcopy.ifr_mtu;
#else
ifi->ifi_mtu = 0;
#endif
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
if (sdlname == NULL || strcmp(sdlname, ifr->ifr_name) != 0)
idx = hlen = 0;
ifi->ifi_index = idx;
ifi->ifi_hlen = hlen;
if (ifi->ifi_hlen > IFI_HADDR)
ifi->ifi_hlen = IFI_HADDR;
if (hlen)
memcpy(ifi->ifi_haddr, haddr, ifi->ifi_hlen);
switch (ifr->ifr_addr.sa_family) {
case AF_INET:
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));
#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;
cast 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;
}
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
}
}
首先创建一个用于 $ioctl$ 的UDP
套接字。$SIOCGIFCONF$ 请求存在的一个严重问题是,在缓冲区的大小不足以存放结果时,一些实现不返回错误,而是截断结果并返回成功 ( 即 $ioctl$ 返回值为 $0$ )。这意味着,要知道缓冲区是否足够大的唯一方法是:发出请求,记下返回的长度,用更大的缓冲区发出请求,比较之前记下的长度和返回的长度,当两个长度相同时,代表缓冲区足够大。
6. 接口操作
$SIOCGIFCONF$ 请求为每个已配置的接口返回其名字以及一个套接字地址结构。我们接着可以发出多个接口类其他请求以设置或获取每个接口的其他特征。这些请求获取版本通常由 $netstat$ 程序发出,设置版本通常由 $ifconfig$ 程序发出。任何用户都可以获取接口信息,设置接口信息却要求具备超级用户权限。这些请求接受或返回一个 $ifreq$ 结构中的信息,而这个结构的地址则作为 $ioctl$ 调用的第三个参数指定。接口总是以其名字标识,在 $ifreq$ 结构的 $ifr_-name$ 成员中指定,如 $leo$ 、$lo0$ 和 $ppp0$ 等。
- $SIOCGIFADDR$ :在 $ifr_-addr$ 成员中返回单播地址;
- $SIOCSIFADDR$ :用 $ifr_-addr$ 成员设置接口地址,这个接口的初始化函数也被调用;
- $SIOCGIFFLAGS$ :在 $ifr_-flags$ 成员中返回接口标志,名字格式为 $IFF_-xxx$ ,在 <$net/if.h$> 头中定义;
- $SIOCSIFFLAGS$ :用 $ifr_-flags$ 成员设置接口标志;
- $SIOCGIFDSTADDR$ :在 $ifr_-dstaddr$ 成员中返回点到点地址;
- $SIOCSIFDSTADDR$ :用 $ifr_-dstaddr$ 成员设置点到点地址;
- $SIOCGIFBRDADDR$ :在 $ifr_-broadaddr$ 成员中返回广播地址。应用程序必须首先获取接口标志,然后发出正确的请求;
- $SIOCSIFBRDADDR$ :用 $ifr_-broadaddr$ 成员设置广播地址;
- $SIOCGIFNETMASK$ :在 $ifr_-addr$ 成员中返回子网掩码;
- $SIOCSIFNETMASK$ :用 $ifr_-addr$ 成员设置子网掩码;
- $SIOCGIFMETRIC$ :用 $ifr_-metric$ 成员返回接口测度。接口测度由内核为每个接口维护,不过使用它的是路由守护进程 $routed$ ,接口测度被 $routed$ 加到跳数上,使得某个接口更不被看好;
- $SIOCSIFMETRIC$ :用 $ifr_-metric$ 成员设置接口的路由测度。
7. ARP
高速缓存操作
使用路由域套接字的系统往往改用路由套接字访问ARP
高速缓存。这些请求使用 $arpreq$ 结构。
#include <net/if_arp.h>
struct arpreq {
struct sockaddr arp_pa; // protocol address
struct sockaddr arp_ha; // hadrware address
int arp_flags; // flags
};
#define ATF_INUSE 0x01 // entry in use
#define ATF_COM 0x02 // completed entry (hardware addr valid)
#define ATF_PERM 0x04 // permanent entry
#define ATF_PUBL 0x08 // published entry (respond for other host)
操纵ARP
高速缓存的 $ioctl$ 请求有 $3$ 个:
- $SIOCSARP$ :把一个新的表项加到
ARP
高速缓存,或者修改其中已经存在的一个表项。其中 $arp_-pa$ 是一个含有IP
地址的网际网套接字地址结构,$arp_-ha$ 则是一个通用套接字地址结构,它的 $sa_-family$ 值为 $AF_-UNSPEC$ ,$sa_-data$ 中含有硬件地址。$ATF_-PERM$ 和 $ATF_-PUBL$ 这两个标志也可以由应用程序指定,另外两个标志由内核设置; - $SIOCDARP$ :从
ARP
高速缓存删除一个表项,调用者指定要删除表项的网际网地址; - $SIOCGARP$ :从
ARP
高速缓存获取一个表项,调用者指定网际网地址,相应的硬件地址 ( 例如以太网地址 ) 随标志一起返回。
8. 路由表操作
有些系统提供 $2$ 个用于操纵路由表的 $ioctl$ 请求,这 $2$ 个请求要求 $ioctl$ 的第三个参数是指向某个 $rtentry$ 结构的一个指针,该结构定义在 <$net/route.h$> 中。这些请求通常由 $route$ 发出,并且只有超级用户才能发出。在支持路由域套接字的系统中,这些请求由路由套接字而不是 $ioctl$ 执行。
- $SIOCADDRT$ :往路由表添加一个表项;
- $SIOCDELRT$ :从路由表删除一个表项。