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. 套接字操作

3. 文件操作

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$ 等。

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$ 个:

8. 路由表操作

        有些系统提供 $2$ 个用于操纵路由表的 $ioctl$ 请求,这 $2$ 个请求要求 $ioctl$ 的第三个参数是指向某个 $rtentry$ 结构的一个指针,该结构定义在 <$net/route.h$> 中。这些请求通常由 $route$ 发出,并且只有超级用户才能发出。在支持路由域套接字的系统中,这些请求由路由套接字而不是 $ioctl$ 执行。

Unix网络编程(10):ioctl