前三日的基礎之上,一個建立好的TCP/IP通道就已經任我們使用了!若將範例程式稍加修改(拿掉recvfrom
和sendto
)並以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)的回應,但是這裡只看得到三個封包的來往,這是因為結束的太快之故。
有些讀者可能會看過更早的範例程式版本,因為很早就push到github上過。最早的時候其實使用的wrapper是
read-write
,後來曾經改成recv-send
,最後也就是現在的版本是recvfrom-sendto
。由前往後有單向的擴充性:recv-send
比read-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
存到iov
與msg
的結構成員之中。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
結構放回使用者空間的拷貝,完成這次的訊息接收。
基本上就是反向進行的接收,都長的非常相似:
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
,繼續追蹤下去會發現那是msg
的msg_iter
的type
成員的值。
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核心中的實作。若說有什麼延伸閱讀,那就是一般來說,一個伺服器應該會需要select
或poll
之類的機制讓accept
呼叫待在能夠隨時能夠應付請求的狀態,有新連線時再將新建立的socket一起加入select之類機制的監控中。
至此網路的部份就先告結束,而我們也將迎來鐵人賽系列的最後一篇。感謝一直以來持續用閱讀的方式鞭策筆者的讀者,我們明天再會!