深入理解TCP协议及其源代码

在写这个作业之前一直在看socket的相关源码

说实话,太难了,很多数据结构不清楚,代码写法也看的模模糊糊,人云亦云

更重要的是,看了几天也没看明白accpet函数和三次握手的syn,ack值这些流程处理有啥关系。。。。

可能是我看的方法不对,也可能我水平不够,但不论怎样,总要写点东西,所以写了写对于socket中accpet4()函数的源码理解。

这篇作业也算是给后来者排雷,让他们可以少废些周折。

有的地方加入了我自己的理(cai)解(xiang),如果有错误,请一定告知我,非常感谢。

 

int __sys_accept4(int fd, struct sockaddr __user *upeer_sockaddr, int __user *upeer_addrlen, int flags)

  这个函数之所以叫accept4,是因为这里有四个参数可以填写,源码里还有个accpet,就只能写前三个参数,第四个参数flags直接给定

        fd:指的是文件标识符,在这里指的是这次链接所对应的文件。我们知道Linux里,一切皆文件,像socket这种网络连接也是通过文件读取来“虚拟”实现

  *upeer_sockaddr 存放结构体信息的首地址
  *upeer_addrlen 存放地址信息的结构体大小
  flags 用于设置标识,例如设置阻塞或非阻塞模式

 

{
struct socket *sock, *newsock;        /* 初始化 */ struct file *newfile;           
int err, len, newfd, fput_needed;    

err一般是错误标识,在Linux中,内部系统调用如果成功则返回0,失败则返回-1,根据err值可以得知程序运行是否正常,也可以通过err值来处理异常,防止程序崩溃
newfd是 新文件标识符

struct sockaddr_storage address;

  下面是struct sockaddr_storage 的定义:

struct sockaddr_storage
 {
     sa_family_t ss_family;        /* 协议族 */
     __ss_aligntype __ss_align;    /* 格式对齐,因为后面有的函数会sockaddr_storage转成SOCKADDR */
char __ss_padding[_SS_PADSIZE]; }; /* _SS_PADSIZE =_SS_SIZE - (2 * sizeof (__ss_aligntype) */

 

if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return -EINVAL;

这里的逻辑有点绕,琢磨半天后明白,如果flags不是SOCK_CLOEXEC 或SOCK_NONBLOCK之一,则返回EINVAL

 

SOCK_CLOEXEC 找不到直接的资料,但源码注释提到了close-on-exec,查资料可知,该标志位是Linux对于文件操作的一个标志,用途为,当fork()或继承而来的同类进程关闭时,也会关闭其他同类进程。在socket中,如果不写这个标识符,那么可能一个进程在占据ip和端口之后,继承它的进程也同样占据ip,而父进程关闭时,子进程没有关闭,从而导致新的父进程无法进行网络通信(因为ip给之前的子进程占了)。

SOCK_NONBLOCK是非阻塞式的标识符。所谓阻塞与非阻塞式的主要区别在于,非阻塞式在read()后如果没有收到数据会立刻返回0,而阻塞式则会等待一段时间再返回0,也就是说,非阻塞式是异步性质的连接,而阻塞式则是同步性质的连接(不做上一步,下一步就无法进行)。

 

EINVAL 是定义在 errno.h 中的一个宏定义,它定义了一个整形变量,是错误代码的一个取值,值为22,,表示无效的参数。

也就是说对于accept4而言,面对的连接是非阻塞式的。

 

if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;

 

O_NONBLOCK指的是非阻塞模式,且读不到数据时返回值为-1

 

sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock) 
goto out;

sockfd_lookup_light函数是根据fd文件标识符找到socket对应实现,这个函数不仅在accpet,在bind,connect等其他关键函数中也很常见。

如果sock没有赋值成功,则直接goto到出口,这里是Linux的一个异常处理。

这里通过fd找到正在监听的socket。整个tcp协议,这个fd都贯穿其中,用于串起不同的函数为同一个连接工作。

 

err = -ENFILE;

ENFILE和之前EINVAL的同样是错误代码的一个取值,值为23,含义为文件表溢出。

 

newsock = sock_alloc();
if (!newsock)
goto out_put;

sock_alloc()函数是用来分配一个与文件绑定的socket变量,在这里是用来给newsock分配变量,用于处理连接。如果失败,goto去出口,又一个异常处理。在Linux源码中的异常处理很多,标志性代码就是goto语句。之后的异常处理不再赘述,读者知道即可。

 

 

newsock->type = sock->type;
newsock->ops = sock->ops;

这里是赋值过程,不谈

 

__module_get(newsock->ops->owner);

module_get()函数,增加文件引用计数,涉及到操作系统内容,不多展开

 

newfd = get_unused_fd_flags(flags);
if (unlikely(newfd < 0)) {
err = newfd;
sock_release(newsock);
goto out_put;
}
newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name);
if (IS_ERR(newfile)) {
err = PTR_ERR(newfile);
put_unused_fd(newfd);
goto out_put;
}

