router学习笔记

router相关的一些基础知识学习与理解。uhttpd源码分析+openwrt启动流程学习

router

基础知识学习与理解

环境

串口调试工具 hyperterminal,需要配合杜邦线和usb转ttl转接板。注意如果提前连接好杜邦线再插入usb口可能会获取不到信息,这时要先插入usb口然后开启路由器等待一段时间后将杜邦线插入到ttl转usb转接板上,接法是GND-GND、RXD-TXD、TXD-RXD。

Bootloader

Bootloader 是系统上电或复位后最先执行的程序。它的任务是初始化硬件并加载操作系统内核到 RAM 中并运行它。如RAM初始化、内核镜像调用、内核解密、文件系统解密等。

常见的 bootloader 有:

GRUB:GNU GRand Unified Bootloader 是 Linux、UNIX 和其他操作系统的常见 bootloader。它提供了一个菜单,允许用户选择不同的操作系统或内核版本。

LILO:Linux Loader,是早期的 Linux bootloader,现已被 GRUB 替代。

U-Boot:通用 bootloader,主要用于嵌入式设备。

Fastboot:Android 设备上的 bootloader。

RedBoot:另一种嵌入式设备 bootloader。

Syslinux/Isolinux:用于启动 live CD 或其他轻量级媒体的 bootloader。

EFI/UEFI:现代计算机上的 bootloader 接口,用于替代传统的 BIOS。

通常,bootloader 存储在设备的只读内存 (ROM) 或其他不可修改的存储中,以确保它在设备启动时始终可用。但也有一些 bootloader 是可以更新的,这样能够为设备提供新功能或修复错误。

uhttpd源码分析

