51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

查看: 728|回复: 0
打印 上一主题 下一主题

浅谈Linux 内核网络之 connect 的实现

[复制链接]
  • TA的每日心情
    无聊
    5 小时前
  • 签到天数: 938 天

    连续签到: 5 天

    [LV.10]测试总司令

    跳转到指定楼层
    1#
    发表于 2023-3-14 13:18:23 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
    对于面向连接的协议,如 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数据复制到内核空间的address
      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;
      //通过套接口系统调用的跳转表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;
      }


      通过套接口系统调用的跳转表 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;
      }


      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;
      }


      当前进程加入到 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);
      }
      ...
      }


      当链路建立成功后异步发送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);
      }


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

    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏
    回复

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-4-26 14:54 , Processed in 0.066973 second(s), 23 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表