FreeBSD kernel SOCKET I/F 探検 (24)

bind() システムコール (1)

必要とする知識など:
  • UNIX (含む Linux) 上での C 言語と SOCKET によるプログラミング経験および TCP/IP に関する知識。

クライアント側bind()システムコール、一回目です。

前回 まではサーバ側の処理を見てきましたが、今回からクライアント側の処理を見ていきます。

本連載の 二回目 に掲載したclient.cを再度掲載します。

client.c

#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>

#define NLOOPS 10
#define MAX_VAL 10
#define DATA_LENGTH 16

/**
 * Main
 */
int
main(int argc, char *argv[])
{
       char *addr;
       int port;
       int sd;
       int result;
       struct addrinfo hints;
       struct addrinfo *res;
       struct sockaddr_in from;
       struct sockaddr_in to;
       socklen_t addrlen;
       ssize_t rbytes;
       ssize_t sbytes;
       int i;

       /* Is not enough argument? */
       if (argc != 3) {
               fprintf(stderr, "Usage: client host PortNo\n");
               exit(1);
       }
       addr = argv[1];
       port = atoi(argv[2]);

       /* Get address information */
       bzero(&hints, sizeof (hints));
       hints.ai_family = AF_INET;
       hints.ai_socktype = SOCK_DGRAM;
       result = getaddrinfo(addr, 0, &hints, &res);
       if (result != 0) {
               fprintf(stderr, "getaddrinfo: %s: %s\n", addr, gai_strerror(result));
               if (result == EAI_SYSTEM) {
                       fprintf(stderr, "getaddrinfo: %s: %s\n", addr, strerror(errno));
               }
               exit(1);
       }
       bzero(&to, sizeof (to));
       bcopy(res->ai_addr, &to, res->ai_addrlen);
       to.sin_port = htons(port);
       addrlen = res->ai_addrlen;
       freeaddrinfo(res);

       /* Create a socket */
       sd = socket(PF_INET, SOCK_DGRAM, 0);
       if (sd < 0) {
               perror("socket");
               exit(1);
       }

       /* Bind a port number */
       bzero(&from, sizeof (from));
#ifdef __FreeBSD__
       from.sin_len = sizeof (from);
#endif /* __FreeBSD__ */
       from.sin_family = AF_INET;
       from.sin_port = 0;
       result = bind(sd, (struct sockaddr *)&from, sizeof (from));
       if (result < 0) {
               perror("bind");
               exit(1);
       }

       /* Bind the destination address and port number */
       result = connect(sd, (struct sockaddr *)&to, addrlen);
       if (result < 0) {
               perror("connect");
               exit(1);
       }

       for (i = 0; i < NLOOPS; i++) {
               int j;
               char sbuf[DATA_LENGTH];
               char rbuf[DATA_LENGTH+1];

               /* Inisialize send buffer */
               for (j = 0; j < DATA_LENGTH; j++) {
                       sbuf[j] = '0' + ((i + j) % MAX_VAL);
               }

               /* Send data */
               sbytes = send(sd, sbuf,  sizeof (sbuf), 0);
               if (sbytes < 0) {
                       perror("send");
                       break;
               }
               if (sbytes != sizeof (sbuf)) {
                       fprintf(stderr, "Length of the transmitted data does not matched: Tried to send %dbytes, transmitted %dbytes.\n", sizeof (sbuf), sbytes);
               }

               /* Receive data */
               bzero(&rbuf, sizeof (rbuf));
               rbytes = recv(sd, rbuf, sbytes, 0);
               if (rbytes < 0) {
                       perror("recv");
                       break;
               }
               if (sbytes != rbytes) {
                       fprintf(stderr, "Length of the received data does not matched: Transmitted %dbytes, received %dbytes\n", sbytes, rbytes);
               }

               /* Print data */
               printf("Data: Transmitted: \"%s\", Received: \"%s\"\n", sbuf, rbuf);
       }

       /* Success */
       return 0;
}

