# 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)