iT邦幫忙

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

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

trace 30個基本Linux系統呼叫第二十八日:互相交握的accept與connect

前情提要

關於網路,我們已經基本上看完了所有的前置動作(伺服器端3個、客戶端1個),本日的重點就是兩者如何搭上線。


伺服器端接受連線:accept

NAME
       accept, accept4 - accept a connection on a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

       #define _GNU_SOURCE             /* See feature_test_macros(7) */
       #include <sys/socket.h>

       int accept4(int sockfd, struct sockaddr *addr,
                   socklen_t *addrlen, int flags);

這個是伺服器端的接受用的系統呼叫。後面兩個傳指標呼叫是為了取得提出連接請求的客戶的位址資訊,前者則當然就是先前準備好的檔案描述子了。成功的話會回傳一個用來與之溝通的檔案描述子。

老樣子,在net/socket.c中,

1499 SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr,
1500                 int __user *, upeer_addrlen)
1501 {
1502         return sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0);
1503 }

accept呼叫直接在不使用第四個flags參數的情況下引用accept4呼叫。

1418 SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,
1419                 int __user *, upeer_addrlen, int, flags)
1420 {
1421         struct socket *sock, *newsock;
1422         struct file *newfile;
1423         int err, len, newfd, fput_needed;
1424         struct sockaddr_storage address;
1425 
1426         if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
1427                 return -EINVAL;
1428 
1429         if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
1430                 flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
1431 
1432         sock = sockfd_lookup_light(fd, &err, &fput_needed);
1433         if (!sock)
1434                 goto out;

我們昨天就看過的sockfd_lookup_light再度出現,這很正常,因為是從檔案描述子提取socket結構的重要方法。

1436         err = -ENFILE;
1437         newsock = sock_alloc();
1438         if (!newsock)
1439                 goto out_put;
1440 
1441         newsock->type = sock->type;
1442         newsock->ops = sock->ops;
1443 
1444         /*
1445          * We don't need try_module_get here, as the listening socket (sock)
1446          * has the protocol module (sock->ops->owner) held.
1447          */
1448         __module_get(newsock->ops->owner);

創造一個新的socket作為聯絡之用。這裡不需要整組引用socket呼叫的原因是,有些部份已經不用重來,而是可以直接挪用聆聽用得socket的設定,比方說他們必然是相同的協定之類,像是1441、1442兩行的作為。1448處理協定使用核心模組的情況,這也是前日在socket呼叫的時候看過的。

1450         newfd = get_unused_fd_flags(flags);
1451         if (unlikely(newfd < 0)) {
1452                 err = newfd;
1453                 sock_release(newsock);
1454                 goto out_put;
1455         }
1456         newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name);
1457         if (IS_ERR(newfile)) {
1458                 err = PTR_ERR(newfile);
1459                 put_unused_fd(newfd);
1460                 sock_release(newsock);
1461                 goto out_put;
1462         }

這也是socket中看過的內容,也就是取得未利用的檔案描述子,然後配置檔案給他,然後將兩者連通。

1468         err = sock->ops->accept(sock, newsock, sock->file->f_flags);
1469         if (err < 0)
1470                 goto out_fd;
1471 
1472         if (upeer_sockaddr) {
1473                 if (newsock->ops->getname(newsock, (struct sockaddr *)&address,
1474                                           &len, 2) < 0) {
1475                         err = -ECONNABORTED;
1476                         goto out_fd;
1477                 }
1478                 err = move_addr_to_user(&address,
1479                                         len, upeer_sockaddr, upeer_addrlen);
1480                 if (err < 0)
1481                         goto out_fd;
1482         }

accept介面看似一個動作,在這裡可以清楚的看見是->accept的建立連結與->getname的取得位址資訊兩個步驟。這兩者也分別在net/ipv4/af_inet.c之中。先看inet_accept

 671 int inet_accept(struct socket *sock, struct socket *newsock, int flags)
 672 {
 673         struct sock *sk1 = sock->sk;
 674         int err = -EINVAL;
 675         struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err);
 676 
 677         if (!sk2)
 678                 goto do_err;
 679 
 680         lock_sock(sk2);
 681 
 682         sock_rps_record_flow(sk2);
 683         WARN_ON(!((1 << sk2->sk_state) &
 684                   (TCPF_ESTABLISHED | TCPF_SYN_RECV |
 685                   TCPF_CLOSE_WAIT | TCPF_CLOSE)));
 686 
 687         sock_graft(sk2, newsock);
 688 
 689         newsock->state = SS_CONNECTED;
 690         err = 0;
 691         release_sock(sk2);
 692 do_err:
 693         return err;
 694 }

值得一提的是687行的sock_graft指定了newsocksk2之間的親子關係,因為sk2是由sk在網路層的處理得來,而newsock卻是從原本的sock得到創建的資訊。675行的accept指向net/ipv4/inet_connection_sock.cinet_csk_accept,其中會卡在inet_csk_wait_for_connect一行等待著連接。

回到accept4的尾聲,這時呼叫了inet_getname而透過更下層的資料結構取得連接者的資訊。然後將相關資訊複製回使用者空間,之後:

1486         fd_install(newfd, newfile);
1487         err = newfd; 
1488                
1489 out_put:
1490         fput_light(sock->file, fput_needed);
1491 out:           
1492         return err;
1493 out_fd:                
1494         fput(newfile);
1495         put_unused_fd(newfd);
1496         goto out_put;
1497 }

