Linux 内核网络之 connect 的实现

对于面向连接的协议,如 TCP, connect() 建立一条与指定的外部地址的连接。若在connect调用之前没有绑定地址和端口,则会自动绑定一个地址和端口号套接口。
首页 新闻资讯 行业资讯 Linux 内核网络之 connect 的实现

f5e6ecf328ef4a390e6076b9111351385da9d1.jpg

对于面向连接的协议,如 TCP, connect() 建立一条与指定的外部地址的连接。若在connect调用之前没有绑定地址和端口,则会自动绑定一个地址和端口号套接口。

复制

asmlinkage long sys_connect(int fd, struct sockaddr __user *uservaddr,int addrlen)
{struct socket *sock;char address[MAX_SOCK_ADDR];int err, fput_needed;//根据文件描述符获取套接口指针,并且返回是否需要减少对文件引用计数标志。sock = sockfd_lookup_light(fd, &err, &fput_needed);if (!sock)goto out;//将用户空间的uservaddr数据复制到内核空间的addresserr = 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;//通过套接口系统调用的跳转表proto_ops,调用connect操作。TCP 中为 inet_stream_connect(), UDP 为 inet_dgram_connect()err = sock->ops->connect(sock, (struct sockaddr *)address, addrlen,sock->file->f_flags);out_put:// 根据fput_needed标志,调用fput_light减少对文件引用计数操作fput_light(sock->file, fput_needed);out:return err;
}
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

  • 22.

  • 23.

  • 24.

  • 25.

  • 26.

  • 27.

  • 28.

  • 29.

通过套接口系统调用的跳转表 proto_ops ,调用 inet_stream_connect。

复制

int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,int addr_len, int flags)
{struct sock *sk = sock->sk;int err;long timeo;lock_sock(sk);/* socket的协议族错误 */if (uaddr->sa_family == AF_UNSPEC) {/* 如果使用的是TCP,则sk_prot为tcp_prot,disconnect为tcp_disconnect() */err = sk->sk_prot->disconnect(sk, flags);sock->state = err ? SS_DISCONNECTING : SS_UNCONNECTED;goto out;
}switch (sock->state) {default:err = -EINVAL;goto out;case SS_CONNECTED:err = -EISCONN;goto out;case SS_CONNECTING:err = -EALREADY;/* 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;break;
}/* sock的发送超时时间,非阻塞则为0 */timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);/* 发出SYN包后,等待后续握手的完成 */if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {/* Error code is set above *//* 如果是非阻塞的,那么就直接返回错误码-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))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;/* sk->sk_err may be not zero now, if RECVERR was ordered by user* and error was received after socket entered established state.* Hence, it is handled normally after connect() return successfully.*//* 更新socket状态为连接已建立 */sock->state = SS_CONNECTED;/* 清除错误码 */err = 0;out:release_sock(sk);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;
}
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

  • 22.

  • 23.

  • 24.

  • 25.

  • 26.

  • 27.

  • 28.

  • 29.

  • 30.

  • 31.

  • 32.

  • 33.

  • 34.

  • 35.

  • 36.

  • 37.

  • 38.

  • 39.

  • 40.

  • 41.

  • 42.

  • 43.

  • 44.

  • 45.

  • 46.

  • 47.

  • 48.

  • 49.

  • 50.

  • 51.

  • 52.

  • 53.

  • 54.

  • 55.

  • 56.

  • 57.

  • 58.

  • 59.

  • 60.

  • 61.

  • 62.

  • 63.

  • 64.

  • 65.

  • 66.

  • 67.

  • 68.

  • 69.

  • 70.

  • 71.

  • 72.

  • 73.

  • 74.

  • 75.

  • 76.

  • 77.

  • 78.

  • 79.

  • 80.

  • 81.

  • 82.

  • 83.

  • 84.

  • 85.

  • 86.

  • 87.

  • 88.

  • 89.

  • 90.

  • 91.

  • 92.

  • 93.

  • 94.

  • 95.

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

对协议族进行检查。

此时套接口状态为 SS_UNCONNECTED, 调用 tcp_v4_connect() 来发送SYN包。

等待后续握手的完成:

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

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

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

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

  • 三次握手成功,sock的状态从 TCP_SYN_SENT 或 TCP_SYN_RECV 变为TCP_ESTABLISHED,sock I/O事件的状态变化处理函数sock_def_wakeup() 就会唤醒进程。connect() 返回0。

客户端调用tcp_v4_connect 发送SYN包时,设置客户端状态为 TCP_SYN_SENT。

进程休眠

复制

static long inet_wait_for_connect(struct sock *sk, long timeo)
{/* 初始化等待任务 */DEFINE_WAIT(wait);/* 把等待任务加入到socket的等待队列头部,把进程的状态设为TASK_INTERRUPTIBLE */prepare_to_wait(sk->sk_sleep, &wait, TASK_INTERRUPTIBLE);/* 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->sk_sleep, &wait, TASK_INTERRUPTIBLE);
}/* 等待结束时,把等待进程从等待队列中删除,把当前进程的状态设为TASK_RUNNING */finish_wait(sk->sk_sleep, &wait);return timeo;
}
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

  • 15.

  • 16.

  • 17.

  • 18.

  • 19.

  • 20.

  • 21.

  • 22.

  • 23.

  • 24.

  • 25.

  • 26.

  • 27.

  • 28.

  • 29.

  • 30.

  • 31.

  • 32.

  • 33.

  • 34.

当前进程加入到 socket 的等待队列 sk_sleep 中,然后进入休眠,直到超时或接收到信号。

进程被唤醒

在三次握手中,当客户端收到 SYN+ACK、发出ACK后,连接就成功建立了。此时连接的状态从TCP_SYN_SENT或TCP_SYN_RECV变成了 TCP_ESTABLISHED,表示连接建立成功。最终会调用 sock_def_wakeup() 来处理连接状态变化事件,唤醒进程,connect()成功返回。

调用过程如下

tcp_v4_rcv

-> tcp_v4_do_rcv

-> tcp_rcv_state_process

-> tcp_rcv_synsent_state_process

-> sk_wake_async(sk, 0, POLL_OUT);

复制

static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,struct tcphdr *th, unsigned len)
{...if (!sock_flag(sk, SOCK_DEAD)) {/* 指向sock_def_wakeup,会唤醒调用connect()的进程,完成连接的建立 */sk->sk_state_change(sk);/* 若采用异步通知,则发送SIGIO通知进程可写 */sk_wake_async(sk, 0, POLL_OUT);
}...}
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

  • 10.

  • 11.

  • 12.

  • 13.

  • 14.

当链路建立成功后异步发送SIGIO信号,唤醒阻塞的进程并通知 socket 可写,这也就是为什么非阻塞调用 connect 时检查 socket 是否可写事件的原因。

复制

static void sock_def_wakeup(struct sock *sk)
{read_lock(&sk->sk_callback_lock);/* 有进程阻塞在此socket上 */if (sk->sk_sleep && waitqueue_active(sk->sk_sleep))/* 唤醒此socket上的所有睡眠进程 */wake_up_interruptible_all(sk->sk_sleep);read_unlock(&sk->sk_callback_lock);
}
  • 1.

  • 2.

  • 3.

  • 4.

  • 5.

  • 6.

  • 7.

  • 8.

  • 9.

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

12    2023-03-10 14:56:37    Linux connect 系统