最初に呼び出しているライブラリ関数getaddrinfo()は、第一引数で指定したホスト名をIPアドレスに変換し、第四引数のresに格納して返却します。ホスト名ではなく “192.168.0.1”のようのIPアドレスを指定した場合、これを数値に変換して返却します。

getaddrinfo()で取得したアドレス(res->ai_addr)をstruct sockaddr_in toにコピーした後freeaddrinfo()を使用してresで返却された領域を解放しています。freeaddrinfo()を忘れるとメモリーリークの原因となりますので、扱いには気を付けてください。

getaddrinfo()/freeaddrinf()に関しては詳細は割愛します。必要であればオンラインマニュアルなどを参照してください。

次に実行しているのはsocket()システムコールです。この呼び出しに関しては、本連載の 第三回 と同じなので、詳細は割愛します。

socket()システムコール呼び出し時のデータ構造を以下に示します。なお以下のデータ構造は、本連載の 第十二回 の末尾に掲載したものと同一です。

socket()システムコールの呼び出しで確保した領域を緑、変更した変数を青、変更した値は赤で示しています。

次の処理はbind()システムコールです。bind()は、本連載の 第十三回 で説明しています。server.cでは起動時の引数で指定されたポート番号を指定していますが、client.cではポート番号に0を指定しています。ポート番号に0を指定した場合には、カーネル内で未使用のポート番号を割り当ててくれます。

bind()のカーネル内の処理はserver.cからの呼び出しと途中までは同じです。しかし、第十七回 で解説したポート番号の扱いのあたりから異なります。第十七回 ではポート番号に8000番が指定されたものとして説明していますが、client.cでは以下のように明示的に0番が指定されています。

from.sin_family = AF_INET;
from.sin_port = 0;
result = bind(sd, (struct sockaddr *)&from, sizeof (from));

