DNS查询lookupdns程序分析-下—StateThreads示例程序介绍

作者:罗上文,微信:Loken1,公众号:FFmpeg弦外之音

lookupdns 程序的流程图如下:

1-1

lookupdns 程序有 3 个重点,如下:

1, st_thread_create(do_resolve,...) 创建协程

什么是协程?你可以把协程理解成一个函数,do_resolve() 函数就是一个协程,在 lookupdns 里面创建了两个 do_resolve() 协程。

虽然一个线程里面可以有多个协程,但是任何一个协程都是独占 线程的时间片的。假设 lookupdns 里面有两个协程 A 跟 B,协程A 在 0.02~0.04 的时候运行,然后协程 A 阻塞了。协程 A 发送完 DNS 请求之后就阻塞了,在等待服务器回复。这时候,协程B 就在 0.04~0.06 的时间片运行,来发送 DNS 请求。

任何一个时候,都只有一个协程在运行。

而且 StateThreads 的协程,可以说是由我们的程序主动切换的,是我们主动切换的协程,就是在 st_recvfrom() 函数里面进行切换协程。

st_recvfrom() 是一个阻塞函数,但是不要认为阻塞就是坏事,无论是线程阻塞,还是协程阻塞,阻塞都不代表计算机停下来不工作了,而是计算机切换到另个一个地方去工作了,所以在当前的任务看起来就是阻塞的状态。

例如 线程A 阻塞了,CPU 就会切换到 线程B 进行工作。协程A 阻塞了,CPU 就会切换到 协程B 进行工作。这是同样的原理来的。

阻塞是因为需要等待 文件IO 到来,或者 网络IO 到了,程序必须等待 读取到文件数据 或者 读取到网络的IO数据 才能往下走,这时候就需要阻塞。


回到我们主题,StateThreads 的协程是主动切换的,就在 st_recvfrom() 函数里面进行切换协程。主动切换有一个好处,就是多协程操作一些全局变量的时候会变得非常方便,不需要加锁。

多线程 针对一个 int num 进行 ++ 操作的时候,通常需要加锁,或者采用一些别的方法技巧,否则就会导致数据错乱。

多协程 针对一个 int num 进行 ++ 操作的时候,就完全不需要加锁,伪代码如下:

int num = 0;
void *do_resolve(void *host){
    num++; //全局变量 num ++
    ...省略逻辑...
    st_sendto();
    st_recvfrom();
    ...省略逻辑...
}

上面的代码里,即使两个 do_resolve() 协程跑起来,num 的递增也是正确的,最后的结果一定是 2 。为什么呢?

答:虽然 num++ 这行 C 代码翻译成汇编是 3 条指令,如下:

movl    num(%rip), %eax
addl    $1, %eax
movl    %eax, num(%rip)

上面的代码先把 num 的值放到 eax 寄存器,然后对 eax 寄存器加1,最后把 eax 的值拷贝回去 num 的内存地址。

这 3 条指令是存在中间状态的,但是只有执行到后面 st_recvfrom() 函数才会切换协程,所以这 3 条指令肯定是一起执行完的,中间不会切换到其他协程。所以不会有可见性问题

而如果是多线程来运行 do_resolve,线程的切换是操作系统控制的,所以可能会在 num++ 的 3 条指令中间切开。

例如当线程A执行到 第1条指令的时候,刚读取 0 到 eax 的时候。就切换到 线程B。这时候,线程 A 看到的 num 内存数据的值是 0 ,线程 B 看见的 num 内存的数据也是 0,当线程 B 把 eax + 1 之后,把 eax 的值拷贝回去 num 的内存地址 之后。操作系统再切换回 线程A,因为线程A 之前已经读取 0 到 eax 了,所以此时线程 A 的 eax 寄存器是 0 ,而不是 线程 B 修改后的 1。

线程 A 把 eax + 1 之后,再把 eax 的值拷贝回去 num 的内存地址。

所以最终 线程A+B 运行完之后, num 的内存地址是 1,而不是 2,这就是线程间的可见性问题。

但是多协程是没有这个可见性问题的,因为不会在 num++ 的中间被切走。这就是 StateThreads 用户层主动切换协程的好处。

提醒:每个线程也是有自己的独立的 eax 等通用寄存器的。


2,st_netfd_open_socket() 打开 socket

1-2

上图的 socket() 函数是操作系统的函数,是用来创建一个 UDP socket 的,如果不了解网络编程,请先阅读一遍《Unix网络编程》。

我们知道 UDP socket 其实是不需要 open 的,只需创建一个 UDP socket ,然后 sendto 直接往这个 UDP socket 发送数据就行了。

那疑问来了,这个 st_netfd_open_socket() 是干什么的?

st_netfd_open_socket() 函数的作用其实就是创建一个 StateThread 自己的 socket,也就是 _st_netfd_st_netfd 是对 UDP socket 进行了一层包装,方便使用,如下:

typedef struct _st_netfd {
  int osfd;                   /* Underlying OS file descriptor */
  int inuse;                  /* In-use flag */
  void *private_data;         /* Per descriptor private data */
  _st_destructor_t destructor; /* Private data destructor function */
  void *aux_data;             /* Auxiliary data for internal use */
  struct _st_netfd *next;     /* For putting on the free list */
} _st_netfd_t;

这里需要注意,StateThread 协程的切换,就是根据 socket fd 进行切换的,只有某个 fd 阻塞了,才会进行协程切换。

当然你可以用 st_usleep() 让出来当前协程的时间片,st_usleep() 也会导致切换到其他协程运行。这个函数后面会讲到。

st_recvfrom() 进行协程切换的地方如下图所示:

1-3

st_netfd_poll() 里面协程管理的逻辑是非常复杂的,这里简单剧透一下,总之就是一个线程里面需要执行的指令有很多很多,这些指令又分给线程里面的各个协程。

协程有自己的上下文,可以理解为协程有自己的寄存器跟栈空间。当开始切换到协程B的时候,会把协程A的 eax 寄存器等等信息先保存到内存里面,然后再把协程B的寄存器数据换进来,协程B 的寄存器信息之前在内存里。

协程B的寄存器数据换进来之后,协程B 就能继续跑了。协程B 阻塞之后,StateThread 的管理器就会看看还有没其他协程需要运行,如果有,就切换到其他协程运行。例如 协程B 阻塞的时候,协程A 的网络IO 已经有数据到了,那协程A 就可以激活继续跑。

st_netfd_poll() 里面干的就是这么一个事情。


版权所属 xianwanzhiyin.net 罗上文 2023 all right reserved,powered by Gitbook该文件修订时间: 2024-01-09 18:00:21

results matching ""

    No results matching ""