先从main函数开始,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main(int argc, char **argv)
{
struct alias *alias;
bool nofork = false;
char *port;
int opt, ch;
int cur_fd;
int bound = 0;
#ifdef HAVE_TLS
int n_tls = 0;
const char *tls_key = NULL, *tls_crt = NULL, *tls_ciphers = NULL;
#endif
#ifdef HAVE_LUA
const char *lua_prefix = NULL, *lua_handler = NULL;
#endif
#ifdef HAVE_UCODE
const char *ucode_prefix = NULL, *ucode_handler = NULL;
#endif

根据条件初始化一些变量

1
2
3
4
5
BUILD_BUG_ON(sizeof(uh_buf) < PATH_MAX);//检查缓冲区大小

uh_dispatch_add(&cgi_dispatch);
init_defaults_pre();
signal(SIGPIPE, SIG_IGN);

uh_dispatch_add函数负责吧cgi添加到链表中

1
2
3
4
5
6
7
8
9
struct dispatch_handler cgi_dispatch = {
.script = true,
.check_path = check_cgi_path,
.handle_request = cgi_handle_request,
};
void uh_dispatch_add(struct dispatch_handler *d)
{
list_add_tail(&d->list, &dispatch_handlers);
}
  • .script = true:这可能意味着该处理器是用来处理脚本请求的。
  • .check_path = check_cgi_path:一个函数指针,可能用于检查请求路径是否为有效的CGI路径。
  • .handle_request = cgi_handle_request:一个函数指针,用于处理CGI请求。

init_defaults_pre函数负责初始化配置的默认值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void init_defaults_pre(void)
{
conf.script_timeout = 60;
conf.network_timeout = 30;
conf.http_keepalive = 20;
conf.max_script_requests = 3;
conf.max_connections = 100;
conf.realm = "Protected Area";
conf.cgi_prefix = "/cgi-bin";
conf.cgi_path = "/sbin:/usr/sbin:/bin:/usr/bin";
INIT_LIST_HEAD(&conf.cgi_alias);
INIT_LIST_HEAD(&conf.lua_prefix);
#if HAVE_UCODE
INIT_LIST_HEAD(&conf.ucode_prefix);
#endif
}

signal(SIGPIPE, SIG_IGN);用来忽略信号,防止客户端突然断开导致uhttpd进程退出

1
2
3
4
5
`signal(SIGPIPE, SIG_IGN);` 是一个常见的编程技巧,用于忽略 `SIGPIPE` 信号。在网络编程中,当试图写入一个已经关闭的套接字或管道时,进程会收到一个 `SIGPIPE` 信号。默认情况下,这个信号会导致进程终止。

在服务器应用程序中,客户端突然断开连接是一个常见的情况。如果没有处理 `SIGPIPE` 信号,那么每当这种情况发生时,服务器进程都可能会意外终止,这显然是不可接受的。

通过使用 `signal(SIGPIPE, SIG_IGN);`,进程告诉操作系统它希望忽略 `SIGPIPE` 信号。这意味着即使尝试写入已关闭的套接字,进程也不会因为收到 `SIGPIPE` 信号而终止。相反,写入操作会返回一个错误,通常是 `EPIPE`,这样程序可以适当地处理这种情况,而不是意外终止。

在后面就是一个很大的switch case负责解析命令行参数

1
while ((ch = getopt(argc, argv, "A:ab:C:c:Dd:E:e:fh:H:I:i:K:k:L:l:m:N:n:O:o:P:p:qRr:Ss:T:t:U:u:Xx:y:")) != -1)

具体对每个参数的解释和编译配置相关,假如某些配置没有编译那么就会报错,如TLS的选项没有的话就会打印如下错误信息

1
2
fprintf(stderr, "uhttpd: TLS support not compiled, "
"ignoring -%c\n", ch);

参数的解释与使用可以看源码的usage函数。

switch case完后会执行如下函数

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
static void uh_config_parse(void)
{
const char *path = conf.file;
FILE *c;
char line[512];
char *col1;
char *col2;
char *eol;

if (!path)
path = "/etc/httpd.conf";

c = fopen(path, "r");
if (!c)
return;

memset(line, 0, sizeof(line));

while (fgets(line, sizeof(line) - 1, c)) {
if ((line[0] == '/') && (strchr(line, ':') != NULL)) {
if (!(col1 = strchr(line, ':')) || (*col1++ = 0) ||
!(col2 = strchr(col1, ':')) || (*col2++ = 0) ||
!(eol = strchr(col2, '\n')) || (*eol++ = 0))
continue;

uh_auth_add(line, col1, col2);
} else if (!strncmp(line, "I:", 2)) {
if (!(col1 = strchr(line, ':')) || (*col1++ = 0) ||
!(eol = strchr(col1, '\n')) || (*eol++ = 0))
continue;

uh_index_add(strdup(col1));
} else if (!strncmp(line, "E404:", 5)) {
if (!(col1 = strchr(line, ':')) || (*col1++ = 0) ||
!(eol = strchr(col1, '\n')) || (*eol++ = 0))
continue;

conf.error_handler = strdup(col1);
}
else if ((line[0] == '*') && (strchr(line, ':') != NULL)) {
if (!(col1 = strchr(line, '*')) || (*col1++ = 0) ||
!(col2 = strchr(col1, ':')) || (*col2++ = 0) ||
!(eol = strchr(col2, '\n')) || (*eol++ = 0))
continue;

uh_interpreter_add(col1, col2);
}
}

fclose(c);
}

参数-c可以指定config文件,如果没有指定的话就会使用默认的/etc/httpd.conf

然后就是解析config文件分为四种

  1. / 开头,且包含 : 的行
    这种类型的行被认为是定义了HTTP Basic Auth的行。每一行的格式应为/path:username:password,其中/path是需要保护的路径,username是用户名,password是密码。这些信息会被提取出来,并通过调用uh_auth_add(line, col1, col2)函数添加到验证信息列表中。
  2. I: 开头的行
    这种类型的行用来定义默认的索引文件。索引文件的名称紧随I:之后。该名称会被提取出来,通过uh_index_add(strdup(col1))函数添加到索引文件列表中。
  3. E404: 开头的行
    这种类型的行用来定义HTTP 404错误的处理程序。错误处理程序的路径紧随E404:之后。该路径会被提取出来,保存到conf.error_handler变量中。
  4. \* 开头,且包含 : 的行
    这种类型的行用来定义解释器。每一行的格式应为*.ext:/path/to/interpreter,其中*.ext是需要被解释的文件扩展名,/path/to/interpreter是用来解释这类文件的解释器的路径。这些信息会被提取出来,并通过调用uh_interpreter_add(col1, col2)函数添加到解释器列表中。

接着依旧是根据编译选项的几个分支处理

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
#ifdef HAVE_TLS
if (n_tls) {
if (!tls_crt || !tls_key) {
fprintf(stderr, "Please specify a certificate and "
"a key file to enable SSL support\n");
return 1;
}

if (uh_tls_init(tls_key, tls_crt, tls_ciphers))
return 1;
}
#endif

#ifdef HAVE_LUA
if (lua_handler || lua_prefix) {
fprintf(stderr, "Need handler and prefix to enable Lua support\n");
return 1;
}

if (!list_empty(&conf.lua_prefix) && uh_plugin_init("uhttpd_lua.so"))
return 1;
#endif
#ifdef HAVE_UCODE
if (ucode_handler || ucode_prefix) {
fprintf(stderr, "Need handler and prefix to enable ucode support\n");
return 1;
}

if (!list_empty(&conf.ucode_prefix) && uh_plugin_init("uhttpd_ucode.so"))
return 1;
#endif
#ifdef HAVE_UBUS
if (conf.ubus_prefix && uh_plugin_init("uhttpd_ubus.so"))
return 1;
#endif

主要是负责初始化工作。

下面是根据参数来判断是否fork使得uhttpd成为守护进程,从而可以在后台无干扰的运行,其中cur_fd = open("/dev/null", O_WRONLY);这一行使其无需与任何终端交互。

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
/* fork (if not disabled) */
if (!nofork) {
switch (fork()) {
case -1:
perror("fork()");
exit(1);

case 0:
/* daemon setup */
if (chdir("/"))
perror("chdir()");

cur_fd = open("/dev/null", O_WRONLY);
if (cur_fd > 0) {
dup2(cur_fd, 0);
dup2(cur_fd, 1);
dup2(cur_fd, 2);
}

break;

default:
exit(0);
}
}

至于守护进程解释如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
守护进程(Daemon Process)是运行在Unix和类Unix系统(如Linux)后台的特殊进程。与普通进程不同,它不与任何终端相关联。以下是守护进程的一些关键特点:

1. **后台运行**:守护进程在后台运行,不需要与前台交互。

2. **没有终端**:守护进程没有控制终端,因此它不会收到用户输入。

3. **生命周期**:通常,守护进程的生命周期从系统启动开始,一直到系统关闭。

4. **常见的守护进程**:很多系统级的服务,如web服务器、数据库服务器等,都是作为守护进程运行的。

5. **创建守护进程**:通常,守护进程是通过以下步骤创建的:
- 调用`fork()`并使父进程退出,这样子进程成为孤儿进程,并被init进程(进程ID为1)接管。
- 更改文件模式掩码(umask),确保守护进程有适当的文件权限。
- 更改工作目录到根目录,以确保守护进程不会阻止任何文件系统被卸载。
- 关闭所有打开的文件描述符,这样守护进程不会持有不需要的资源。
- 打开`/dev/null`,并将标准输入、输出和错误重定向到它,这样守护进程不会在任何终端上显示输出或接收输入。
- 调用`setsid()`以创建新的会话,并使守护进程成为会话领导。

6. **使用场景**:守护进程通常用于提供各种网络服务(例如HTTP、SSH、FTP)或执行特定任务,如日志轮换、备份和系统监控。

7. **结束与管理**:守护进程可以由系统管理员使用命令(如`kill`)终止,或通过系统工具(如`systemd``init``upstart`等)进行管理和监控。

最后就是

return run_server();

到此main函数结束。

服务启动函数定义在main函数如下

1
2
3
4
5
6
7
8
9
10
static int run_server(void)
{
uloop_init(); // 初始化事件循环库
uh_setup_listeners(); // 设置HTTP服务器的监听器
uh_plugin_post_init(); // 初始化插件后的处理
uloop_run(); // 运行事件循环,等待客户端连接和处理

return 0;
}

下面分析其功能

  1. uloop_init:

    这个函数用于初始化uloop库。在开始使用任何uloop功能之前,你首先需要调用此函数。它为后续的事件处理做好准备。

  2. uloop_run:

    这个函数启动了事件循环。当你设置了一些事件监听器或计时器并准备好处理这些事件时,你可以调用这个函数。事件循环将持续运行,直到没有更多的事件要处理,或者直到调用uloop_end

主要功能

uloop的主要目的是允许你的程序异步地响应各种事件,如:

  • 文件描述符上的可读/可写事件:例如,当有新的TCP连接到达监听套接字时,或当一个文件变得可读时。
  • 计时器:允许你在某个时间点后执行某个操作。
  • 信号:允许你捕获和响应系统信号。

简单来说,uloop允许你编写一个能够同时处理多个操作并响应外部事件的程序,而无需使用多线程或多进程。

使用uloop的一般流程是:

  1. 使用uloop_init初始化。

  2. 设置你想要监听的事件。

  3. 调用uloop_run来启动事件循环。

  4. 在事件循环中,你的回调函数将被触发并处理相关的事件。

  5. 当完成所有操作或需要退出程序时,可以调用uloop_end来结束事件循环。

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
void uh_setup_listeners(void)
{
struct listener *l;
int yes = 1;

list_for_each_entry(l, &listeners, list) {
int sock = l->fd.fd;

/* TCP keep-alive */
if (conf.tcp_keepalive > 0) {
#ifdef linux
int tcp_ka_idl, tcp_ka_int, tcp_ka_cnt, tcp_fstopn;

tcp_ka_idl = 1;
tcp_ka_cnt = 3;
tcp_ka_int = conf.tcp_keepalive;
tcp_fstopn = 5;

setsockopt(sock, SOL_TCP, TCP_KEEPIDLE, &tcp_ka_idl, sizeof(tcp_ka_idl));//TCP_KEEPIDLE: 这定义了当连接上最后一次数据传输之后,多久开始发送keep-alive探针。
setsockopt(sock, SOL_TCP, TCP_KEEPINTVL, &tcp_ka_int, sizeof(tcp_ka_int));//TCP_KEEPINTVL: 在探针之间等待的时间。
setsockopt(sock, SOL_TCP, TCP_KEEPCNT, &tcp_ka_cnt, sizeof(tcp_ka_cnt));//TCP_KEEPCNT: 在认为连接已死之前,需要发送的未被确认的探针的数量。
setsockopt(sock, SOL_TCP, TCP_FASTOPEN, &tcp_fstopn, sizeof(tcp_fstopn));//TCP_FASTOPEN: 它允许数据在初始的SYN(TCP连接的开始)中被发送,这可以加速开放新的连接。
#endif

setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &yes, sizeof(yes));
}

l->fd.cb = listener_cb;
uloop_fd_add(&l->fd, ULOOP_READ);//将监听器的文件描述符(socket)添加到事件循环中,并设置为读模式。
}
}