將檔案描述子安裝到新的檔案上,大功告成。


客戶端提出連接請求:connect

然後我們來看看客戶端的狀況。

NAME
       connect - initiate a connection on a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);

雖然和accept很類似,但是用意不同;這裡的後兩個參數是要指定伺服器所在的位置用的,而非為了取得。

程式碼:

1517 SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
1518                 int, addrlen)
1519 {
1520         struct socket *sock;
1521         struct sockaddr_storage address;
1522         int err, fput_needed;
1523 
1524         sock = sockfd_lookup_light(fd, &err, &fput_needed);
1525         if (!sock)
1526                 goto out;
1527         err = move_addr_to_kernel(uservaddr, addrlen, &address);
1528         if (err < 0)
1529                 goto out_put;
1530 
1531         err =
1532             security_socket_connect(sock, (struct sockaddr *)&address, addrlen);
1533         if (err)
1534                 goto out_put;
1535 
1536         err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
1537                                  sock->file->f_flags);
1538 out_put:
1539         fput_light(sock->file, fput_needed);
1540 out:
1541         return err;
1542 }

1524行取得相對應的應用層socket物件,然後1527行將使用者空間的位址資訊複製到核心空間來。然後1536行呼叫inet_stream_connect(這次不能靠亂猜了),

 655 int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
 656                         int addr_len, int flags)
 657 {
 658         int err;
 659 
 660         lock_sock(sock->sk);
 661         err = __inet_stream_connect(sock, uaddr, addr_len, flags);
 662         release_sock(sock->sk);
 663         return err;
 664 }

__inet_stream_connect就在上方不遠處。內容有一個狀態機:

 584         switch (sock->state) {
 585         default:
 586                 err = -EINVAL;
 587                 goto out;
 588         case SS_CONNECTED:
 589                 err = -EISCONN;
 590                 goto out;
 591         case SS_CONNECTING:
 592                 err = -EALREADY;
 593                 /* Fall out of switch with err, set for this state */
 594                 break;
 595         case SS_UNCONNECTED:
 596                 err = -EISCONN;
 597                 if (sk->sk_state != TCP_CLOSE)
 598                         goto out;
 599          
 600                 err = sk->sk_prot->connect(sk, uaddr, addr_len);
 601                 if (err < 0)
 602                         goto out;
 603          
 604                 sock->state = SS_CONNECTING;
 605          
 606                 /* Just entered SS_CONNECTING state; the only
 607                  * difference is that return value in non-blocking
 608                  * case is EINPROGRESS, rather than EALREADY.
 609                  */
 610                 err = -EINPROGRESS;
 611                 break;
 612         }

最一開始進入的當然是595行的未連接狀態,這裡有600行的sk->sk_prot->connect呼叫,在範例程式的case指到net/ipv4/tcp_ipv4.ctcp_v4_connect,裡面就是TCP的細節了。

 614         timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);
 615                                 
 616         if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
 617                 int writebias = (sk->sk_protocol == IPPROTO_TCP) &&
 618                                 tcp_sk(sk)->fastopen_req &&
 619                                 tcp_sk(sk)->fastopen_req->data ? 1 : 0;
 620                                 
 621                 /* Error code is set above */
 622                 if (!timeo || !inet_wait_for_connect(sk, timeo, writebias))                                                             
 623                         goto out;
 624                                 
 625                 err = sock_intr_errno(timeo);
 626                 if (signal_pending(current))
 627                         goto out;
 628         }                       

從狀態機離開之後會設定一個timeo變數當作time out的期限。這裡因為給了O_NONBLOCK所以都會是0,進而導致622行的inet_wait_for_connect不會花時間等待。筆者猜想這是TCP連線的特質,假設伺服器存在,所以不需要等待。

 633         if (sk->sk_state == TCP_CLOSE)
 634                 goto sock_error;
 635                                 
 636         /* sk->sk_err may be not zero now, if RECVERR was ordered by user
 637          * and error was received after socket entered established state.
 638          * Hence, it is handled normally after connect() return successfully.
 639          */                     
 640                                 
 641         sock->state = SS_CONNECTED;
 642         err = 0;                
 643 out:                            
 644         return err;             
 645                                 
 646 sock_error:                     
 647         err = sock_error(sk) ? : -ECONNABORTED;
 648         sock->state = SS_UNCONNECTED;
...

最後的關鍵就是633行的判斷,經過上述手續之後,網路層的socket狀態是否是TCP_CLOSE?如果是的話就只能錯誤收場,反之則可以進到641設定連接狀態。


結論

本日我們大致看過了「伺服器—客戶」模型建立連線的背後機制。TCP/IP本身是個很龐大的架構,對於更深入的部份,不是筆者此時能夠探討的,只能瀏覽到核心如何管理相關的資料結構以及檔案安排,這樣未來要進一步學習的時候有個基礎知識。至此,雖然忽略的底層細節,但是已經可以看到兩者互相連通了。明日就將介紹連結建立之後的訊息傳送,也正式結束網路的部份。感謝各位讀者,我們明日再會!


上一篇
trace 30個基本Linux系統呼叫第二十七日:bind-listen
下一篇
trace 30個基本Linux系統呼叫第二十九日:對話:recvfrom與sendto
系列文
跨界的追尋:trace 30個基本Linux系統呼叫30

尚未有邦友留言

立即登入留言