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上所有的等待进程都唤醒。

时间: 2024-10-10 05:49:37

Socket层实现系列 — connect()的实现的相关文章

Socket层实现系列 — 睡眠驱动的同步等待

主要内容:Socket的同步等待机制,connect和accept等待的实现. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 概述 socket上定义了几个IO事件:状态改变事件.有数据可读事件.有发送缓存可写事件.有IO错误事件. 对于这些事件,socket中分别定义了相应的事件处理函数,也称回调函数. Socket I/O事件的处理过程中,要使用到sock上的两个队列:等待队列和异步通知队列,这两个队列中 都保存着等待该Socket I/O事件

Socket层实现系列 — send()类发送函数的实现

主要内容:socket发送函数的系统调用.Socket层实现. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 发送流程图 以下是send().sendto().sendmsg()和sendmmsg()的发送流程图,这四个函数除了在系统调用层面 上有些差别,在Socket层和TCP层的实现都是相同的. 应用层 应用层可以使用以下Socket函数来发送数据: ssize_t write(int fd, const void *buf, size_t c

Socket层实现系列 — 信号驱动的异步等待

主要内容:Socket的异步通知机制. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 概述 socket上定义了几个IO事件:状态改变事件.有数据可读事件.有发送缓存可写事件.有IO错误事件. 对于这些事件,socket中分别定义了相应的事件处理函数,也称回调函数. Socket I/O事件的处理过程中,要使用到sock上的两个队列:等待队列和异步通知队列,这两个队列中 都保存着等待该Socket I/O事件的进程. Q:为什么要使用两个队列,等待

Socket层实现系列 — I/O事件及其处理函数

主要内容:Socket I/O事件的定义.I/O处理函数的实现. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd I/O事件定义 sock中定义了几个I/O事件,当协议栈遇到这些事件时,会调用它们的处理函数. struct sock { ... struct socket_wq __rcu *sk_wq; /* socket的等待队列和异步通知队列 */ ... /* callback to indicate change in the state

Socket层上的协议

Socket层上的协议指的数据传输的格式 HTTP协议 传输格式:假设:这是假设,实际http的格式不是这样的. http1.1,content-type:multipart/form-data,content-length:188,body:username=zhangsan&password=123456 XMPP协议,是一款即时通讯协议 可扩展消息处理现场协议是基于可扩展标记语言(XML)的协议,它用于即时消息(IM)以及在线现场探测.这个协议可能最终允许因特网用户向因特网上的其他任何人发

网络编程Socket之TCP之connect详解

对TCP套接字调用connect会激发三次握手,如下: 客户端是主动打开连接的一端,会发送第一个SYN分节,然后等待确认,此时连接状态为SYN_SENT,当收到服务端的确认后连接建立,状态变为ESTABLISHED: 服务器是被动打开连接的一端,调用listen导致套接字从CLOSED状态变为LISTEN状态,当收到来自客户端的SYN分节以后状态变为SYN_RCVD,然后发送第二个SYN分节,等待客户端的确认,收到客户端的确认以后连接建立,状态变为ESTABLISHED: 三次握手中的两个SYN

socket使用非阻塞connect

在使用tcp的connect调用时,默认是使用阻塞方式,当服务器当前不可用时,connect会等待(内部在重试?)直到超时时间到达,而这个超时时间是系统内核规定的,不能使用setSocketOpt来设置. 在碰到服务器不可用,上层逻辑进行重试时,如果超时时间过长,会产生卡死的感觉,用户体验也不佳,所以需要控制connect的超时时间. 参考网络上的资料,这里使用select.实现方式是:将socket设置为非阻塞方式,使用select来轮询socket,在select里指定超时时间,根据sock

socket TCP编程中connect的一些坑

1.服务端listen成功后,系统就自动接收客户端请求了 man listen: 其中有一段 The  behavior of the backlog argument on TCP sockets changed with Linux 2.2.  Now it specifies the  queue  length  for  completely  established sockets  waiting  to  be  accepted, instead of the number o

socket 由浅入深系列------ 原理(一)

来自:网络整理 个人觉得写一个网络应用程序没有是一件非常easy的事.其实,我们刚開始的时候总觉得的原则: 建立------>连接套接字------->接受一个连接---->发送数据 而真正复杂编写一个网络应用程序的规模从一个连接到成千上万的连接! 那么本系列将对sockt由浅入深的介绍. msdn The overlapped I/O mechanism in Win32? allows an application to initiate an operation and recei