函数的主要步骤如下:

  1. 遍历每一个监听器,服务可能多端口监听。

  2. 如果配置中指定了TCP keep-alive的时间,那么设置相关的socket选项。这是用来检测并丢弃死掉的连接,确保资源不会被无效的客户端长时间占用。

  3. 在Linux系统上,可以设置更详细的TCP keep-alive参数,如keep-alive闲置时间、未确认的探针数等。

  4. 为每个监听器设置回调函数listener_cb,该函数会在有新的连接请求时被调用。

  5. 将监听器添加到事件循环中,并设置为监听读事件。这意味着每当有新的连接请求时,listener_cb函数将被调用。

简单来说功能是为了初始化所有的监听器为其设置回调函数。

回调函数功能如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void listener_cb(struct uloop_fd *fd, unsigned int events)
{
struct listener *l = container_of(fd, struct listener, fd);

while (1) {
if (!uh_accept_client(fd->fd, l->tls))
break;
}

if (conf.max_connections && n_clients >= conf.max_connections)
uh_block_listener(l);
}
static void uh_block_listener(struct listener *l)
{
uloop_fd_delete(&l->fd);
n_blocked++;
l->blocked = true;
}

不断接受客户端链接,超出最大链接数量则阻塞。阻塞函数为uh_block_listener会为其设置对应的标志位,并增加对应的引用计数。

