IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> 基础服务器 IO 模型 Proactor 模型 Reactor 模型 IO 多路复用 异步 IO 模型 Linux 服务器开发 网络编程服务器模型 -> 正文阅读

[系统运维]基础服务器 IO 模型 Proactor 模型 Reactor 模型 IO 多路复用 异步 IO 模型 Linux 服务器开发 网络编程服务器模型

?本文主要记录服务器的 IO 模型的类型(从多路复用,异步 IO 讲到 Proactor Reactor 模型),包括 Real World nginx 和 apache ,kafka 等分析,配备自洽的所有知识点方便自己复习。


先把 APUE 第八章进程控制过一遍吧

Linux 进程的控制

启动与复制

  • 首先是他说的 swapper pid==0 的进程,就如在 xv6 里面的那样,init 做的就是 wait 到子进程(shell)退出然后就退出而已。scheduler 的部分是另外的,由于他的调度过程十分复杂,所以有很多设置和初始化的内容,但是大体都是在 timer interrupt 之后处理调度的问题。
  • swapper 进程是当 CPU idle 的时候运行的(现在应该没有了),这个应该是第一个内核线程(因为 init 是用户进程运行的),The swapper process, as was, used to perform process swap operations. It used to swap entire processes — including all of the kernel-space data structures for the process — out to disc and swap them back in again。然后的内核进程还有 page daemon (负责换页)等。
  • 额,现在这个应该是 idle 进程,overflow 说了:两个原因,编程原因是不用特判无进程可调度的 case,历史原因是老 CPU 不支持什么都不做(节电模式),起码要运行一大堆 NOP,今天可以用 HALT 来停机。
  • fork 后全部共享的,dup

After a successful return, the old and new file descriptors may
?????? be used interchangeably.? Since the two file descriptors refer to
?????? the same open file description, they share file offset and file
?????? status flags;

From <dup(2) - Linux manual page>

  • 总之知道 fd 对应的文件结构肯定是同一个 -> 共享 offset 几乎是必然。(fd 只不过是指向内核里的打开文件表,然后每个文件表又指向 vfs 的节点表最后到 inode)。但是子进程不会继承文件锁,因此还是有一定的线程安全性。
  • fork 和 exec 分开一个好处是可以换条件再 exec 比如修改重定向,修改用户组,解绑终端。
  • vfork 保证是要执行一个新的程序,尽管他说不推荐使用,但是实际还是有挺多区别的。第一个是 vfork 不会赋值地址空间(页表),而是直接使用父亲的,所以需要保证不要修改。然后第二点是 vfork 马上保证子进程在运行直到 exec 或者 exit 被调用回到 kernel。fork 的问题是他会新建一个 mm_struct 复制全部的页表,exec 则会创建全新的地址空间,覆写整个 mm_struct,即两步内容。(对于这个 CoW 操作到底浪费多少性能存疑)。由于共享,所以如果没有 exec,然后调用了 exit 会引发所有的文件以及标准 io 被关闭,unix 的解决方案是引入一个 _exit 不冲刷标准 io 文件符。
  • 进程可以通过 nice 修改自己的友好度,inc 意味着调度 priority 下降。

login 原理

  • 登录的原理,init 读取 ttys 文件,根据指定 fork tty数量个 init,然后他们将会运行 gettty 程序(死循环 login 提示直到登录成功),生成包含代环境变量的环境,设备将会被 open 调用驱动程序打开。getty 会执行 login 程序,login 会配置环境变量,修改终端的各种权限(终端的权限本来是继承 init which 是空的,没有任何权限),然后调用用户登录 shell (写在 passwd 里面)。
  • 如果这个登录的 shell 死了之后,顶部的 init 将会知道,他重启这个 tty 的 gettty 程序等待 login。(或者用户关闭设备后,getty 会收到一个 write 错误(或者 print),init 将停止这个 tty 设备的访问,直到下次死循环 open 这个设备又成功,循环往复)。
  • 网络登录原理,由于网络登录需要持续等待一个请求进来,本质上需要一个服务器。INADDR_ANY ,一个伪终端设备,pseudo terminal。由于不直接使用 shell,对于所有公共用户的开机自启程序 /etc/rc(which 使用脚本定义) 需要先启动,包括 inetd 服务器。
  • 而 shell 的作用只是执行脚本,结束后 shell 会死,脚本启动的程序(forked 来的)就会被 init 收养。然后 inetd 只是超级的,对于子连接协议比如 telnet 还要用 telnetd 来监控网络请求和负责和伪终端通信。伪终端是通过虚拟设备实现的(比如覆盖标准io的fd),实际还是通过驱动程序。

