# server-framewrok
contributed by <`rwe0214`> & <`winonecheng`>
###### tags: `Linux`, `rwe0214`, `NCKU`
## 預期目標
* 參照 [server-framework 2016](https://hackmd.io/s/B1s8hX1yg) 和 [server-framework 2017](https://hackmd.io/s/rkT3Zt5eG),詳閱共筆內容和錄影,嘗試更新內容
* [source code](https://github.com/ktvexe/server-framework)
## 程式碼
#### async.c
* 嘗試使用 atom_operation 取代 mutex_lock 並比較效能
* 在 `struct Async` 結構下新增一個 `int atom_lock` 成員,並將它初始化成 `0`
* 接著把 `async.c` 裡的 mutex_lock 改成 atom_operation 風格,如下程式碼:
``` clike=
- pthread_mutex_lock(&async->lock);
+ while(__sync_fetch_and_add(&(async->atom_lock), 1)!=0){;}
...(critical section)...
- pthread_mutex_unlock(&async->lock);
+ __sync_fetch_and_and(&(async->atom_lock), 0);
```
* 執行結果 (只擷取時間部份)
1. mutex_lock:
```
$ ./test-async
# signal finish at (218.213214) ms
# elapsed time: (225.651327) ms
```
2. atom_operation:
```
$ ./test-async
# signal finish at (90.613301) ms
# elapsed time: (92.631035) ms
```
> 雖然以時間上來看,atom_operation 比 mutex_lock 表現來的優異。但是我並不確定這個結果可以確保在每個情況下都是如此。因為在試著撰寫 atom_lock 的時候有遇到以上的寫法但表現卻遜色於 mutex_lock 的情況@@,所以有點懷疑自己寫的 atom_operation 並不是正確的。[name=Willy Chiu][color=#6495ED]
* 執行結果 ( 更新 )
以 `ab -c 32 -n 100 http://localhost:8080/` 並執行 `./httpd` 來做效能分析
1. mutex_lock:
```
Concurrency Level: 32
Time taken for tests: 16.135 seconds
Complete requests: 100
Failed requests: 0
Total transferred: 9900 bytes
HTML transferred: 1300 bytes
Requests per second: 6.20 [#/sec] (mean)
Time per request: 5163.348 [ms] (mean)
Time per request: 161.355 [ms] (mean, across all concurrent requests)
Transfer rate: 0.60 [Kbytes/sec] received
```
2. atom_operation:
```
Concurrency Level: 32
Time taken for tests: 15.225 seconds
Complete requests: 100
Failed requests: 0
Total transferred: 9900 bytes
HTML transferred: 1300 bytes
Requests per second: 6.57 [#/sec] (mean)
Time per request: 4871.896 [ms] (mean)
Time per request: 152.247 [ms] (mean, across all concurrent requests)
Transfer rate: 0.64 [Kbytes/sec] received
```
> 這個執行結果完全和我預期的不同...[name=Willy Chiu][color=#6495ED]
> 推測因為這裡的 critical section 是一段程式碼而不是單一個 integer 或變數,所以 atomic operation 的程式碼可能會使 thread 陷入 busy waiting 的狀態,因而效能提昇不起來。[name=Willy Chiu][color=#6495ED]
:::danger
表示有 lock contention
參照 [Mergesort 效能分析](https://hackmd.io/s/HyR_SEzkg),使用 mutrace 來找出問題點
:notes: jserv
:::
## 技術原理 - 整合2017及2016作業內容
### Framework 架構介紹
[server-framework](https://github.com/ktvexe/server-framework) : 用 C99 撰寫、具備物件導向程式設計風格的伺服器開發框架,架構包含以下三個部分:
* `async`: a native POSIX thread pool
* 結合 `pipe`(作為喚醒 thread 的訊號) 以及 `mutex`(管理 task queue,避免 work thread 同時對同一 task 進行讀取)
* `reactor`: reactor pattern implementation using callbacks
* 利用 `epoll` 系統呼叫,來實做 [Reactor pattern](https://travisliu.gitbooks.io/learn-eventmachine/content/reactorpattern.html)
* Reactor pattern 處理一個 application 收到從一個或多個 client 同時送 request 的情況
* `protocol-server`: server construction library
* 管理有關 server 運作的所有事情,包括 thread pool, process forking, accepting new connections, setting up the initial protocol for new connections, and user space socket writing buffers
### 運作流程
![](https://i.imgur.com/kn7CY52.jpg)
#### **async**
* 在 async.c 的第一個函式便重新定義了 write(),如下:
* 對應 CS:APP3e 第十章: 10.5 用 RIO 包健壯地讀寫( ==page 626== )。
``` clike=
static inline ssize_t write_wrapper(int fd, const void *buf, size_t count)
{
ssize_t s;
if ((s = write(fd, buf, count)) < count) perror("write");
return s;
}
#undef write
#define write write_wrapper
```
因為舊有的 write() 是相當據風險的,它並不會確保資料的寫入是完整的,換句話說,雖然 write() 有提供一個 argument 是讓使用者指定寫入的大小,但是卻沒有保證一定會寫完指定的大小。
這在網路傳輸裡,如果連線不穩定導致漏送封包資料便不完整而出錯,此處重寫則是確保一定 write() 完整的大小,否則便預先印出錯誤提醒。
而在 CS:APP 一書中有提到如何實做一個充分確保 write 能安全寫完指定大小的方式。
* Async async_p 結構體簡介
``` clike=
struct Async {
/** the task queue - MUST be first in the struct */
pthread_mutex_t lock; /**< a mutex for data integrity */
struct AsyncTask * volatile tasks; /**< active tasks */
struct AsyncTask * volatile pool; /**< a task node pool */
struct AsyncTask ** volatile pos; /**< the position for new tasks */
/** The pipe used for thread wakeup */
struct {
int in; /**< read incoming data (opaque data), used for wakeup */
int out; /**< write opaque data (single byte),
used for wakeup signaling */
} pipe;
int count; /**< the number of initialized threads */
unsigned run : 1; /**< the running flag */
/** the thread pool */
pthread_t threads[];
};
```
當中我覺得很特別的地方是 `*tasks` 和 `*pool`,`*tasks` 是一個待進行的 task-list,而 `*pool` 則是 `*tasks` 中完成的 task-node 所串接起來的 free-task-list,如果接收到新的 task 時,會優先從 `*pool` 中取一個 free-task-node ,去接收新的 task,除非 `*pool` 為空,否則不會另外新增一個 task-node。
如此一來便可以省下創建 node 記憶體的時間,而 `**pos` 則是利用 pointer to pointer 去修改上述的 list 結構,簡而言之就是接收新 task 的 address。
* Reactor 的 Client 工作會將處理 request 的工作丟入Work queue,並且在 pipe 寫入 1 byte 紀錄
```c=
static int async_run(async_p async, void (*task)(void *), void *arg)
{
struct AsyncTask *c; /* the container, storing the task */
if (!async || !task) return -1;
// 利用 mutex 避免 work thread 同時對同一 task 進行讀取
pthread_mutex_lock(&(async->lock));
... /* get a container from the pool of grab a new container */
pthread_mutex_unlock(&async->lock);
/* wake up any sleeping threads
* any activated threads will ask to require the mutex
* as soon as we write.
* we need to unlock before we write, or we will have excess
* context switches.
*/
// 在 pipe 寫入 1 byte,喚醒 thread
write(async->pipe.out, c->task, 1);
return 0;
}
```
* Worker thread 若發現 pipe 有紀錄,則會將該 1 byte 取出並執行 Work queue 中對應的 request
```c=
static void *worker_thread_cycle(void *_async)
{
/* setup signal and thread's local-storage async variable. */
struct Async *async = _async;
char sig_buf;
/* pause for signal for as long as we're active. */
while (async->run && (read(async->pipe.in, &sig_buf, 1) >= 0)) {
perform_tasks(async);
// causes the calling thread to relinquish the CPU
sched_yield();
}
// 執行 Work queue 中剩餘未被執行的 tasks
perform_tasks(async);
return 0;
}
```
> 在 `perform_tasks` 中頻繁的進行 mutex lock 和 unlock,會對效能有所影響,或許可以在這上面進行改善。[name=ChengCheng]
#### **reactor**
- 需先理解 [Reactor Pattern](https://travisliu.gitbooks.io/learn-eventmachine/content/reactorpattern.html) (在此系統中以 `epoll` 實做)
- [Reactor Structure](https://en.wikipedia.org/wiki/Reactor_pattern#Structure) :
- **Resources** : 從系統中任何可以提供輸入或 consume 輸出的資源。如: client 的 request 請求。
- **Synchronous Event Demultiplexer** : 用單一個 event loop 去阻擋所有 resource,並傳送 resource 給 dispatch。如:epoll 將收到的 client request 放到 work queue 等待執行,而不會被 blocking。
- **Dispatcher** : 分派從 demultiplexer 來的 resource 給相關的 request handler。如:將 client 待處理的 request 丟進 work queue ,並喚醒 thread 處理這些 request。函式 async_run( ) 即是呈現此功能。
- **Request Handler** : 定義如何應用 requset handler 和他相關的資源。如:worker thread 去處理 client request
- epoll
```c=
// 建立 epoll 物件並回傳其 fd
int epoll_create(int size)
int epoll_create1(int flags)
// 依照 op 去新增或移除監聽的 fd 或更改 fd 的監聽選項
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
// 等待已註冊之 event 被觸發或 timeout, 並回傳 ready fd 的數量
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout)
```
- 呼叫 epoll_wait 後,會一直 block 住,直到有 event 發生或 timeout 時,回傳 I/O ready 的 fd 數量,並在參數 `events` 所指向的記憶體空間有被觸發的那些 event
- `set_fd_polling()` : 將 `request` 送入 `epoll` 之中
~~~c=
int set_fd_polling(int queue, int fd, int action, long milliseconds)
{
struct epoll_event chevent;
chevent.data.fd = fd;
// 這邊為 epoll 的系統呼叫參數來表達不同的 epoll 狀態
// EPOLLET : 將 epoll 設為 Edge Triggered
chevent.events = EPOLLOUT | EPOLLIN |
EPOLLET | EPOLLERR |
EPOLLRDHUP | EPOLLHUP;
...
// 對 request fd 執行 op 操作
return epoll_ctl(queue, action, fd, &chevent);
}
~~~
- `reator_review()` : 將 epoll 中的工作拿出來執行
~~~c=
int reactor_review(struct Reactor *reactor){
...
// 此 Marco 呼叫 epoll_wait(),並回傳有幾個工作要執行
int active_count = _WAIT_FOR_EVENTS_;
if (active_count < 0) return -1;
if (active_count > 0) {
for (int i = 0; i < active_count; i++) {
if (_EVENTERROR_())
/*errors are hendled as disconnections (on_close)*/
reator_close(reactor, _GETFD_(i));
}else{
/*no error, then it is an active event*/
// 以下的部分是在 ready queue 等待服務的事件抓出來執行
if (_EVEBTREADY_(i) && reactor->on_ready)
reactor->on_ready(reactor, _GETFD_(i));
if (_EVENTDATA_(i) && reactor->on_data)
reactor->on_data(reactor, _GETFD_(i));
}
}
}
return active_count;
}
~~~
> epoll 這部分較難理解,和我一樣對於 I/O event 以及 epoll 完全不了解的新手,可以先看這篇 [Linux IO模式及 select、poll、epoll详解](https://segmentfault.com/a/1190000003063859),能大致理解 epoll 是在做甚麼事情。[name=ChengCheng]
#### **protocol-server**