阻塞和非阻塞的实现

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

[[403799]]

本文转载自微信公众号「编程杂技」,作者theanarkh。转载本文请联系编程杂技公众号。

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

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

  • 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.

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

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 = { currentNULL }; 
  current->state = state; 
  add_wait_queue(p, &wait); 
  save_flags(flags); 
  sti(); 
  schedule(); 
  remove_wait_queue(p, &wait); 
  restore_flags(flags); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

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

struct wait_queue { 
    struct task_struct * task; 
    struct wait_queue * next
}; 
  • 1.
  • 2.
  • 3.
  • 4.

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

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

1 加入等待队列

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

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

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); 
        } 
      } 
    } 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

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

static void def_callback1(struct sock *sk) 

  if(!sk->dead) 
    wake_up_interruptible(sk->sleep); 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

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

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

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

 

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

 

责任编辑:武晓燕 来源: 编程杂技
相关推荐

2012-10-10 10:00:27

同步异步开发Java

2015-07-03 10:12:04

编程同步非阻塞

2022-06-22 08:16:29

异步非阻塞框架

2019-07-23 11:01:57

Python同步异步

2017-03-01 16:40:12

Linux驱动技术设备阻塞

2012-02-22 21:15:41

unixIO阻塞

2025-02-17 13:23:34

Python同步阻塞MySQL

2023-03-15 08:39:07

远程服务调用

2023-07-31 08:55:01

Java NIO非阻塞阻塞

2018-03-28 08:52:53

阻塞非阻塞I

2024-11-26 10:37:19

2023-12-13 09:45:49

模型程序

2023-12-06 07:28:47

阻塞IO异步IO

2024-09-23 17:15:28

Python并发并行

2013-08-09 09:27:31

2021-02-27 16:08:17

Java异步非阻塞

2011-12-07 17:17:02

JavaNIO

2011-12-08 10:12:34

JavaNIO

2020-05-08 10:34:30

Spring非阻塞编程

2021-02-04 10:50:11

网络安全非阻塞模Winsock编程
点赞
收藏

51CTO技术栈公众号