在写这个作业之前一直在看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.对于异常的处理,源码中有将近一半的篇幅都在用于异常的处理,这是我们平时代码中非常值得借鉴的地方,在大型项目中,这样的写法可以快速定位错误,同时也不会让程序崩溃。