前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >阻塞和非阻塞的实现

阻塞和非阻塞的实现

作者头像
theanarkh
发布2021-07-08 16:03:27
2.2K0
发布2021-07-08 16:03:27
举报
文章被收录于专栏:原创分享原创分享

我们可能都已经听过阻塞非阻塞的概念,本文以tcp中的connect系统调用为例子(基于1.12.13内核,新版的原理类似,但是过程就很复杂了,有时间再分析),分析阻塞和非阻塞是什么并且看他是如何实现的。话不多说,直接开始。

代码语言:javascript
复制
static int inet_connect(struct socket *sock, struct sockaddr * uaddr,
      int addr_len, int flags)
{
  struct sock *sk=(struct sock *)sock->data;
  // 调用底层的连接函数,发一个syn包
  err = sk->prot->connect(sk, (struct sockaddr_in *)uaddr, addr_len);
  if (err < 0) 
    return(err);

  // 还没建立连接成功并且是非阻塞的方式,直接返回
  if (sk->state != TCP_ESTABLISHED &&(flags & O_NONBLOCK)) 
      return(-EINPROGRESS);
  // 早期通过关中断防止竞态情况
  cli(); 
  // 连接建立中,阻塞当前进程
  while(sk->state == TCP_SYN_SENT || sk->state == TCP_SYN_RECV) 
  {
    // 阻塞进程
    interruptible_sleep_on(sk->sleep);
    // 连接失败
    if(sk->err && sk->protocol == IPPROTO_TCP)
    {
      sti();
      sock->state = SS_UNCONNECTED;
      err = -sk->err;
      sk->err=0;
      return err; /* set by tcp_err() */
    }
  }
  sti();
  // 连接建立
  sock->state = SS_CONNECTED;
  // 返回成功
  return(0);
}

我们看到connect函数首先会调用tcp层的函数发送一个sync包,然后根据socket的属性(阻塞非阻塞,可以通过setsocketopt设置)做下一步处理,如果是非阻塞,那么就比较简单,直接返回给应用层。这也是非阻塞+事件驱动架构中的做法。因为这种架构下通常是单进程的,要避免阻塞进程,那么返回后什么时候才能知道连接成功呢?这就是epoll提供的机制,当连接成功后,tcp层会通知epoll,epoll就会通知应用层。下面我们继续分析阻塞的过程,interruptible_sleep_on(sk->sleep)。我们看到socket中有一个sleep字段,该字段用于管理队列。我们看看interruptible_sleep_on

代码语言:javascript
复制
void interruptible_sleep_on(struct wait_queue **p)
{
  __sleep_on(p,TASK_INTERRUPTIBLE);
}

static inline void __sleep_on(struct wait_queue **p, int state)
{
  unsigned long flags;
  struct wait_queue wait = { current, NULL };
  current->state = state;
  add_wait_queue(p, &wait);
  save_flags(flags);
  sti();
  schedule();
  remove_wait_queue(p, &wait);
  restore_flags(flags);
}

这里我们只关注两个地方add_wait_queue和schedule。add_wait_queue就是把一个节点插入队列。我们看看wait_queue的定义。

代码语言:javascript
复制
struct wait_queue {
    struct task_struct * task;
    struct wait_queue * next;
};

所以add_wait_queue执行完之后架构如下。

接着调用schedule调度其他进程执行,我们发现这时候当前进程的状态是TASK_INTERRUPTIBLE,所以是不会被调度执行的。这就是进程阻塞的原理,主要是两个过程

1 加入等待队列

2 让出CPU,调度其他进程执行。

我们这个进程什么时候被唤醒呢?我们从收到sync的回包开始分析。具体逻辑在tcp_rcv中。

代码语言:javascript
复制
  if(sk->state==TCP_SYN_SENT)
    {
      /* Crossed SYN or previous junk segment */
      // 发送了syn包,收到ack包说明可能是建立连接的ack包
      if(th->ack)
     {
        // 发送第三次握手的ack包,进入连接建立状态
        tcp_send_ack(sk->sent_seq,sk->acked_seq,sk,th,sk->daddr);
        tcp_set_state(sk, TCP_ESTABLISHED);
        // 唤醒阻塞在connect函数的进程
        if(!sk->dead)
        {
          // 唤醒进程
          sk->state_change(sk);
          // 给进程发送SIGIO信号
          sock_wake_async(sk->socket, 0);
        }
      }
    }

我们看到收到ack后,tcp层调用state_change回调,state_change的值是def_callback1。

代码语言:javascript
复制
static void def_callback1(struct sock *sk)
{
  if(!sk->dead)
    wake_up_interruptible(sk->sleep);
}

我们看到这里会调用wake_up_interruptible唤醒进程。我们看看实现。

代码语言:javascript
复制
void wake_up_interruptible(struct wait_queue **q)
{
  struct wait_queue *tmp;
  struct task_struct * p;

  if (!q || !(tmp = *q))
    return;
  do {
    if ((p = tmp->task) != NULL) {
      if (p->state == TASK_INTERRUPTIBLE) {
        p->state = TASK_RUNNING;
        if (p->counter > current->counter + 3)
          need_resched = 1;
      }
    }
    tmp = tmp->next;
  } while (tmp != *q);
}

我们看到wake_up_interruptible会唤醒所有进程,这就是导致惊群效应的地方,新版内核已经处理了相关问题。另外我们看到,这里这是修改进程为可执行状态,但是不会立刻调度,要等下一次进程调度的时候才发生进程调度。以上就是进程阻塞和非阻塞的原理。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-06-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程杂技 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档