uh_accept_client函数定义在client.c

这个函数会尝试接受一个新的客户端的链接

  • 先声明和初始化一些局部变量。
  • 使用accept函数尝试从给定的文件描述符fd接受一个新的客户端连接。
    • 如果accept失败,则返回false
  • set_addr函数用于保存客户端和服务器的地址信息。
  • 根据是否是TLS连接,为新连接分配一个流并设置适当的回调。
  • ustream_fd_init用于初始化新连接的ustream。
  • uh_poll_connection函数调用来轮询连接。
  • list_add_tail将新客户端添加到客户端列表中。
  • 增加n_clients的计数器。

uh_plugin_post_init函数主要是初始化所注册的插件

1
2
3
4
5
6
7
8
void uh_plugin_post_init(void)
{
struct uhttpd_plugin *p;

list_for_each_entry(p, &plugins, list) // 遍历所有已注册的插件
if (p->post_init) // 检查插件是否有post_init方法
p->post_init(); // 调用该插件的post_init方法
}

以上是基本的启动流程代码,还有很多其他的部分如cgi处理,总的来说这类web服务器功能就是接受客户端的请求并做出响应,可以自行处理也可以交给后端的一些其他接口来处理如cgi lua php等

openwrt启动流程

vmlinux-->/etc/preinit-->/sbin/init-->/etc/inittab-->/etc/rc.d/*

内核在执行完kernel_init后会执行/etc/preinit脚本,该脚本主要功能负责初始化,特别是文件系统。preinit的工作之一就是将rom和overlay的文件系统合并。其次就是一些环境变量的设置,硬件方面的检测和设置等。

preinit执行完后会执行/sbin/init,该程序会负责初始化系统并管理所有其他的进程,通过解析/etc/inittab文件来实现,文件如下

1
2
3
4
5
::sysinit:/etc/init.d/rcS S boot
::shutdown:/etc/init.d/rcS K shutdown
ttyS0::askfirst:/usr/libexec/login.sh
hvc0::askfirst:/usr/libexec/login.sh
tty1::askfirst:/usr/libexec/login.sh

前俩行分别指系统初始化和关机时要启动的脚本他们分别会执行/etc/rc.d/下所有S或K开头的脚本,按照优先级依次执行。rc.d中存放的都是快捷方式链接到了init.d目录下。

想要持续化可以考虑修改rc.d和init.d目录下的文件,创建对应的文件也可以。前提是有overlay特性,注意不能修改preinit,因为preinit使用的还是rom中的,运行完后会把overlay和rom合并。

具体可见参考资料openwrt启动流程


router学习笔记
http://www.psbazx.com/2024/01/22/router学习笔记/
Beitragsautor
皮三宝
Veröffentlicht am
January 22, 2024
Urheberrechtshinweis