• Socket层实现系列 — connect()的实现


    主要内容:connect()的Socket层实现、期间进程的睡眠和唤醒。

    内核版本:3.15.2

    我的博客:http://blog.csdn.net/zhangskd

    应用层

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

    Connects the socket referred to by the file descriptor sockfd to the address specified by serv_addr.

    服务器端的socket会使用bind()来绑定IP和端口,客户端的socket则一般让系统自动选取IP和端口。

      

    系统调用

    connect()是由glibc提供的,声明位于include/sys/socket.h中,实现位于sysdeps/mach/hurd/connect.c中,

    主要是用来从用户空间进入名为sys_socketcall的系统调用,并传递参数。sys_socketcall()实际上是所有

    socket函数进入内核空间的共同入口。

    SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
    {
        ...
        switch(call) {
        ...
        case SYS_CONNECT:
            err = sys_connect(a0, (struct sockaddr __user *) a1, a[2]);
            break;
        ...
        }
        return err;
    }
    

    经过了socket层的总入口sys_socketcall(),现在进入sys_connect()。

    SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr, int, addrlen)
    {
        struct socket *sock;
        struct sockaddr_storage address;
        int err, fput_needed;
    
        /* 通过文件描述符fd,找到对应的socket实例。
         * 以fd为索引从当前进程的文件描述符表files_struct实例中找到对应的file实例,
         * 然后从file实例的private_data成员中获取socket实例。
        */
        sock = sockfd_lookup_light(fd, &err, &fput_needed);
        if (!sock)
            goto out;
    
        /* 把套接字地址从用户空间拷贝到内核空间 */
        err = move_addr_to_kernel(uservaddr, addrlen, &address);
        if (err < 0)
            goto out_put;
    
        err = security_socket_connect(sock, (struct sockaddr *)&address, addrlen);
        if (err)
            goto out_put;
    
        /* 调用Socket层的操作函数,如果是SOCK_STREAM,则proto_ops为inet_stream_ops,
         * 函数指针指向inet_stream_connect()。
         */
        err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen, sock->file->f_flags);
    
    out_put:
        fput_light(sock->file, fput_needed); /* 减少套接口文件的引用计数 */
    
    out:
        return err;
    }
    

    Socket层

    SOCK_STREAM套接口的socket层操作函数集实例为inet_stream_ops,其中主动建立连接的函数

    为inet_stream_connect()。

    const struct proto_ops inet_stream_ops = {
        .family = PF_INET,
        .owner = THIS_MODULE,
        ....
        .connect = inet_stream_connect,
        ...
    };
    
    int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr, int addr_len, int flags)
    {
        int err;
    
        lock_sock(sock->sk);
        err = __inet_stream_connect(sock, uaddr,addr_len, flags);
        release_sock(sock->sk);
        return err;
    }

    __inet_stream_connect()主要做了以下事情:

    1. 检查socket地址长度和使用的协议族。

    2. 检查socket的状态,必须是SS_UNCONNECTED或SS_CONNECTING。

    3. 调用tcp_v4_connect()来发送SYN包。

    4. 等待后续握手的完成:

        如果socket是非阻塞的,那么就直接返回错误码-EINPROGRESS。

        如果socket为阻塞的,就调用inet_wait_for_connect(),通过睡眠来等待。在以下三种情况下会被唤醒:

        (1) 使用SO_SNDTIMEO选项时,睡眠时间超过设定值,返回0。connect()返回错误码-EINPROGRESS。

        (2)  收到信号,返回剩余的等待时间。connect()返回错误码-ERESTARTSYS或-EINTR。

        (3) 三次握手成功,sock的状态从TCP_SYN_SENT或TCP_SYN_RECV变为TCP_ESTABLISHED,

              sock I/O事件的状态变化处理函数sock_def_wakeup()就会唤醒进程。connect()返回0。

    /* Connect to a remote host. There is regrettably still a little TCP magic in here. */
    int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr, int addr_len, int flags)
    {
        struct sock *sk = sock->sk;
        int err;
        long timeo;
    
        /* socket地址长度错误 */
        if (addr_len < sizeof(uaddr->sa_family))
            return -EINVAL;
    
        /* socket的协议族错误 */
        if (uaddr->sa_family == AF_UNSPEC) {
            /* 如果使用的是TCP,则sk_prot为tcp_prot,disconnect为tcp_disconnect() */
            err = sk->sk_prot->disconnect(sk, flags);
    
            /* 根据是否成功断开连接,来设置socket状态 */
            sock->state = err ? SS_DISCONNECTING : SS_UNCONNECTED;
            goto out;
        }
        
        switch(sock->state) {
        default:
            err = -EINVAL; /* Invalid argument */
            goto out;
    
        case SS_CONNECTED: /* 此套接口已经和对端的套接口相连接了,即连接已经建立 */
            err = -EISCONN; /* Transport endpoint is already connected */
            goto out;
    
        case SS_CONNECTING: /* 此套接口正在尝试连接对端的套接口,即连接正在建立中 */
            err = -EALREADY; /* Operation already in progress */
            /* Fall out of switch with err, set for this state */
            break;
    
        case SS_UNCONNECTED: /* 此套接口尚未连接对端的套接口,即连接尚未建立 */
            err = -EISCONN;
    
            if (sk->sk_state != TCP_CLOSE)
                goto out;
    
            /* 如果使用的是TCP,则sk_prot为tcp_prot,connect为tcp_v4_connect() */
            err = sk->sk_prot->connect(sk, uaddr, addr_len); /* 发送SYN包 */
            if (err < 0)
                goto out;
    
            /* 发出SYN包后socket状态设为正在连接 */
            sock->state = SS_CONNECTING;
    
            /* Just entered SS_CONNECTING state; the only difference
             * is that return value in non-blocking case is EINPROGRESS,
             * rather than EALREADY.
             */
            err = -EINPROGRESS; /* Operation now in progress */
            break;
        }
    
        /* sock的发送超时时间,非阻塞则为0 */
        timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);
    
        /* 发出SYN包后,等待后续握手的完成 */
        if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
    
            int writebias = (sk->sk_protocol == IPPROTO_TCP) &&
                tcp_sk(sk)->fastopen_req && 
                tcp_sk(sk)->fastopen_req->data ? 1 : 0;
    
            /* 如果是非阻塞的,那么就直接返回错误码-EINPROGRESS。
             * socket为阻塞时,使用inet_wait_for_connect()来等待协议栈的处理:
             * 1. 使用SO_SNDTIMEO,睡眠时间超过timeo就返回0,之后返回错误码-EINPROGRESS。
             * 2. 收到信号,就返回剩余的等待时间。之后会返回错误码-ERESTARTSYS或-EINTR。
             * 3. 三次握手成功,被sock I/O事件处理函数唤醒,之后会返回0。
             */
            if (! timeo || ! inet_wait_for_connect(sk,timeo, writebias))
                goto out;
    
           err = sock_intr_errno(timeo);
           /* 进程收到信号,如果err为-ERESTARTSYS,接下来库函数会重新调用connect() */
            if (signal_pending(current))
                goto out;
        }
    
        /* Connection was closed by RST, timeout, ICMP error or
         * another process disconnected us.
         */
        if (sk->sk_state == TCP_CLOSE)
            goto sock_error;
    
        sock->state = SS_CONNECTED; /* 更新socket状态为连接已建立 */
        err = 0; /* 清除错误码 */
    
    out:
        return err;
    
    sock_error:
        err = sock_error(sk) ?: -ECONNABORTED;
        sock->state = SS_UNCONNECTED;
    
         /* 如果使用的是TCP,则sk_prot为tcp_prot,disconnect为tcp_disconnect() */
        if (sk->sk_prot->disconnect(sk, flags)) /* 如果失败 */
            sock->state == SS_DISCONNECTING;
    
        goto out;
    } 
    
    static inline long sock_sndtimeo(const struct sock *sk, bool noblock)
    {
        return noblock ? 0 : sk->sk_sndtimeo;
    }
    
    static inline int sock_intr_errno(long timeo)
    {
        return timeo == MAX_SCHEDULE_TIMEOUT ? -ERESTARTSYS : -EINTR;
    }
    

    进程的睡眠

    connect()的超时时间为sk->sk_sndtimeo,在sock_init_data()中初始化为MAX_SCHEDULE_TIMEOUT,

    表示无限等待,可以通过SO_SNDTIMEO选项来修改。

    static long inet_wait_for_connect(struct sock *sk, long timeo, int writebias)
    {
        DEFINE_WAIT(wait);  /* 初始化等待任务 */
    
        /* 把等待任务加入到socket的等待队列头部,把进程的状态设为TASK_INTERRUPTIBLE */
        prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE);
        sk->sk_write_pending += writebias;
    
        /* Basic assumption: if someone sets sk->sk_err, he _must_ change state of the socket
         * from TCP_SYN_*. Connect() does not allow to get error notifications without closing
         * the socket.
         */
    
        /* 完成三次握手后,状态就会变为TCP_ESTABLISHED,从而退出循环 */
        while ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
            release_sock(sk); /* 等下要睡觉了,先释放锁 */
    
            /* 进入睡眠,直到超时或收到信号,或者被I/O事件处理函数唤醒。
             * 1. 如果是收到信号退出的,timeo为剩余的jiffies。
             * 2. 如果使用了SO_SNDTIMEO选项,超时退出后,timeo为0。
             * 3. 如果没有使用SO_SNDTIMEO选项,timeo为无穷大,即MAX_SCHEDULE_TIMEOUT,
             *      那么返回值也是这个,而超时时间不定。为了无限阻塞,需要上面的while循环。
             */
            timeo = schedule_timeout(timeo); 
            
            lock_sock(sk); /* 被唤醒后重新上锁 */
    
            /* 如果进程有待处理的信号,或者睡眠超时了,退出循环,之后会返回错误码 */
            if (signal_pending(current) || !timeo)
                break;
        
            /* 继续睡眠吧 */
            prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE);
        }
    
        /* 等待结束时,把等待进程从等待队列中删除,把当前进程的状态设为TASK_RUNNING */
        finish_wait(sk_sleep(sk), &wait);
        sk->sk_write_pending -= writebias;
        return timeo;
    }

    进程的唤醒

     

    三次握手中,当客户端收到SYNACK、发出ACK后,连接就成功建立了。

    此时连接的状态从TCP_SYN_SENT或TCP_SYN_RECV变为TCP_ESTABLISHED,sock的状态发生变化,

    会调用sock_def_wakeup()来处理连接状态变化事件,唤醒进程,connect()就能成功返回了。

    sock_def_wakeup()的函数调用路径如下:

    tcp_v4_rcv

    tcp_v4_do_rcv

    tcp_rcv_state_process

    tcp_rcv_synsent_state_process

    tcp_finish_connect

    sock_def_wakeup

    wake_up_interruptible_all

    __wake_up

    __wake_up_common

    void tcp_finish_connect(struct sock *sk, struct sk_buff *skb)
    {
        ...
        tcp_set_state(sk, TCP_ESTABLISHED); /* 在这里设置为连接已建立的状态 */
        ...
        if (! sock_flag(sk, SOCK_DEAD)) {
            sk->sk_state_change(sk); /* 指向sock_def_wakeup,会唤醒调用connect()的进程,完成连接的建立 */
            sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT); /* 如果使用了异步通知,则发送SIGIO通知进程可写 */
        }
    }
    
    static void sock_def_wakeup(struct sock *sk)
    {
        struct socket_wq *wq; /* socket的等待队列和异步通知队列 */
    
        rcu_read_lock();
        wq = rcu_dereference(sk->sk_wq);
    
        if (wq_has_sleeper(wq)) /* 有进程阻塞在此socket上 */
            wake_up_interruptible_all(&wq->wait); /* 唤醒此socket上的所有睡眠进程 */
    
        rcu_read_unlock();
    }
    

    最终调用__wake_up_common(),由于nr_exclusive为0,会把此socket上所有的等待进程都唤醒。

  • 相关阅读:
    Spring个人冲刺阶段总结
    开学测试代码
    《人月神话》读后感(三)
    《人月神话》读后感(二)
    《人月神话》读后感(一)
    记账本小程序7天开发记录(第七天)
    记账本小程序7天开发记录(第六天)
    记账本小程序7天开发记录(第五天)
    记账本小程序7天开发记录(第四天)
    记账本小程序7天开发记录(第三天)
  • 原文地址:https://www.cnblogs.com/aiwz/p/6333246.html
Copyright © 2020-2023  润新知