脱离控制终端

  • 守护进程如果由 shell 或者其他应用 fork+exec 而来,必须自己管理组,脱离活动终端。
  • crontab (不是英语,是时间表的意思),这个守护进程会定制执行程序(程序全部作为守护进程运行,即没有活动终端,可以重定向 stdio 写文件)。
  • 由于没有终端,设计 syslog 来打 log(补充一下 window 下用 OutputDebugString 然后用 DebugView 软件捕获)。(但是这个也是一个守护进程,涉及 IPC)。syslog 可配置文件,终端或者其他各种东西(比如再转发一次 IPC)。syslog 的 well-known 端口号是 UDP 514。他会循环 select log 套接字,服务套接字,内核错误套接字。
  • syslog 函数的参数,level 和 facility 的概念, level 由 EMERG,ALERT,CRIT(临界),ERR,WARNING,NOTICE,INFO,DEBUG。facility 主要是进程类型。通过一个 priority 和 | 运算符实现单个参数表示两种值了属于。
  • 这些不同的 priority 会根据 conf 路由到不同的地方,配置是基于 priority 的。openlog 可以添加选项比如进程通信失败的时候直接 stdio,控制延迟和 stderr 同步输出,每条信息登记 PID 等。然而 linux 里面(ubuntu)可能不用 syslog 了,搞了一个 rsyslog,多线程 tcp,ssl,tls,支持过滤和自定义格式,支持 module。

daemon process

  • 复习一下 APUE 先,sid,tgid 这些都是用头头进程的 pid 标识的。 session 的原理是区分不同的用户的,每个 session 会有多个进程组,进程组是用来区分各种后台的(以及控制终端),组内管理信号,组间信号独立。一个 session 有一个拥有 ct 的 group(前台进程组),以及后台进程组。终端关闭的时候会发送 sigup 信号。Ctrl+C 对应的是 SIGINT,Ctrl+Z 对应的是 SIGTSTP,Ctrl+\ 对应SIGQUIT, 关机会发送 SIGTERM 信号,最后会 SIGKILL。作业控制是 shell 支持的 shell 中创建进程组的方法。

The? daemon()? function? is for programs wishing to detach themselves from the controlling terminal and run in the background as system daemons.

  • 实现原理是,fork 之后做掉父进程,这样 shell 恢复正常。然后修改 session(脱离原来的 session 使各方面都独立),setsid 根据 APUE,会脱离控制终端,切断控制终端,成为新的 leader,但是不能已经是 leader。为了不接收 tc 的 sighup(很容易理解,就是为了告知大家控制终端被关了的实现方式->leader死了是必要条件,然而不充分) :
  • SIGHUP会在以下3种情况下被发送给相应的进程:

  1、终端关闭时,该信号被发送到session首进程以及作为job提交的进程(即用 & 符号提交的进程);

  2、session首进程退出时,该信号被发送到该session中的前台进程组中的每一个进程;

  3、若父进程退出导致进程组成为孤儿进程组,且该进程组中有进程处于停止状态(收到SIGSTOP或SIGTSTP信号),该信号会被发送到该进程组中的每一个进程。

  • 在新的 session 下再 fork 保证不会关联控制终端(这是因为如果一个会话 leader 打开一个新的终端,并且open 没有用 NOCTTY,就会绑定一个终端给整个 session)。由于守护进程有可能会打开终端(比如作为远程连接的 daemon 时)。还有一些其他设置包括重定向io,关闭文件,打 log,chdir。
  • 由于 linux 下 daemon 函数都为我们做好了,这里不再深究细节了(不过已经太多细节了)。

自启动

  • /etc/rc,这里 linux 里是一系列文件:rcx.d

