iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 29
1
自我挑戰組

跨界的追尋:trace 30個基本Linux系統呼叫系列 第 29

trace 30個基本Linux系統呼叫第二十九日:對話:recvfrom與sendto

前情提要

前三日的基礎之上,一個建立好的TCP/IP通道就已經任我們使用了!若將範例程式稍加修改(拿掉recvfromsendto)並以tcpdump工具觀察send-recv之前的封包溝通,會有類似的結果:

[root@linux demo]# tcpdump -n -i lo tcp port 12345
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes
14:36:16.892662 IP 127.0.0.1.58572 > 127.0.0.1.12345: Flags [S], seq 3055098810, win 43690, options [mss 65495,sackOK,TS val 1514693 ecr 0,nop,wscale 7], length 0
14:36:16.892682 IP 127.0.0.1.12345 > 127.0.0.1.58572: Flags [S.], seq 1537869874, ack 3055098811, win 43690, options [mss 65495,sackOK,TS val 1514647 ecr 1514693,nop,wscale 7], length 0
14:36:16.892695 IP 127.0.0.1.58572 > 127.0.0.1.12345: Flags [.], ack 1, win 342, options [nop,nop,TS val 1514693 ecr 1514647], length 0
14:36:16.892762 IP 127.0.0.1.58572 > 127.0.0.1.12345: Flags [F.], seq 1, ack 1, win 342, options [nop,nop,TS val 1514693 ecr 1514647], length 0
14:36:16.892775 IP 127.0.0.1.12345 > 127.0.0.1.58572: Flags [F.], seq 1, ack 2, win 229, options [nop,nop,TS val 1514693 ecr 1514693], length 0
14:36:16.892790 IP 127.0.0.1.58572 > 127.0.0.1.12345: Flags [.], ack 2, win 342, options [nop,nop,TS val 1514693 ecr 1514693], length 0

其中,12345是伺服器端配置的port。觀察TCP旗標,可以看到前三行的SYN->SYN-ACK->ACK的三方交握過程。至於連結斷開的部份,理論上的TCP終止連線會像是兩組一來一往(FIN-ACK)的回應,但是這裡只看得到三個封包的來往,這是因為結束的太快之故。


接收訊息:recvfrom

有些讀者可能會看過更早的範例程式版本,因為很早就push到github上過。最早的時候其實使用的wrapper是read-write,後來曾經改成recv-send,最後也就是現在的版本是recvfrom-sendto。由前往後有單向的擴充性:recv-sendread-write多一個flag的參數;recvfrom-sendto則比recv-read多兩個參數代表要指定的來源或是目的地的位址結構與其長度。從大前天的socket開始我們就都採用recvfrom-sendto一組作為範例。

net/socket.c

1668 SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t, size,
1669                 unsigned int, flags, struct sockaddr __user *, addr,
1670                 int __user *, addr_len)
1671 {
1672         struct socket *sock;
1673         struct iovec iov;
1674         struct msghdr msg;
1675         struct sockaddr_storage address;
1676         int err, err2;
1677         int fput_needed;
1678 
1679         err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter);
1680         if (unlikely(err))
1681                 return err;
1682         sock = sockfd_lookup_light(fd, &err, &fput_needed);
1683         if (!sock)
1684                 goto out;
...

可以看到一些很陌生的資料結構,像是iovec或是msghdr這樣的東西,這些都是核心用來傳遞訊息的機制,我們無法在這系列之內詳細解說,但可以看個大概。1679行的import_single_range函數來自lib/iov_iter.c,是要將使用者空間的資料存放區ubuf的位址與預期接收的資料長度size存到iovmsg的結構成員之中。1682行則先取得對應的sock結構。

1686         msg.msg_control = NULL;
1687         msg.msg_controllen = 0;
1688         /* Save some cycles and don't copy the address if not needed */
1689         msg.msg_name = addr ? (struct sockaddr *)&address : NULL;
1690         /* We assume all kernel code knows the size of sockaddr_storage */
1691         msg.msg_namelen = 0;
1692         msg.msg_iocb = NULL;
1693         if (sock->file->f_flags & O_NONBLOCK)
1694                 flags |= MSG_DONTWAIT;
1695         err = sock_recvmsg(sock, &msg, flags);

1679行只有針對msg.msg_iter做初始化而已,這裡接下來對msg的其他成員初始化。然後這個可以承載通用訊息的結構體,會被當作參數傳入一看就知道是關鍵呼叫的sock_recvmsg。經過幾次轉手之後會來到net/ipv4/af_inet.c之中的inet_recvmsg

 762 int inet_recvmsg(struct socket *sock, struct msghdr *msg, size_t size,
 763                  int flags)
 764 {
 765         struct sock *sk = sock->sk;
 766         int addr_len = 0;
 767         int err;
 768 
 769         sock_rps_record_flow(sk);
 770 
 771         err = sk->sk_prot->recvmsg(sk, msg, size, flags & MSG_DONTWAIT,
 772                                    flags & ~MSG_DONTWAIT, &addr_len);
 773         if (err >= 0)
 774                 msg->msg_namelen = addr_len;
 775         return err;
 776 }

