# cserv 高效網頁伺服器 [cserv](https://hackmd.io/@sysprog/linux2024-ktcp/%2F%40sysprog%2Flinux2024-ktcp-d#L11-ktcp) ### main function ```c int main(int argc, char **argv) { if (argc != 2) { usage(argv[0]); return 0; } if (str_equal(argv[1], "conf")) { load_conf(CONF_FILE); conf_env_init(); print_env(); return 0; } if (str_equal(argv[1], "stop")) { send_signal(SHUTDOWN_SIGNAL); printf("cserv: stopped.\n"); return 0; } /*...*/ /* start the service */ sys_daemon(); proc_title_init(argv); load_conf(CONF_FILE); conf_env_init(); shm_init(); log_init(); tcp_srv_init(); process_init(); printf("cserv: started.\n"); master_process_cycle(); worker_process_cycle(); return 0; } ``` 分析程式碼運作流程 ### load_conf 其中設定檔位於 /conf/cserv.conf,可以設定 log 路徑、等級、worker process 數量、每個 worker 最大連線數、coroutine stack 大小、監聽連接埠以及 HTTP request line 跟 header 的大小 ### conf_env_init 將設定檔內的數值載入到 process 內。 ``` ./cserv conf Number of processor(s) : 16 Log file path : /tmp/cserv.log // g_log_path Logging level : CRIT // g_log_strlevel Number of work processes : 16 // g_worker_processes Connection of each worker : 1024 // g_worker_connections Coroutine stack size : 4KiB // g_coro_stack_kbytes Web server listen port : localhost:8081 // g_server_addr ``` ### shm_init [shm_init](https://hackmd.io/@sysprog/linux2024-ktcp/%2F%40sysprog%2Flinux2024-ktcp-d#Shared-memory) ### log_init [log_init](https://hackmd.io/@sysprog/linux2024-ktcp/%2F%40sysprog%2Flinux2024-ktcp-d#Logger) ### tcp_srv_init ```c void tcp_srv_init() { listen_fd = create_tcp_server(g_server_addr, g_server_port); accept_lock = shm_alloc(sizeof(spinlock_t)); if (!accept_lock) { printf("Failed to allocate global accept lock\n"); exit(0); } } ``` ```c static int create_tcp_server(const char *ip, int port) { int listenfd = socket(AF_INET, SOCK_STREAM, 0); // 建立一個 IPV4 的 TCP socket if (listenfd == -1) { printf("Failed to initiate socket: %s\n", strerror(errno)); exit(0); } if (set_reuse_addr(listenfd)) { // 設定 socket 可以在關閉後馬上重用 printf("Failed to set reuse listen socket: %s\n", strerror(errno)); exit(0); } if (set_nonblock(listenfd)) { // 設定 對 socket 監聽為 nonblocking printf("Failed to set listen socket non-bloacking: %s\n", strerror(errno)); exit(0); } struct sockaddr_in svraddr; memset(&svraddr, 0, sizeof(svraddr)); svraddr.sin_family = AF_INET; svraddr.sin_port = htons(port); svraddr.sin_addr.s_addr = ip_to_nl(ip); if (0 != bind(listenfd, (struct sockaddr *) &svraddr, sizeof(svraddr))) // 綁定 listenfd 到特定 svraddr { printf("Failed to bind: %s\n", strerror(errno)); exit(0); } if (0 != listen(listenfd, 1000)) { // 設定監聽模式,準備接收請求 printf("Failed to listen: %s\n", strerror(errno)); exit(0); } return listenfd; } ``` 在 shm_init 之中已經配置一個 master 跟 worker 可以溝通的共享空間,它自帶一個鎖,如果要使用這個空間就先將鎖鎖上,使用後在將鎖釋放。 而在 `tcp_srv_init` 之中需要一個全局的鎖(accept_lock),所以透過使用共享空間建立一個大小為一個鎖的空間,並回傳該鎖的起始位置。 ```c /* Allocate without reclaiming */ void *shm_alloc(size_t size_bytes) { size_bytes = ROUND_UP(size_bytes, MEM_ALIGN); spin_lock(&shm_object->lock); if (unlikely(shm_object->offset + size_bytes > shm_object->size)) { spin_unlock(&shm_object->lock); return NULL; } char *addr = shm_object->addr + shm_object->offset; shm_object->offset += size_bytes; spin_unlock(&shm_object->lock); return addr; } ``` ### process_init ```c void process_init() { worker_pid = getpid(); mastr_pid = worker_pid; g_process_type = MASTER_PROCESS; set_proc_title("cserv: master process"); create_pidfile(mastr_pid); if (log_worker_alloc(worker_pid) < 0) { printf("Failed to allocate log for process:%d\n", worker_pid); exit(0); } /*...*/ } ``` 根據main process pid 為 master pid 建立 pidfile,寫入 `conf/cserv.pid`。 **Master/Worker process** ```c struct process { pid_t pid; int cpuid; /* bounded cpu ID */ }; static struct process worker[MAX_WORKER_PROCESS]; void process_init() { /*...*/ for (int i = 0; i < g_worker_processes; i++) { struct process *p = &worker[i]; p->pid = INVALID_PID; p->cpuid = i % get_ncpu(); } } ``` 在 1 個 cpu core 運行 1 個 worker process 利用 mod 把 worker process 分配給各個 cpu ### master_process_cycle ```c void master_process_cycle() { if (master_init_proc && master_init_proc()) { ERR("Failed to init master process, cserv exit"); exit(0); } INFO("master success running..."); for (;;) { /*..這裡先不提..*/ if (shall_create_worker) { shall_create_worker = false; spawn_worker_process(); if (worker_pid != mastr_pid) break; } log_scan_write(); usleep(10000); } } ``` 在 master process 回圈中有兩個工作 1. 收到 signal (之後再談)後的一系列操作 2. fork cpu 數量個 child process (`spawn_worker_process`) 並綁定 child process 在特定 cpu 上 ### worker_process_cycle ```c void worker_process_cycle() { if (worker_init_proc && worker_init_proc()) { ERR("Failed to initialize worker process"); exit(0); } schedule_init(g_coro_stack_kbytes, g_worker_connections); event_loop_init(g_worker_connections); dispatch_coro(worker_accept_cycle, NULL); INFO("worker success running..."); schedule_cycle(); } ``` `schedule_init` : 建立一個 排程器,裡面會有active/inative/idle 的 coroutine,以後會在細說。 `event_loop_init` : 建立一個可以接受多個請求的 event loop 註冊想要監聽的 socket ,使用 `epoll_wait` 來對多個 socket 做監聽。 接下來每個 worker 運作時會先派發一個 coroutine 來處理 tcp accept (等待 connection 進來並且處理) 的工作 每個 coroutine 會執行 `worker_accept_cycle` 最後進入 coroutine 排程的循環 ```c static int worker_accept() // 監聽 socket 是否有請求有回傳 由 accept 建立與 user {//之間的 file descriptor 若無 則立即回傳 0 因為 socket 被設定為 non-blocking struct sockaddr addr; socklen_t addrlen; int connfd; if (likely(g_worker_processes > 1)) { if (worker_can_accept() && spin_trylock(accept_lock)) { connfd = accept(listen_fd, &addr, &addrlen); spin_unlock(accept_lock); return connfd; } return 0; } else connfd = accept(listen_fd, &addr, &addrlen); return connfd; } static void worker_accept_cycle(void *args __UNUSED) { for (;;) { /*...*/ int connfd = worker_accept(); if (likely(connfd > 0)) { // 如果有收到請求 在分配一個 coroutine 取處理請求 if (dispatch_coro(handle_connection, (void *) (intptr_t) connfd)) { WARN("system busy to handle request."); close(connfd); continue; } increase_conn(); } else if (connfd == 0) { // 如果沒有就換別的 coroutine schedule_timeout(200); continue; } } } ``` 接下來來談 `worker_accept` 是如何運作 ```c static int worker_accept() { struct sockaddr addr; socklen_t addrlen; int connfd; if (likely(g_worker_processes > 1)) { if (worker_can_accept() && spin_trylock(accept_lock)) { connfd = accept(listen_fd, &addr, &addrlen); spin_unlock(accept_lock); return connfd; } return 0; } else connfd = accept(listen_fd, &addr, &addrlen); return connfd; } ``` 在同一時間上有多個 worker 在不同 cpu上面執行 `worker_accept` ,所以這邊我們需要對 這個 socket (listen_fd)的監聽做同步處理,自我們從`tcp_srv_init` 中使用 `shm_alloc` 配置一個全局鎖,為了確保對 socket (listen_fd) 的監聽操作是同步的。 而在這邊開始說明 `accept` : cserv 使用 [System-call-hooking](https://hackmd.io/@sysprog/linux2024-ktcp/%2F%40sysprog%2Flinux2024-ktcp-d#System-call-hooking) 的技巧,將 accpet 的系統操作替換成我們自己定義的 `accept`,而將本的系統呼叫重新命名為 `real_sys_##name` ,例如 `accpet` 的系統操作被重新命名為 `real_sys_accept`, `accept` 的程式碼實現 : 迴圈內 會不斷監聽 sockfd,直到有事件發生,為了確保效能,在迴圈內如果 errno是 `EINTR`則就技術監聽,但如果不是就將監聽的工作交給 `schedule_cycle` 中的 `event_cycle`去做監聽(`sched.policy`),並將自己(`worker_accept_cycle`) coroutine 放入 inative list ,並呼叫`coroutine_switch` 換下一個 coroutine。 `event_cycle` 如果有監聽到事件之後,會透過 `event_conn_callback` 函式將`current_coro`從 inactive list 拉回 active list。 ```c int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) { int connfd = 0; while ((connfd = real_sys_accept(sockfd, addr, addrlen)) < 0) { if (EINTR == errno) continue; if (!fd_not_ready()) return -1; if (add_fd_event(sockfd, EVENT_READABLE, event_conn_callback, current_coro())) return -2; schedule_timeout(ACCEPT_TIMEOUT); del_fd_event(sockfd, EVENT_READABLE); if (is_wakeup_by_timeout()) { errno = ETIME; return -3; } } if (set_nonblock(connfd)) { close(connfd); return -4; } if (enable_tcp_no_delay(connfd)) { close(connfd); return -5; } if (set_keep_alive(connfd, KEEP_ALIVE)) { close(connfd); return -6; } return connfd; } ``` [Coroutine](https://hackmd.io/@sysprog/linux2024-ktcp/%2F%40sysprog%2Flinux2024-ktcp-d#Coroutine) ### send_signal ```c static void send_signal(int signo) { int pid = read_pidfile(); kill(pid, signo); } ``` 會先從 `conf/cserv.pid` 讀出 master worker ,先來看看 `read_pidfile()` 如何實做。 ```c int read_pidfile() { char buff[32]; FILE *fp = fopen(MASTER_PID_FILE, "r"); if (!fp) { printf("Failed to open %s: %s\n", MASTER_PID_FILE, strerror(errno)); exit(0); } if (!fgets(buff, sizeof(buff) - 1, fp)) { fclose(fp); exit(0); } fclose(fp); return atoi(buff); } ``` 在 Makefile 之中有一段 ```Makefile CFLAGS += -D MASTER_PID_FILE="\"conf/cserv.pid\"" ``` 這個作法是在預處理階段中定義一個 macro :`MASTER_PID_FILE` ,他的值是 : conf/cserv.pid。 所以在編譯器編譯的時候會將所有用到 `MASTER_PID_FILE` 地方都替換成 `conf/cserv.pid`。 ```c FILE *fp = fopen("conf/cserv.pid", "r"); ``` 而 `kill(pid, signo)` 是發送 signal 給指定 pid process 。 在 cserv 中就利用 sigaction 來處理幾種不同的 signal 並且對部分 signal 作了特殊的定義,如下方程式碼中,將 SIGHUP 當作重新設定的訊號 ```c //@ /src/process.h enum { SHUTDOWN_SIGNAL = SIGQUIT, TERMINATE_SIGNAL = SIGINT, RECONFIGURE_SIGNAL = SIGHUP }; ``` [Signal](https://hackmd.io/@sysprog/linux2024-ktcp/%2F%40sysprog%2Flinux2024-ktcp-d#Signal)