Linux TCP/IP协议栈之Socket的实现分析(socket bind)

简介: 当创建了一个Socket 套接字后,对于服务器来说,接下来的工作,就是调用 bind(2)为服务器指明本地址、协议端口号,常常可以看到这样的代码:strut sockaddr_in sin;  sin.
当创建了一个Socket 套接字后,对于服务器来说,接下来的工作,就是调用 bind(2)为服务器指明本地址、
协议端口号,常常可以看到这样的代码:
strut sockaddr_in sin;
 
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = xxx;
sin.sin_port = xxx;
bind(sock, (struct sockaddr *)&sin, sizeof(sin));

从这个系统调用中可以知道当进行 SYS_BIND 操作的时候:

1. 对于 AF_INET 协议簇来讲,其地址格式是 strut sockaddr_in,而对于 socket 来讲,strut sockaddr结构
表示的地址格式实现了更高层次的抽像,因为每种协议长簇的地址不一定是相同的,所以系统调用的第三个
参数
得指明该协议簇
的地址格式的长度

2. 进行 bind(2)系统调用时,除了地址长度外,还得向内核提供:sock 描述符、协议簇名称、本地
地址、端口这
些参数

sys_bind的实现

操作 SYS_BIND  是由 sys_bind()实现的


1335 asmlinkage long sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
1336 {
1337     struct socket *sock;
1338     char address[MAX_SOCK_ADDR];
1339     int err, fput_needed;
1340
1341     if((sock = sockfd_lookup_light(fd, &err, &fput_needed))!=NULL)
1342     {
1343         if((err=move_addr_to_kernel(umyaddr,addrlen,address))>=0) {
1344             err = security_socket_bind(sock, (struct sockaddr *)address, addrlen);
1345             if (!err)
1346                 err = sock->ops->bind(sock,
1347                     (struct sockaddr *)address, addrlen);
1348         }
1349         fput_light(sock->file, fput_needed);
1350     }          
1351     return err;
1352 }
在socket 的创建中已经反复分析了 socket 与文件系统的关系,现在已知socket的描述符号,要找出与之相关的socket结构
应该是件容易的事情

 490 static struct socket *sockfd_lookup_light(int fd, int *err, int *fput_needed)                                          
 491 {
 492     struct file *file;
 493     struct socket *sock;
 494
 495     *err = -EBADF;
 496     file = fget_light(fd, fput_needed);
 497     if (file) {
 498         sock = sock_from_file(file, err);
 499         if (sock)
 500             return sock;
 501         fput_light(file, *fput_needed);
 502     }
 503     return NULL;
 504 }

fget 从当前进程的 files 指针中,根据 sock 对应的描述符号,找到已打开的文件 file,再根据文件的
目录项中的 inode,利用inode 与 sock 被封装在同一个结构中的事实,调用宏 SOCKET_I找到待查
的 sock 结构。最后做一个小小的判断,因为正常情况下,sock 的 file 指针是 回指与之相关的 file
.

接下来的工作是把用户态的地址拷贝至内核中来
 228 int move_addr_to_kernel(void __user *uaddr, int ulen, void *kaddr)                                                     
 229 {
 230     if(ulenMAX_SOCK_ADDR)
 231         return -EINVAL;
 232     if(ulen==0)
 233         return 0;
 234     if(copy_from_user(kaddr,uaddr,ulen))
 235         return -EFAULT;
 236     return audit_sockaddr(ulen, kaddr);
 237 }

bind(2)第三个参数必须存在的原因之一,copy_from_user 必须知道拷贝的字节长度

因为 sock 的 ops 函数指针集,在创建之初,就指向了对应的协议类型,例如如果类型是
SOCK_STREAM,那么它就指向 inetsw_array[0].ops。也就是 inet_stream_ops:
struct proto_ops inet_stream_ops = {
        .family = PF_INET,
        .bind = inet_bind,
        ……
};

sys_bind()在做完了一个通用的 socket bind 应该做的事情,包括查找对应 sock 结构,拷贝地址。
就调用对应协议族的对应协议类型的 bind 函数,也就是 inet_bind
.

inet_bind

说 bind(2)的最重要的作用就是为套接字绑定地址和端口,那么要分析inet_bind()之前,要搞清楚
的一件事情就是,这个绑定是绑定到哪儿?或者说是绑定到内核的哪个数据结构的哪个成员变量上面?