771行從上層socket物件轉手到下層的tcp socket,也就是進到net/ipv4/tcp.c裡面的tcp_recvmsg,略過細節。在前述步驟中,會將payload從封包內解析出來,取得應用層真正需要的資料長度,放在addr_len回傳,這也會被用來更新msg的成員。回到recvmsg系統呼叫:

1695         err = sock_recvmsg(sock, &msg, flags);
1696 
1697         if (err >= 0 && addr != NULL) {
1698                 err2 = move_addr_to_user(&address,
1699                                          msg.msg_namelen, addr, addr_len);
1700                 if (err2 < 0)
1701                         err = err2; 
1702         }
1703 
1704         fput_light(sock->file, fput_needed);
1705 out:
1706         return err;
1707 }

主要是把所得的msg結構放回使用者空間的拷貝,完成這次的訊息接收。


傳送訊息:sendto

基本上就是反向進行的接收,都長的非常相似:

1612 SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
1613                 unsigned int, flags, struct sockaddr __user *, addr,
1614                 int, addr_len)
1615 {
1616         struct socket *sock;
1617         struct sockaddr_storage address;
1618         int err;
1619         struct msghdr msg;
1620         struct iovec iov;
1621         int fput_needed;
1622 
1623         err = import_single_range(WRITE, buff, len, &iov, &msg.msg_iter);
1624         if (unlikely(err))
1625                 return err;
1626         sock = sockfd_lookup_light(fd, &err, &fput_needed);
1627         if (!sock)
1628                 goto out;

1623行READ改成WRITE,繼續追蹤下去會發現那是msgmsg_itertype成員的值。

1630         msg.msg_name = NULL;
1631         msg.msg_control = NULL;
1632         msg.msg_controllen = 0;
1633         msg.msg_namelen = 0;
1634         if (addr) { 
1635                 err = move_addr_to_kernel(addr, addr_len, &address);
1636                 if (err < 0)
1637                         goto out_put;
1638                 msg.msg_name = (struct sockaddr *)&address;
1639                 msg.msg_namelen = addr_len;
1640         }
1641         if (sock->file->f_flags & O_NONBLOCK)
1642                 flags |= MSG_DONTWAIT;
1643         msg.msg_flags = flags;
1644         err = sock_sendmsg(sock, &msg);

如果addr,也就是第五個參數有傳入的話,就表示這裡是指定的目的地位址資訊,必須在1635行以下的判斷區塊內跨空間複製。反之,一如我們的範例程式的case,其實socket就保有相關的資訊,不需要這個額外的資訊來源。設好所有msg結構之後,進行sock_sendmsg,一樣是經過幾個轉手之後進入inet_sendmsg

 729 int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
 730 {
 731         struct sock *sk = sock->sk;
 732 
 733         sock_rps_record_flow(sk);
 734 
 735         /* We may need to bind the socket. */
 736         if (!inet_sk(sk)->inet_num && !sk->sk_prot->no_autobind &&
 737             inet_autobind(sk))
 738                 return -EAGAIN;
 739 
 740         return sk->sk_prot->sendmsg(sk, msg, size);
 741 }
 742 EXPORT_SYMBOL(inet_sendmsg);

sk初始化的時候,在我們使用的範例當中應該是會具備inet_num,因此這個判斷不會生效。接下來是呼叫tcp_sendmsg,然後完成這個傳送。


結論

礙於筆者的學理知識不足,在這個部份的追蹤無法下到TCP層的部份進一步探討。過程中我們常見的INET其實是BSD Socket機制的實作的意思,也是我們整個網路管理篇章所用的這些系統呼叫的設計原型,目的是為了做好使用者空間的網路溝通。所以在下到TCP層(或其他使用情境運用不同協定時)之前,都會透過af_inet.c內的函數來處理。

雖然差強人意,但我們還是掃過了大部分的inet在Linux核心中的實作。若說有什麼延伸閱讀,那就是一般來說,一個伺服器應該會需要selectpoll之類的機制讓accept呼叫待在能夠隨時能夠應付請求的狀態,有新連線時再將新建立的socket一起加入select之類機制的監控中。

至此網路的部份就先告結束,而我們也將迎來鐵人賽系列的最後一篇。感謝一直以來持續用閱讀的方式鞭策筆者的讀者,我們明天再會!


上一篇
trace 30個基本Linux系統呼叫第二十八日:互相交握的accept與connect
下一篇
trace 30個基本Linux系統呼叫第三十日:從今以後繼續努力:reboot
系列文
跨界的追尋:trace 30個基本Linux系統呼叫30

尚未有邦友留言

立即登入留言