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文件分为四种
以 /
开头,且包含 :
的行 : 这种类型的行被认为是定义了HTTP Basic Auth的行。每一行的格式应为/path:username:password
,其中/path
是需要保护的路径,username
是用户名,password
是密码。这些信息会被提取出来,并通过调用uh_auth_add(line, col1, col2)
函数添加到验证信息列表中。
以 I:
开头的行 : 这种类型的行用来定义默认的索引文件。索引文件的名称紧随I:
之后。该名称会被提取出来,通过uh_index_add(strdup(col1))
函数添加到索引文件列表中。
以 E404:
开头的行 : 这种类型的行用来定义HTTP 404错误的处理程序。错误处理程序的路径紧随E404:
之后。该路径会被提取出来,保存到conf.error_handler
变量中。
以 \*
开头,且包含 :
的行 : 这种类型的行用来定义解释器。每一行的格式应为*.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 if (!nofork) { switch (fork()) { case -1 : perror("fork()" ); exit (1 ); case 0 : 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(); uh_plugin_post_init(); uloop_run(); return 0 ; }
下面分析其功能
uloop_init :
这个函数用于初始化uloop库。在开始使用任何uloop功能之前,你首先需要调用此函数。它为后续的事件处理做好准备。
uloop_run :
这个函数启动了事件循环。当你设置了一些事件监听器或计时器并准备好处理这些事件时,你可以调用这个函数。事件循环将持续运行,直到没有更多的事件要处理,或者直到调用uloop_end
。
主要功能 :
uloop
的主要目的是允许你的程序异步地响应各种事件,如:
文件描述符上的可读/可写事件 :例如,当有新的TCP连接到达监听套接字时,或当一个文件变得可读时。
计时器 :允许你在某个时间点后执行某个操作。
信号 :允许你捕获和响应系统信号。
简单来说,uloop
允许你编写一个能够同时处理多个操作并响应外部事件的程序,而无需使用多线程或多进程。
使用uloop
的一般流程是:
使用uloop_init
初始化。
设置你想要监听的事件。
调用uloop_run
来启动事件循环。
在事件循环中,你的回调函数将被触发并处理相关的事件。
当完成所有操作或需要退出程序时,可以调用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; 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)); setsockopt(sock, SOL_TCP, TCP_KEEPINTVL, &tcp_ka_int, sizeof (tcp_ka_int)); setsockopt(sock, SOL_TCP, TCP_KEEPCNT, &tcp_ka_cnt, sizeof (tcp_ka_cnt)); setsockopt(sock, SOL_TCP, TCP_FASTOPEN, &tcp_fstopn, sizeof (tcp_fstopn));#endif setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &yes, sizeof (yes)); } l->fd.cb = listener_cb; uloop_fd_add(&l->fd, ULOOP_READ); } }
函数的主要步骤如下:
遍历每一个监听器,服务可能多端口监听。
如果配置中指定了TCP keep-alive的时间,那么设置相关的socket选项。这是用来检测并丢弃死掉的连接,确保资源不会被无效的客户端长时间占用。
在Linux系统上,可以设置更详细的TCP keep-alive参数,如keep-alive闲置时间、未确认的探针数等。
为每个监听器设置回调函数listener_cb
,该函数会在有新的连接请求时被调用。
将监听器添加到事件循环中,并设置为监听读事件。这意味着每当有新的连接请求时,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
接受一个新的客户端连接。
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) p->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启动流程