このため、server.cとは異なる部分から処理を追いかけていきます。具体的には、sys_bind() -> kern_bind() -> sobind() -> udp_bind() -> in_pcbbind() -> in_pcbbind_setup()と処理が進んで、in_pcbbind_setup()の以下の「} else {」

if (nam != NULL && laddr.s_addr != INADDR_ANY)
        return (EINVAL);
if ((so->so_options & (SO_REUSEADDR|SO_REUSEPORT)) == 0)
        lookupflags = INPLOOKUP_WILDCARD;
if (nam == NULL) {
        if ((error = prison_local_ip4(cred, &laddr)) != 0)
                return (error);
} else {

の中の、以下の処理からとなります。

laddr = sin->sin_addr;
if (lport) {
        struct inpcb *t;
        struct tcptw *tw;

上記の二行目のif文

if (lport) {

で参照している変数lportは、in_pcbbind_setup()内で以下のよう定義されています。

int
in_pcbbind_setup(struct inpcb *inp, struct sockaddr *nam, in_addr_t *laddrp,
    u_short *lportp, struct ucred *cred)
{
        struct socket *so = inp->inp_socket;
        struct sockaddr_in *sin;
        struct inpcbinfo *pcbinfo = inp->inp_pcbinfo;
        struct in_addr laddr;
        u_short lport = 0;

ここまでの処理で変数lportは変更されていません。したがって、

if (lport) {

このif文は偽となるので次の処理に進みます。

if (*lportp != 0)
        lport = *lportp;
if (lport == 0) {
        error = in_pcb_lport(inp, &laddr, &lport, cred, lookupflags);
        if (error != 0)
                return (error);

}

lportpには、struct inpcbのinp_lportのポインタが渡されます。struct inpcbは 十一回目 で解説しましたが、bzero()にて初期化したのち、まだinp_lportには何も代入していないので、

if (*lportp != 0)

このif文は偽となります。

次のif文ですが、

if (lport == 0) {

前述の通り、lportには0が代入されているので、このif文は真となり次の行の in_pcb_lport()が呼ばれます。

in_pcb_lport()は、/usr/src/sys/netinet/in_pcb.c (http://svnweb.freebsd.org/base/release/9.1.0/sys/netinet/in_pcb.c?view=markup) で以下のように定義されています。

int
in_pcb_lport(struct inpcb *inp, struct in_addr *laddrp, u_short *lportp,
    struct ucred *cred, int lookupflags)
{
        struct inpcbinfo *pcbinfo;
        struct inpcb *tmpinp;
        unsigned short *lastport;
        int count, dorandom, error;
        u_short aux, first, last, lport;
#ifdef INET
        struct in_addr laddr;
#endif

        pcbinfo = inp->inp_pcbinfo;

        /*
         * Because no actual state changes occur here, a global write lock on
         * the pcbinfo isn't required.
         */
        INP_LOCK_ASSERT(inp);
        INP_HASH_LOCK_ASSERT(pcbinfo);

        if (inp->inp_flags & INP_HIGHPORT) {
                first = V_ipport_hifirstauto;   /* sysctl */
                last  = V_ipport_hilastauto;
                lastport = &pcbinfo->ipi_lasthi;
        } else if (inp->inp_flags & INP_LOWPORT) {
                error = priv_check_cred(cred, PRIV_NETINET_RESERVEDPORT, 0);
                if (error)
                        return (error);
                first = V_ipport_lowfirstauto;  /* 1023 */
                last  = V_ipport_lowlastauto;   /* 600 */
                lastport = &pcbinfo->ipi_lastlow;
        } else {
                first = V_ipport_firstauto;     /* sysctl */
                last  = V_ipport_lastauto;
                lastport = &pcbinfo->ipi_lastport;
        }

ローカル変数の定義の直後の以下の処理の説明は割愛し、その次のif文から見ていきます。

pcbinfo = inp->inp_pcbinfo;
INP_LOCK_ASSERT(inp);
INP_HASH_LOCK_ASSERT(pcbinfo);

以下のif文で参照しているinp->inp_flagsは、十一回目 で解説したbzero()にて初期化したのちにまだ何も代入していないので、いずれも偽となりelse内の処理が実行されます。

if (inp->inp_flags & INP_HIGHPORT) {
        ...
} else if (inp->inp_flags & INP_LOWPORT) {
        ...
} else {
        first = V_ipport_firstauto;     /* sysctl */
        last  = V_ipport_lastauto;
        lastport = &pcbinfo->ipi_lastport;
}

V_ipport_firstautoとV_ipport_lastautoは、/usr/src/sys/netinet/in_pcb.h (http://svnweb.freebsd.org/base/release/9.1.0/sys/netinet/in_pcb.h?view=markup) で以下のように定義されています。

#define V_ipport_firstauto      VNET(ipport_firstauto)
#define V_ipport_lastauto       VNET(ipport_lastauto)

VNETに関しての説明は割愛します。

以下の処理のコメントにある通り、

first = V_ipport_firstauto;     /* sysctl */
last  = V_ipport_lastauto;

V_ipport_firstautoとV_ipport_lastautoはsysctlコマンドで操作可能なカーネル内の変数のようで、以下が該当するものと思われます。

$ sysctl net.inet.ip.portrange.first net.inet.ip.portrange.last
net.inet.ip.portrange.first: 10000
net.inet.ip.portrange.last: 65535

ということで、上記の処理は以下と同等と考えます。

first = 10000;
last  = 65535;

この後も行頭に「V _」が付与された変数が出てきますが、いずれもsysctlコマンドで操作可能なカーネル内の変数として扱います。

次の処理を掲載します。

/*
 * For UDP, use random port allocation as long as the user
 * allows it.  For TCP (and as of yet unknown) connections,
 * use random port allocation only if the user allows it AND
 * ipport_tick() allows it.
 */
if (V_ipport_randomized &&
        (!V_ipport_stoprandom || pcbinfo == &V_udbinfo))
        dorandom = 1;
else
        dorandom = 0;

V_ipport_randomizedはsysctlコマンドで確認すると、以下が該当すると思われます。

$ sysctl net.inet.ip.portrange.randomized
net.inet.ip.portrange.randomized: 1

V_ipport_stoprandomはsysctlコマンドで確認しても該当するものが見当たらないので、0とみなします。

pcbinfo (inp->inp_pcbinfo)は、struct inpcbinfo udbinfoを指しています。V_udbinfoはudbinfoなので、上記のif文は真となり

dorandom = 1;

が実行されます。

続くif文で参照している変数ははいずれも先ほど出てきたばかりです。いずれも偽となります。

/*
 * It makes no sense to do random port allocation if
 * we have the only port available.
 */
if (first == last)
        dorandom = 0;
/* Make sure to not include UDP packets in the count. */
if (pcbinfo != &V_udbinfo)
        V_ipport_tcpallocs++;
/*
 * Instead of having two loops further down counting up or down
 * make sure that first is always <= last and go with only one
 * code path implementing all logic.
 */
if (first > last) {
        aux = first;
        first = last;
        last = aux;
}

次に進みます。

#ifdef INET
        /* Make the compiler happy. */
        laddr.s_addr = 0;
        if ((inp->inp_vflag & (INP_IPV4|INP_IPV6)) == INP_IPV4) {
                KASSERT(laddrp != NULL, ("%s: laddrp NULL for v4 inp %p",
                    __func__, inp));
                laddr = *laddrp;
        }
#endif

INETはコンパイル時に指定されているので、この#ifdef INETで括られた処理は有効です。

最初に初期化しているladdrは、本関数の最初のところで定義されていますが、ここで初めて初期化されます。

次のif文で参照しているinp->inp_vflagはINP_IPV4が代入されているので、このif文は真となります。if文の中の処理に関しての説明は割愛します。

続く処理も、arc4random()を除き詳細は割愛します。

tmpinp = NULL;  /* Make compiler happy. */
lport = *lportp;

if (dorandom)
        *lastport = first + (arc4random() % (last - first));

count = last - first;

arc4random()は、/usr/src/sys/libkern/arc4random.c (https://svnweb.freebsd.org/base/release/9.1.0/sys/libkern/arc4random.c?view=markup) で以下のように定義されています。

/*
 * MPSAFE
 */
void
arc4rand(void *ptr, u_int len, int reseed)
{
        u_char *p;
        struct timeval tv;

        getmicrouptime(&tv);
        if (reseed ||
           (arc4_numruns > ARC4_RESEED_BYTES) ||
           (tv.tv_sec > arc4_t_reseed))
                arc4_randomstir();

        mtx_lock(&arc4_mtx);
        arc4_numruns += len;
        p = ptr;
        while (len--)
                *p++ = arc4_randbyte();
        mtx_unlock(&arc4_mtx);
}

uint32_t
arc4random(void)
{
        uint32_t ret;

        arc4rand(&ret, sizeof ret, 0);
        return ret;
}

詳細は割愛しますが、arc4random()はuint32_t型のランダムな数値を返却する関数だと推測し、ここでは1433が返却されたものと見なします。したがって以下の処理は、

if (dorandom)
        *lastport = first + (arc4random() % (last - first));

以下と同等となり、

*lastport = 10000 + (1433 % (65535 - 10000));

結局以下の処理となります。

*lastoport = 11433;

次の処理

count = last - first;

は、

count = 65535 - 10000;

なので、

count = 55535;

となります。

次の処理を掲載します。

       do {
               if (count-- < 0)        /* completely used? */
                       return (EADDRNOTAVAIL);
               ++*lastport;
               if (*lastport < first || *lastport > last)
                       *lastport = first;
               lport = htons(*lastport);

#ifdef INET6
               if ((inp->inp_vflag & INP_IPV6) != 0)
                       tmpinp = in6_pcblookup_local(pcbinfo,
                           &inp->in6p_laddr, lport, lookupflags, cred);
#endif
#if defined(INET) && defined(INET6)
               else
#endif
#ifdef INET
                       tmpinp = in_pcblookup_local(pcbinfo, laddr,
                           lport, lookupflags, cred);
#endif
       } while (tmpinp != NULL);

INET6とINETはいずれもコンパイル時に指定されているので、#ifdef INET6, #ifdef INETで括られた処理は、いずれも有効です。

doの直後から順番に見ていきます。

if (count-- < 0)        /* completely used? */
        return (EADDRNOTAVAIL);
++*lastport;
if (*lastport < first || *lastport > last)
        *lastport = first;
lport = htons(*lastport);

各処理の詳細は割愛しますが、ここまでで各変数は以下のように変化します。

実行前:
 count = 55535;
 *lastport = 11433;
 lport = 0;

実行後:
 count = 55534;
 *lastport = 11434; /* == 0x2caa */
 lport = 0xaa2c;

htons()は、本連載の 第十七回 でも出てきました。詳細はman htonsなどで確認してください。

続く以下のif文ですが、inp->inp_vflagにはINP_IPV4が代入されているので、

if ((inp->inp_vflag & INP_IPV6) != 0)

このif文は偽となり、elseの処理が実行されます。

else
        tmpinp = in_pcblookup_local(pcbinfo, laddr,
            lport, lookupflags, cred);

ここで呼び出しているin_pcblookup_local()は既に本連載の 第十七回 でも出てきましたが、第二引数と第三引数で渡しているladdrとlportに該当するstruct inpcbが存在すれば、そのポインタを返却します。存在しなければNULLを返却します。ここではNULLが返却されたものとみなし、次の処理に進みます。

} while (tmpinp != NULL);

先ほどのin_pcblookup_local()がNULLを返却しているので、do~whileループから抜けます。

#ifdef INET
        if ((inp->inp_vflag & (INP_IPV4|INP_IPV6)) == INP_IPV4)
                laddrp->s_addr = laddr.s_addr;
#endif
        *lportp = lport;

        return (0);
}

最初のif文は真となります。見たままなので詳細は割愛しますが、最後に0を返却して in_pcbbind_setup()に戻ります。結局、in_pcb_lport()は、未使用のポート番号を割り当てて、そのポート番号を*lportpに返却します。

in_pcb_lport()からin_pbbbind_setup()に戻ったところの処理ですが、in_pcb_lport()の呼び出し部分も含めて掲載します。

                error = in_pcb_lport(inp, &laddr, &lport, cred, lookupflags);
                if (error != 0)
                        return (error);

        }
        *laddrp = laddr.s_addr;
        *lportp = lport;
        return (0);
}

* laddrpには、in_pcb_lport()で割り当てた0が代入されます。* lportpには、in_pcb_lport()で割り当てたポート番号11434をhtons()で変換した値 0xaa2cが代入されます。

in_pcbbind_setup()からin_pcbbind()に戻ったところの処理ですが、in_pcbbind_setup()の呼び出し部分も含めて掲載します。

        error = in_pcbbind_setup(inp, nam, &inp->inp_laddr.s_addr,
            &inp->inp_lport, cred);
        if (error)
                return (error);
        if (in_pcbinshash(inp) != 0) {
                inp->inp_laddr.s_addr = INADDR_ANY;
                inp->inp_lport = 0;
                return (EAGAIN);
        }
        if (anonport)
                inp->inp_flags |= INP_ANONPORT;
        return (0);
}

in_pcbbind_setup()からin_pcbbind()に戻ったところの処理は、本連載の 十八回目 で説明していますので、詳細は割愛します。

この後、in_pcbbind() -> udp_bind() -> sobind() -> kern_bind() -> sys_bind() に戻り、client.cのbind()の呼び出しまで戻ります。

bind()システムコールで操作したデータ構造を以下に示します。

bind()システムコールの処理で確保した領域を緑、変更した変数を青、変更した値は赤で示しています。

記事執筆者: asou
記事公開日:2017年01月25日