ST源码分析-server - 弦外之音

/ 0评 / 1

本文主要讲解,ST 提供的示例程序 servermake 编译之后,会在 obj 目录生成 server 可执行文件,如下:

server 是一个 简单的 http 服务器,访问之后输出一个 简单的 html 页面,使用命令如下:

./obj/server -l ./ -b 192.168.0.122:8888

在刚开始调试的时候,推荐加上 -i 选项,可以防止 server 在后台运行。


server 流程图如下:

上面流程几个比较简单的函数,我简单过一遍:

1,parse_arguments(),解析命令行参数到 变量。

2,set_thread_throttling(),就根据一些规则设置 3 个变量,max_threadsmin_wait_threadsmax_wait_threads

3,create_listeners(),创建套接字,然后 listen,这里可以绑定多个 ip 端口 。

4,open_log_files(),创建 pid 文件,这里 pid 文件好像没有限制两个程序同时允许。最重要的是打开 ERRORS_FILE 错误文件。

5,load_configs() ,什么都没做,留给用户实现的。



先讲解一下 server 程序 中的一个重点结构体 struct socket_info

struct socket_info {
  st_netfd_t nfd;               /* Listening socket                     */
  char *addr;                   /* Bind address                         */
  unsigned int port;            /* Port                                 */
  int wait_threads;             /* Number of threads waiting to accept  */
  int busy_threads;             /* Number of threads processing request */
  int rqst_count;               /* Total number of processed requests   */
} srv_socket[MAX_BIND_ADDRS];   /* Array of listening sockets   

上面的代码定义了一个 全局数据srv_socket ,这个数组一般只有 [0] 用到,只绑定一个 ip跟端口。这个结构体的每个字段都比较重要,下面详细讲解一些:

1, st_netfd_t nfd; 这个是 服务器 listentcp 套接字,ST 自己封装了一下。

2, char *addr; IP 地址

3,unsigned int port; 端口。

4,int wait_threads; 代表有多少个协程 阻塞在 st_accept() 函数 。

5,int busy_threads 代表有多少个协程已经 从 st_accept() 拿到 fd,开始处理 http 请求。

6,int rqst_count;代表处理了多少个 http 请求。


server 程序有3个重点函数。

1,start_processes() ,开启多进程。多个进程同时 select() 监听 服务端套接字(srv_socket[0].nfd)。

2,install_sighandlers() ,注册信号 SIGTERMSIGHUPSIGUSR1, 用 pipe() 管道把 信号事件 转成 I/O 事件,这样就能用 select 监听 管道的 fd ,跟其他的网络套接字同一个 select 监听。

Only two types of external events are handled by the library's scheduler, because only these events can be detected by select(2) or poll(2): I/O events (a file descriptor is ready for I/O) and time events (some timeout has expired). However, other types of events (such as a signal sent to a process) can also be handled by converting them to I/O events. For example, a signal handling function can perform a write to a pipe (write(2) is reentrant/asynchronous-safe), thus converting a signal event to an I/O event

3,start_threads() ,开启 max_wait_threads 数量的协程,全部阻塞在 st_accept() 函数 等待客户端请求。


下面仔细分析重点函数 start_processes() ,流程图参考上面的,重点如下。

1,start_processes()fork() 很多子进程,所有子进程都会返回 main() 函数执行后续的逻辑。但是 父进程不返回 main() 函数,父进程一直阻塞在 start_processes() 函数里面,等待某个子进程结束或者意外终结,然后打印子进程退出信息,在 fork() 一个子进程。这样,父进程 就是一个 watchdog,看门人,子进程如果有代码问题,奔溃了,父进程就会被激活,收集子进程的退出信息,方便排查错误,然后父进程再生一个子进程出来处理业务。

这种 watchdog的机制特别好,因为写代码总有一些意外的情况,不是经常发生。偶尔奔溃,就子进程奔溃,那就由父进程再生一个子进程出来就行。

在 Linux 环境,有一个软件也可以实现这种 看门人功能,就是 supervisor

父进程除了等待子进程结束,还会处理信号,如下面代码,wait() 函数在等待子进程结束的时候,会被信号中断,errno 等于 EINTR 就会继续等待。

if ((pid = wait(&status)) < 0) {
     if (errno == EINTR)
continue;
     err_sys_quit(errfd, "ERROR: watchdog: wait");
   }

父进程是用 wdog_sighandler() 函数来处理信号的,所有的信号都会传递一遍给子进程,然后自己再处理信号。演示一下,我用以下命令 发送一个 SIGUSR1 信号给父进程 ,13287 是我的父进程ID。

kill -s SIGUSR1 13287

上面的命令执行之后,所有的进程都会打印一遍信息,如下:

server 程序有处理 3 种信号:

  1. SIGTERM,终结进程。
  2. SIGHUP,虽然注释写的 restart,但是实际上只是重新加载了配置文件。
  3. SIGUSR1 ,打印信息。

主进程不返回 main() , 子进程全部返回 main,然后 阻塞在 process_signals() 处理信号。

相当于 始祖协程 阻塞在 process_signals() 处理信号。 有多少个进程就有多少个始祖协程。


接下来分析最后一个重点函数 start_threads(),这个是子进程的开始协程的函数,用来处理http请求的,流程图参考上面的:

start_threads()st_thread_create() 创建了很多个协程函数 handle_connections()。这些协程函数全部阻塞在 st_accept() 那里,等待客户端请求。


相关阅读:

  1. 《进程,线程,信号》

由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1。QQ:2338195090。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注