rc: /etc/rc5.d /etc/rc4.d /etc/rc6.d /etc/rc0.d /etc/rc3.d /etc/rc1.d /etc/rc2.d

  • 为了减轻大量 daemon 的性能问题,使用 inetd 支持各种 daemon 服务。实现原理是通过配置文件指定各种服务的实现,他只负责 listen,这样就不用一大堆 daemon 来 listen 了。(然后 exec 就行了,通常需要提供参数)。由于 fork exec 分开的设计,可以在中间 hook socket 递交。

25 章了讲 SIGIO 的用法的。现在快速过了他。

各种 IO 模型以及信号驱动模型简析

不同 IO 辨析

  • SIGIO 的原理是不通过 select 来进行 polling,而是通过注册信号,让内核在 IO 启动的时候 interrupt 。
  • 和 asynchronous IO 的区别是,信号驱动是内核告知上层可以操作, whereas asynchronous IO 是说注册 IO 事件实现 kernel IO + 上层的 computing 的 parallelism。
  • 非阻塞只是说如果当前资源不可用才会回到上层,不然还是会 u -> k -> u 的顺序执行。
  • 额,直接看这个图(用 windows 画图画的,,)好了,红色是上层计算工作,橙色是 IO 操作,黑色是用户态进程上下文,灰色是 kernel 上下文。(异步 IO 可能画得不好,kernel 可能会等一段时间才变橙色,或者 kernel 线可能是另一个 background process 的实现也行)

  • 通过 fcntl 的 SETOWN 设置 host(意思是 SIGIO 和 SIGURG 通知的是哪个进程组,方便设置另一个线来做这个 handler),然后通过 SETFL 设置 ASYNC 选项(叫 async 是历史原因),或者通过 ioctl 设置 FIOASYNC。
  • 然后另外提供一个等价 NONBLOCK 的选项是 ioctl 的 FIONBIO。

UDP 事件:

  • datagram 到达或者出现async错误。
  • asynchronous 错误就是之前说的,recvfrom 永远诸塞就算有 ICMP 不可达信号。这是因为 sendto 没有什么 ack 机制,就算不可达,内核也不会告知 upper layer,导致本来不应该调用 recv 的,这是 asynchronuous 错误。
  • sendto 只保证了数据进入 buffer,而不是确认没有收到 ICMP since UDP 没有 ACK 机制
  • 因为 recvfrom 无法同时返回地址信息只能返回 errno,所以规定只有 connected udp 才返回这些错误,比如这里就通过信号(还是要 connected udp)。
  • 不要在 TCP 上使用 SIGIO 因为他的情况太多了。

同步问题

  • 标准的做法是生产者消费者问题,让 handler 做生产者(recvfrom),然后 main loop 不断地消费(writeto),为了保证消费成功以及维护队列 meta 变量(只有 handler 和 main loop 都用到的变量才需要 coordinate),需要 sigprocmask 短暂禁止 SIGIO。这一点知道就行了。
  • 回顾一下之前看过的 sigsuspend ,sigsuspend 和 sleeplock 的那个很像,醒来之后会恢复之前的状态,比如本来阻塞 SIGIO 然后要他睡在 SIGIO 上,收到 SIGIO 醒来之后 SIGIO 还是被阻塞的。然后根据下面的manual 说的(我又忘记了),signal handler 里面不会再接受到 masked 的信号,那些信号会排队(或者重复降为一次pending 等待递交)。

Any signals specified in act->sa_mask when registering the
???????????? handler with
sigprocmask(2) are added to the thread's
???????????? signal mask.? The signal being delivered is also added to
???????????? the signal mask, unless
SA_NODEFER was specified when
???????????? registering the handler.? These signals are thus blocked
???????????? while the handler executes.

From <signal(7) - Linux manual page>

  • 信号的排队问题和递交次数问题,之前说过信号的不可靠的 FIFO 的,之前说过的确有排队重复信号,但是 SIGIO 不是,解决方案是循环调用 NONBLOCK 的 recvfrom。