get_unused_fd_flags,用于获取一个未使用过的文件标识符,在open中也有用到。

sock_alloc_file,创建一个文件描述符。

这里主要是对新文件的创建。

在监听到连接请求后,需要分配一个新文件给这个连接。

err = security_socket_accept(sock, newsock);
if (err)
goto out_fd;

security_socket_accept:在security文件夹下可以找到对应函数,全部都通过一个名为hook()的函数进行更进一层的调用。为什么Linux要这么多此一举?继续查资料,hook是Linux Secrity Module的访问判断函数,出于安全性考虑,在进行关键系统调用之前,首先要通过hook函数对该调用进行安全性检测,如果允许调用,则返回0。

这个涉及到Linux的安全机制,简单地说,hook是一个预留的安全接口,用户可以自己实现安全机制,然后通过hook()来访问关键系统调用。

 

err = sock->ops->accept(sock, newsock, sock->file->f_flags, false);
if (err < 0)
goto out_fd;

accpet函数调用inet_stream_ops.inet_accept,而inet_stream_ops.inet_accept最后调用了tcp_prot.inet_csk_accept来最终实现accept功能。

tcp_prot.inet_csk_accept源代码与注释如下:

struct sock *inet_csk_accept(struct sock *sk, int flags, int *err)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct request_sock_queue *queue = &icsk->icsk_accept_queue;
    struct sock *newsk;
    struct request_sock *req;
    int error;

    lock_sock(sk);

    /* We need to make sure that this socket is listening,
     * and that it has something pending.
     */
    error = -EINVAL;
    if (sk->sk_state != TCP_LISTEN)
        goto out_err;

    /* Find already established connection */
    //如果等待accept的socket队列为空,获取超时时间并等待
    if (reqsk_queue_empty(queue)) {
        //如果设置的是非阻塞,获取接收数据的超时时间
        //可以通过SO_RCVTIMEO选项设置
        long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);

        /* If this is a non blocking socket don't sleep */
        error = -EAGAIN;
        if (!timeo)
            goto out_err;
        //等待可accept的请求到来
        error = inet_csk_wait_for_connect(sk, timeo);
        if (error)
            goto out_err;
    }
    //从全连接accept队列里摘出第一个request_sock处理
    req = reqsk_queue_remove(queue);
    //将request_sock和sock关联,这样返回后才能继续处理
    //其实也就是替代req接管这个请求了
    //req->sk就是在第三次握手后服务端新创建的sock
    newsk = req->sk;
    //sk_ack_backlog计数减1
    sk_acceptq_removed(sk);
    //快速开启选项打开的情况,
    if (sk->sk_protocol == IPPROTO_TCP && queue->fastopenq != NULL) {
        spin_lock_bh(&queue->fastopenq->lock);
        if (tcp_rsk(req)->listener) {
            req->sk = NULL;
            req = NULL;
        }
        spin_unlock_bh(&queue->fastopenq->lock);
    }
out:
    release_sock(sk);//listen的sock
    //请求已被newsk接管,因此这个请求可以释放了
    if (req)
        __reqsk_free(req);
    return newsk;
out_err:
    newsk = NULL;
    req = NULL;
    *err = error;
    goto out;
}

 

简单来说accpet()函数先检查socket队列,如果队列没有等待请求,那么看flags是阻塞还是非阻塞。

如果非阻塞,那么直接返回,如果阻塞,那么设置一个超时标记timeo,timeo时间到了以后再返回。

如果socket请求队列里有请求,则从socket等待队列里拿出第一个sock请求处理。

 

继续看accept4()函数源码:

if (upeer_sockaddr) {
len = newsock->ops->getname(newsock,
(struct sockaddr *)&address, 2);
if (len < 0) {
err = -ECONNABORTED;
goto out_fd;
}
err = move_addr_to_user(&address,
len, upeer_sockaddr, upeer_addrlen);
if (err < 0)
goto out_fd;
}

getname()函数用来获得新地址的连接,move_addr_to_user()函数则将地址移出内核。

 

fd_install(newfd, newfile);

将新建立的文件标识符和文件绑定,至此accept主要功能结束

 

out_put:
fput_light(sock->file, fput_needed);

  出口一,释放文件结构

out:
return err;

  出口二,直接返回错误值

out_fd:
fput(newfile);
put_unused_fd(newfd);
goto out_put;
}

  出口三,释放新文件,再转到出口一

 

读linux源码感想:

1.函数的规范命名与注释非常重要,不然读起来会让人很难受;
2.学到了如何用C语言去实现多态,而且个人认为这种方式容易实现,更好排查错误;(这种方法是指,先实现一个最复杂的函数,然后多态的其他函数return这个函数,并附带一定参数)
3.对于异常的处理,源码中有将近一半的篇幅都在用于异常的处理,这是我们平时代码中非常值得借鉴的地方,在大型项目中,这样的写法可以快速定位错误,同时也不会让程序崩溃。

上一篇:深入理解TCP协议及其源代码


下一篇:python multiprocessing模块