---
# System prepended metadata

title: cserv 高效網頁伺服器
tags: [linux2024]

---

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