为什么 pthread 线程快

  • UNIX 线程是 lightweight process,创建比 process 快,就算 fork 是 Cow 的(3-5倍甚至几十倍)。
  • 我不知道怎么做思想实验,只能知道 glibc 里面 pthread_create 就是分配了一些栈,然后 clone 整个进程。而 fork 是 clone 再复制 4 级页表。
  • 我感觉主要区别在页表上,fork 的页表必须是独立的,然后全部设置 CoW bit,也就是必须复制 4 层页表的每一层,会有一大堆复制操作(存疑?比如几mb情况最多就4个页表,os详细内容都没什么印象了,没法定量算了。。。)而 pthread 改一个页表指针就行了属于,直接全共享。然后对于切换的情况,因为一个是复制的页表,本质还是不一样的(比如 cow bit),tlb 必须 flush,而 pthread 不用。感觉这样就够有说服力了。
  • 额,搜了一些博客才看到狗老师这篇文章Linux fork隐藏的开销,写得很好,这里摘录一个页表开销,对于稀疏地址空间,页表反而更多了,笑死。比如一个低地址一个高地址,至少 1(root) + 2(二级) + 2(三级) + 2(四级) 个目录,说起来当时学的时候没体会到这一点。。而且现在都是 heap 下面,stack 上面往下,中间还有各种 mmap,还是有很多稀疏的情况的。
  • 线程的好处是随便阻塞,反正内核会调度,而且很简单就切一下 context,pagetable 指针不带变的。这里摘一些 APUE 的原文吧。

每个线程都有自己独立的signal mask,但所有线程共享进程的signal action。这意味着,你可以在线程中调用pthread_sigmask(不是sigmask)来决定本线程阻塞哪些信号。但你不能调用sigaction来指定单个线程的信号处理方式。如果在某个线程中调用了sigaction处理某个信号,那么这个进程中的未阻塞这个信号的线程在收到这个信号都会按同一种方式处理这个信号。另外,注意子线程的mask是会从主线程继承而来的。

然后下面是一些总结。


C/S 架构

时间关系对 raw socket DL 层的没时间看了,虽然我的确很感兴趣那些可以用来抓包和做 arp 欺诈等功能。书本还讲了 ping 和 traceroute 怎么写,这下知道面试问这两个原理哪来的了。。。

首先总结我们前面做了些什么工作,(这个说的是服务器还是客户端呢?好像都有)

简单服务器和客户端

  • 基本 TCP 服务器,停止等待,无法利用 CPU
  • select 阻塞 IO 迭代服务器,问题在于标准 io 和 socket buffer 处理速度不一致,在得到标准 I 写的时候或者标准io缓冲满时还是会阻塞。
  • select 非阻塞 IO 迭代服务器,应用层生产者消费者环形缓冲区,如果 block 就挤压到应用缓冲什么都不做下次再(incremental)。
  • 多线程阻塞服务器,内核调度(性能差别在于内核调度是 timer interrupt + 两次 trap ,反正多进程flush tlb 的肯定不行,然后有内核 pcb(因为1:1线程模型)内存开销)。
  • 一种做法是多核多线程 io 复用,这样应该是理论最佳性能的,但是想达到最大吞吐还要精密的调度方案。

Real World 方案

  • apache 有三种模型,preforking 模型,一般 1024 给进程稳定低并发。worker 模型,多个主控进程配备很多被控 thread,动态更新线程数量,线程崩溃进程也崩,中间妥协。event 模型是在 worker 的基础上用一些线程来 epoll 管理 keep-alive 长连接(http需要维护一个长连接),对于真正的请求转发给服务线程。
  • nginx 是多进程模型,每个进程单线程复用。master 进程管理 worker 进程,通过信号来通信。所以同样要处理 accept 的 mutex 问题,然后 worker 内部由于是 epoll,所以全部用异步非阻塞IO,worker 配置为核数,避免 worker 间的 context switch(额,最理想就直接做个内核态服务器不好吗哈哈)。
  • libevent,Reactor 反应堆模型,IO 事件select返回的时候通过 server dispatch 给 callback function。额,这为什么起了个这么 fancy 的名字。