有三个地方是可以考虑的:socket 结构,包括 sock 和 sk,inet结构,以及 protoname_sock 结构。
绑定在 socket 结构上是可行的,这样可以实现最高层面上的抽像,但是因为每一类协议簇 socket 的地址及端口表现
形式差异很大,这样就得引入专门的转换处理功能。绑定在 protoname_sock 也是可行的,但是却是最笨拙的,因为
例如 tcp 和 udp,它们的地址及端口表现形式是一样的,这样就浪费了空间加大了代码处理量。
所以inet 做为一个协议类型的抽像是最理想的地方了,再来回顾一下它的定义


108 struct inet_sock {        
114     /* Socket demultiplex comparisons on incoming packets. */                                                           
115     __u32           daddr;
116     __u32           rcv_saddr;
117     __u16           dport;
118     __u16           num;  
119     __u32           saddr;
去掉了其它成员保留了与地址及端口相关的成员变量,从注释中可以清楚地了解它们的作用。所以我们说的 bind(2)之
绑定主要就是对这几个成员变量赋值的过程了
.

 397 int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
 398 {
            /*  获取地址参数 */
 399     struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;

            /*  获取 sock 对应的 sk */
 400     struct sock *sk = sock->sk;

            /*  获取 sk 对应的inet */
 401     struct inet_sock *inet = inet_sk(sk);

            /*  这个临时变量用来保存用户态传递下来的端口参数 */
 402     unsigned short snum;
 403     int chk_addr_ret;
 404     int err;
 405
            /*  如果协议簇对应的协议自身还有bind函数调用之,例如 SOCK_RAW 就还有一个raw_bind */
 406     /* If the socket has its own bind function then use it. (RAW) */
 407     if (sk->sk_prot->bind) {
 408         err = sk->sk_prot->bind(sk, uaddr, addr_len);
 409         goto out;
 410     }
 411     err = -EINVAL;

            /*  校验地址长度 */
 412     if (addr_len  413         goto out;

 414    
/*  判断地址类型:广播?多播?单播? */
 415     chk_addr_ret = inet_addr_type(addr->sin_addr.s_addr);
 416
            /*  ipv4 有一个 ip_nonlocal_bind标志,表示是否绑定非本地址 IP地址,可以通过 
             *  cat /proc/sys/net/ipv4/ip_nonlocal_bind查看到。
             *  它用来解决某些服务绑定动态 IP地址的情况。作者在注释中已有详细说明.
             *  这里判断,用来确认如果没有开启“绑定非本地址 IP”,地址值及类型是正确的

 417     /* Not specified by any standard per-se, however it breaks too
 418      * many applications when removed.  It is unfortunate since
 419      * allowing applications to make a non-local bind solves
 420      * several problems with systems using dynamic addressing.
 421      * (ie. your servers still start up even if your ISDN link
 422      *  is temporarily down)
 423      */
 424     err = -EADDRNOTAVAIL;
 425     if (!sysctl_ip_nonlocal_bind &&
 426         !inet->freebind &&
 427         addr->sin_addr.s_addr != INADDR_ANY &&
 428         chk_addr_ret != RTN_LOCAL &&
 429         chk_addr_ret != RTN_MULTICAST &&
 430         chk_addr_ret != RTN_BROADCAST)
 431         goto out;

            /* 获取协议端口号 */
 433     snum = ntohs(addr->sin_port);
 434     err = -EACCES;

            /*  校验当前进程有没有使用低于 1024 端口的能力  */

 435     if (snum && snum  436         goto out;
 437
 438     /*      We keep a pair of addresses. rcv_saddr is the one
 439      *      used by hash lookups, and saddr is used for transmit.
 440      *
 441      *      In the BSD API these are the same except where it
 442      *      would be illegal to use them (multicast/broadcast) in
 443      *      which case the sending device address is used.
 444      */
 445     lock_sock(sk);
 446
 447     /* Check these errors (active socket, double bind). */
 448     err = -EINVAL;

            /*  检查socket是否已经被绑定过了: 用了两个检查项, 一个是 sk 状态, 另一个是是否已经绑定过端口了
                  当然地址本来就可以为0,所以不能做为检查项  */

 449     if (sk->sk_state != TCP_CLOSE || inet->num)
 450         goto out_release_sock;

 451     /*  绑定inet的接收地址(地址服务绑定地址)和来源地址为用户态指定地址 */

 452     inet->rcv_saddr = inet->saddr = addr->sin_addr.s_addr;

             /*  若地址类型为广播或多播,则将地址置 0,表示直接使用网络设备 */
 453     if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)
 454         inet->saddr = 0;  /* Use device */
 455
            /*  调用协议的 get_port 函数,确认是否可绑定端口.
             *  若可以, 则绑定在 inet->num 之上, 注意这里虽然没有把inet传过去,但是第一个参数sk
             *  它本身和 inet是可以互相转化的 */

 456     /* Make sure we are allowed to bind here. */
 457     if (sk->sk_prot->get_port(sk, snum)) {
 458         inet->saddr = inet->rcv_saddr = 0;
 459         err = -EADDRINUSE;
 460         goto out_release_sock;
 461     }

 462     
/*  如果端口和地址可以绑定,置标志位 */
 463     if (inet->rcv_saddr)
 464         sk->sk_userlocks |= SOCK_BINDADDR_LOCK;
 465     if (snum)
 466         sk->sk_userlocks |= SOCK_BINDPORT_LOCK;

            /* inet的 sport(来源端口)成员也置为绑定端口 */

 467     inet->sport = htons(inet->num);
 468     inet->daddr = 0;
 469     inet->dport = 0;  
 470     sk_dst_reset(sk);
 471     err = 0;
 472 out_release_sock:
 473     release_sock(sk);
 474 out:
 475     return err;
 476 }

上述分析中忽略的第一个细节是capable()函数调用,它是 Linux 安全模块(LSM)的一部份简单地讲其用来对权限做出检查
检查是否有权对指定的资源进行操作。这里它的参数是CAP_NET_BIND_SERVICE表示的含义是: 
/* Allows binding to TCP/UDP sockets below 1024 */
/* Allows binding to ATM VCIs below 32 */

#define CAP_NET_BIND_SERVICE 10[/code]
 
另一个就是协议的端口绑定,调用了协议的get_port函数,如果是SOCK_STREAM的TCP协议,那么它
就是tcp_v4_get_port()函数
.

协议端口的绑定
 
要分析这个函数还是得先绕一些基本的东东,这里涉及到内核中提供hash链表的操作的API。
可以参考其它相关资料。
http://www.ibm.com/developerworks/cn/linux/kernel/l-chain/index.html
这里讲了链表的实现,顺道提了一个 hash 链表,觉得写得还不错,收藏一下。
 
对于 TCP已注册的端口,是采用一个 hash 表来维护的。hash 桶用 struct tcp_bind_hashbucket 结构来表示:
struct tcp_bind_hashbucket {
        spinlock_t                lock;
        struct hlist_head        chain;
};
 
hash 表中的每一个 hash节点,用 struct tcp_bind_hashbucket 结构来表示:
struct tcp_bind_bucket {
        unsigned short           port;                        /*  节点中绑定的端口 */
        signed short               fastreuse;
        struct hlist_node        node;
        struct hlist_head        owners;
};
 
tcp_hashinfo 的 hash 表信息,都集中封装在结构 tcp_hashinfo 当中,而维护已注册端口只是它其
中一部份:
 
extern struct tcp_hashinfo {
        ……
        /* Ok, let's try this, I give up, we do need a local binding
         * TCP hash as well as the others for fast bind/connect.          */
        struct tcp_bind_hashbucket *__tcp_bhash;
 
        int __tcp_bhash_size;
        ……
} tcp_hashinfo;
 
#define tcp_bhash        (tcp_hashinfo.__tcp_bhash)
#define tcp_bhash_size        (tcp_hashinfo.__tcp_bhash_size)
 
其使用的 hash 函数是 tcp_bhashfn:
/* These are AF independent. */
static __inline__ int tcp_bhashfn(__u16 lport)
{
        return (lport & (tcp_bhash_size - 1));
}
 
这样,如果要取得某个端口对应的 hash 链的首部hash 桶节点的话,可以使用:
struct tcp_bind_hashbucket *head;
head = &tcp_bhash[tcp_bhashfn(snum)];
 
如果要新绑定一个端口就是先创建一个 struct tcp_bind_hashbucket 结构的 hash 节点,然后把它插入到对应的
hash 链中去:
struct tcp_bind_bucket *tb;

tb = tcp_bucket_create(head, snum);
 
struct tcp_bind_bucket *tcp_bucket_create(struct tcp_bind_hashbucket *head,
                                          unsigned short snum)
{
        struct tcp_bind_bucket *tb = kmem_cache_alloc(tcp_bucket_cachep,
                                                      SLAB_ATOMIC);
        if (tb) {
                tb->port = snum;
                tb->fastreuse = 0;
                INIT_HLIST_HEAD(&tb->owners);
                hlist_add_head(&tb->node, &head->chain);
        }
        return tb;
}
另外sk 中还维护了一个类似的 hash 链表,同时需要调用 tcp_bind_hash()函数把 hash 节点插入进去:
struct sock {
        struct sock_common        __sk_common;
#define sk_bind_node              __sk_common.skc_bind_node
        ……
}
 
/* @skc_bind_node: bind hash linkage for various protocol lookup tables */
struct sock_common {
        struct hlist_node        skc_bind_node;
        ……
}
        
if (!tcp_sk(sk)->bind_hash)
        tcp_bind_hash(sk, tb, snum);
        
void tcp_bind_hash(struct sock *sk, struct tcp_bind_bucket *tb,
                   unsigned short snum)
{
        inet_sk(sk)->num = snum;
        sk_add_bind_node(sk, &tb->owners);
        tcp_sk(sk)->bind_hash = tb;
}
 
这里就顺道绑定了 inet 的 num成员变量,并置协议的 bind_hash 指针为当前分配的 hash 节点。
而sk_add_bind_node 函数,就是一个插入 hash 表节点的过程:
 
static __inline__ void sk_add_bind_node(struct sock *sk,
                                        struct hlist_head *list)
{
        hlist_add_head(&sk->sk_bind_node, list);
}
 
如果要遍历 hash 表的话,例如在插入之前,先判断端口是否已经在 hash表当中了。就可以调用:
 
#define tb_for_each(tb, node, head) hlist_for_each_entry(tb, node, head, node)
 
struct tcp_bind_hashbucket *head;
struct tcp_bind_bucket *tb;
 
head = &tcp_bhash[tcp_bhashfn(snum)];
spin_lock(&head->lock);
tb_for_each(tb, node, &head->chain)
        if (tb->port == snum)                 found,do_something;
              
有了这些基础知识,再来看 tcp_v4_get_port()的实现,就要容易得多了:
 
static int tcp_v4_get_port(struct sock *sk, unsigned short snum)
{
        struct tcp_bind_hashbucket *head;
        struct hlist_node *node;
        struct tcp_bind_bucket *tb;
        int ret;
 
        local_bh_disable();
        
        /*  如果端口值为 0,意味着让系统从本地可用端口用选择一个,并置 snum为分配的值 */
        if (!snum) {
                int low = sysctl_local_port_range[0];
                int high = sysctl_local_port_range[1];
                int remaining = (high - low) + 1;
                int rover;
 
                spin_lock(&tcp_portalloc_lock);
                if (tcp_port_rover                         rover = low;
                else
                        rover = tcp_port_rover;
                do {
                        rover++;
                        if (rover > high)
                                rover = low;
                         head = &tcp_bhash[tcp_bhashfn(rover)];
                        spin_lock(&head->lock);
                        tb_for_each(tb, node, &head->chain)
                                if (tb->port == rover)
                                        goto next;
                        break;
                next:
                        spin_unlock(&head->lock);
                } while (--remaining > 0);
                tcp_port_rover = rover;
                spin_unlock(&tcp_portalloc_lock);
 
                /* Exhausted local port range during search? */
                ret = 1;
                if (remaining                         goto fail; 
                /* OK, here is the one we will use.  HEAD is
                 * non-NULL and we hold it's mutex.
                 */
                snum = rover;
        } else {
                /*  否则,就在 hash 表中,查找端口是否已经存在 */
                head = &tcp_bhash[tcp_bhashfn(snum)];
                spin_lock(&head->lock);
                tb_for_each(tb, node, &head->chain)
                        if (tb->port == snum)
                                goto tb_found;
        }
        tb = NULL;
        goto tb_not_found;
tb_found:
        /*  稍后有对应的代码:
         *  第一次分配 tb 后,会调用 tcp_bind_hash加入至相应的 sk,这里先做一个判断,
         *  来确定这一步工作是否进行过 */
        if (!hlist_empty(&tb->owners)) {

        /* socket的SO_REUSEADDR选项,用来确定是否允许本地地址重用,例如同时启动多个服务器、多个套接字
         * 绑定至同一端口等等,sk_reuse 成员对应其值,因为如果一个绑定的 hash节点已经存在,而且不允许重用的话,
         * 那么则表示因冲突导致出错,调用 tcp_bind_conflict 来处理之 */
                if (sk->sk_reuse > 1)
                        goto success;
                if (tb->fastreuse > 0 &&
                    sk->sk_reuse && sk->sk_state != TCP_LISTEN) {
                        goto success;
                } else {
                        ret = 1;
                        if (tcp_bind_conflict(sk, tb))
                                goto fail_unlock;
                }
        }
tb_not_found:
        /*  如果不存在,则分配 hash节点,绑定端口 */
        ret = 1;
        if (!tb && (tb = tcp_bucket_create(head, snum)) == NULL)
                goto fail_unlock;
        if (hlist_empty(&tb->owners)) {
                if (sk->sk_reuse && sk->sk_state != TCP_LISTEN)
                        tb->fastreuse = 1;
                else
                        tb->fastreuse = 0;
        } else if (tb->fastreuse && (!sk->sk_reuse || sk->sk_state == TCP_LISTEN))
                tb->fastreuse = 0;
success:
        if (!tcp_sk(sk)->bind_hash)
                tcp_bind_hash(sk, tb, snum);
        BUG_TRAP(tcp_sk(sk)->bind_hash == tb);
        ret = 0;
 
fail_unlock:
        spin_unlock(&head->lock);
fail:
        local_bh_enable();
        return ret;
}
 
到这里,可以为这部份下一个小结了,所谓绑定,就是:
1. 设置内核中 inet 相关变量成员的值,以待后用;
2. 协议中,如TCP协议,记录绑定的协议端口的信息,采用 hash 链表存储,sk 中也同时维护了这么一个链表。
    两者的区别应该是前者给协议用, 后者给socket 用。



相关文章
|
1月前
|
网络协议 Linux C语言
Linux实现socket网络通信
Linux实现socket网络通信
|
1月前
|
Linux Android开发
嵌入式linux中Framebuffer 驱动程序框架分析
嵌入式linux中Framebuffer 驱动程序框架分析
26 0
|
1月前
|
Linux C语言 SoC
嵌入式linux总线设备驱动模型分析
嵌入式linux总线设备驱动模型分析
32 1
|
1月前
|
Linux
嵌入式linux系统设备树实例分析
嵌入式linux系统设备树实例分析
34 0
|
26天前
|
监控 Shell Linux
【Shell 命令集合 网络通讯 】Linux 分析串口的状态 statserial命令 使用指南
【Shell 命令集合 网络通讯 】Linux 分析串口的状态 statserial命令 使用指南
32 0
|
26天前
|
存储 Shell Linux
【Shell 命令集合 磁盘管理 】⭐⭐ Linux 显示当前shell会话中的目录栈 dirs命令使用教程
【Shell 命令集合 磁盘管理 】⭐⭐ Linux 显示当前shell会话中的目录栈 dirs命令使用教程
27 0
|
6天前
|
存储 算法 Linux
【实战项目】网络编程:在Linux环境下基于opencv和socket的人脸识别系统--C++实现
【实战项目】网络编程:在Linux环境下基于opencv和socket的人脸识别系统--C++实现
20 6
|
27天前
|
Shell Linux C语言
【Shell 命令集合 磁盘管理 】Linux 显示当前shell会话中的目录栈 dirs命令使用教程
【Shell 命令集合 磁盘管理 】Linux 显示当前shell会话中的目录栈 dirs命令使用教程
34 1
|
2天前
|
网络协议 Ubuntu Unix
Linux 下使用 socket 实现 TCP 客户端
Linux 下使用 socket 实现 TCP 客户端
|
15天前
|
Prometheus 监控 数据可视化
linux分析方法与技巧
【4月更文挑战第3天】在Linux环境中,进行日志分析和系统性能分析的关键方法包括:使用`cat`, `less`, `tail`查看和过滤日志,`logrotate`管理日志文件,`rsyslog`或`syslog-ng`聚合日志,以及通过`top`, `mpstat`, `pidstat`, `free`, `iostat`, `netstat`, `strace`, `sar`, `dstat`等工具监控CPU、内存、磁盘I/O和网络。对于高级分析,可利用Brendan Gregg的性能工具,以及Grafana、Prometheus等可视化工具。
16 2
linux分析方法与技巧