preforking 进程池方案

  • 父进程只是负责创建 listenfd 和 fork 一堆孩子
  • main loop 调用 pause 等待一个信号(关闭服务器),如果信号来了就 kill 孩子(sigterm)。然后 wait。
  • 孩子不断地 accept 然后处理。
  • 理解多进程 accept 原理,listenfd 是完全 dup 出来的,然后题目指向的是同一个 opened file 结构,
  • thundering herd 性能问题,这里还要由内核来保证互斥锁的问题。一种优化是让 kernel 不 thundering herd 而是只 wakeup sleep on chan 的第一个。
  • 不要让多进程阻塞在 select 上,这个和 kernel 的 socket 实现有关,如果 socket 的 wake chan 里面只记录了一个 pid,会引发冲突(就频繁睡在 select 上 which 更新这个 pid 导致原来的 select 睡者没办被唤醒)。
  • 对于 accept 没有实现互斥锁(用户态 accept)就需要自定义互斥锁了。(apache 1.1 跨平台支持就要做这两种实现)
  • 不想实现锁的方法让父进程(master)单进程 accept,就没有并发锁问题了,然而现在两个问题了他有,一个是 IPC,一个是调度策略。但是这样涉及怎么把 fd 传过去,理论上只有 fork 才能继承描述符结构体,现在通过 socketpair 来实现(Unix domain socket,啊这,我跳过这些章节了,没事 apue 还有)。
  • udsocket 的原理是 sendmsg 和 recvmsg,然后msghdr 里面会指明一些属性。里面有一个是 void* 的 msgcontrol 字段,可以让他指向一个 cmsghdr 意思是 control msg,然后这个 hdr 里面的 cmsg_type 的 SCM_RIGHTS 代表文件描述符。
  • 本来我以为 loopback 优化很好了

最开始 127.0.0.1也是走 tcp协议栈的,很多冗余的东西,而 AF_UNIX 更像跨进程管道,因此会快很多。但是后面 Linux持续优化,给 127.0.0.1加了很多 fast path。使得本机内部的tcp性能和 AF_UNIX差不多,也和管道一个数量级了,所以今天 Linux下,保证代码的简单性,没有非要用 AF_UNIX的必要。

链接:https://www.zhihu.com/question/29910140/answer/46640164

  • 然而 UDS 真的有用啊,这种涉及特殊的 PCB 数据传送,,,,为什么不直接提供一个抽象出来呢。。。

消息队列

MQ 特性

  • 消息队列是链表,具备 header:length,priority
  • mq 的标识符是 mqd,ms descriptor。mqclose 和 close 的做法是一样的(linux manual)。
  • mq 队列是内核持续性,即在内核生命周期存在。mqopen 之后会产生3个文件 .MQDxxx, .MQLxxx, .MQPxxx。unp 也不知道他们是什么,只能猜测(v22e5.3)是 data + lock + permission 三个文件。
  • 每个mq 具备一个 attr 结构体,这些是设置 mq 的属性的,一个是 flags,block 什么的和file的一样,一个是 maxmsg 链表长度,最大消息长度,当前size。比较简单的 adt。

信号(纯 posix)

  • mqnotify 函数,参数中 sigevent 是一个 posix 新的信号结构体管理实时信号(之前看到的 SIGUSRX,其中 X 是数字)的回调函数的。。。
  • 一个函数负责注册和注销(sigevent 用 nullptr 区分)。
  • 如果空的消息队列被添加了信号就会激活。
  • 然后 mqreceive 是不可重入的,所以不要在 handler 里面调用他。(记住 IO 只用 read write 就行了!)signal-safety(7) - Linux manual page (man7.org),还有回顾那个设置一个 bit(atomic),然后在main 的控制流里面轮询检测也是一种方案(但是需要非常 sophisticate 的编程,比如要配合 sigprocmask + sigsuspend + pselect 使用),5.6.3 也详细讲了这个方案的实施。
  • 整理一下这些信号引发的系统性问题,除了更久之前的因为在慢系统调用之前信号到达所以要用 pselect 或者非阻塞,前面讲非阻塞 IO 和 高级 UDP 的时候的问题是 recvfrom 之前 alarm 到达(他这里 alarm 的handler 什么都不做,只是为了打断慢系统调用而已),这里则是 mqnotify 重新注册太慢了,而信号只支持空队列添时通知,注册的时候已经来了一个了,所以失败了。(注意在处理数据的时候需要短暂 block 信号)
  • 解决方案是 mqrecv 的时候用非阻塞并且循环调用,额,这个问题又是新的了,所以循环读 mq 就行了,因为非阻塞最后会返回 egain 即 -1 bytes (return val 是 msg bytes)这个时候就能下次 mqnotify 了。(这些例程可都太重要了,但是总感觉网络编程是在 linux / unix 这个框框下搞各种注意事项和 workaround,有点 c++ 的感觉了 😓)。
  • 结果他跟我说可以思想升级,直接用 sigwait 而不用设置什么信号 bit 和 suspend 前判断 bit。sigwait 不用 handler,直接等信号发生就返回。(sigwait 调用前先 sigprocmask block 先)。这个太重要了,代码必须贴一份(首先 mark 一下路径 pxmsg/mqnotifysig4.c)

区分一下这两种方案:

mq 不支持 select 的 workaroud

  • mqd 不能被 select 和 poll!epoll 也不支持。。需要魔改一下。
  • 看完后笑死了,搞一个中间商 pipe 徒增功耗。这是什么ios 锁屏歌词刷新壁纸的方案。。。
  • 实现方法是因为 write 是 async signal safe 的,所以创建一个 private pipe,让 mqnotify 的 signal handler 写管道,这样就变相通知 poll / select 了,于是可以返回。。。。我真的持续体会各种 workaround 的难受了属于。
  • 等于搞了一大堆 context switch 和 vfs 中间路由层层转包了。

mq 异步信号通知还支持创建一个线程来 callback

  • 只需要把之前那个 event 结构体的参数设置成 SIGEV_THREAD 就行了,然后回调函数会被调用。
  • 不过 callback function 记得要用 pthread_exit 来退出。

实现问题

  • 这里用 mmap 和 fix length msg 来实现了一个互斥的 posix mq。500行 C 代码
  • 涉及互斥锁的东西。

real world - kafka

  • ?真正的中间件级别的消息队列涉及更专业的 recv 和 send。主要就是生产消费模型。
  • 由于没有内核的控制了,所以基本都要自己造轮子。(我们之前是用 kill 发信号实现 mqnotify 的)
  • 消息队列的用途比如写日志,秒杀事务backlog。
  • 但是 kafka 这个东西是集群系统的,分区+容错(没有实时,又复习 CAP 了),很难继续看下去(除非开一个源码分析来学)。

因为 Epoll 原理线索另一篇讲了(没有上下文信息应该看不懂。。。)

Reactor 和 Proactor

这里因为只能看游双那本,有点像 specification 的笔记了,但是没办法,这个东西实在是我觉得就是一个花哨名字而已。

Reactor 模式

反应堆模型,master 负责 IO 复用,监听 listend,然后 IP 通知 worker。我不懂他为什么要起一个 fancy name。

  • master epoll 全部 d ,包括 listend 和 连接好的 d。epoll wait
  • 对于 listen d,dispatch 给 connection manager 做就行了。
  • 然后 socket 从 epoll wait 回来之后,加入一个队列里(生产者消费者模型),而且应该要做一个 EPOLLONESHOT 的选项。
  • 然后还要通知 wake up 一个工作线程。(不是很懂怎么 wake up)

proactor 模式 前摄式,这个单词不懂什么意思

  • 所有 IO 让主线程负责,解耦 worker
  • 异步 IO 模型,aio read,aio write 再让主线程和 内核解耦(信号的关键字是 sigevent,其实之前讲过了,可能印象不太深)。
  • aread 成功之后,dispatch 一个 worker 来进行业务操作,然后 worker 又 aio write 进行写。

  • 实现同步 IO 模拟 proactor,

  • 原理是主线程来完成本来交给 aio 的工作。(用户态调度 io 了属于)

性能分析

  • 对于 compute bound 的,上下文切换浪费生命。
  • 对于 IO bound,切换上下文浪费的时间就很少了。
  • HTTP 解析是一个有限状态机。
  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2021-11-12 19:59:45  更:2021-11-12 20:01:54 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/15 23:33:22-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码