本文簡要介紹一下BIND9中的UDP數據處理,包括如何創建socket、設置什么socket參數、多線程環境中如何讓多個線程讀取53端口的數據等等。
BIND9的架構采用event-driven和task-based。對於像TCP這樣的事件采用event-driven來等待讀寫時間,而在多線程方面,采用task-based的方式,服務器啟動時由task_manager創建一系列干活的線程task(CPU個數個),然后外部的用戶通過調用isc_task_send函數將事件掛到task_manager的事件隊列中,各線程(task)從這個事件隊列中獲得要干的活,開始干活。
對於UDP而言,BIND9的做法是在主進程中創建好UDP socket,然后各個線程都在這個fd上通過recvmsg來收取消息並處理(由於recvmsg是線程安全的,所以多線程環境中可以這樣用而不用加鎖)。
關鍵流程如下所示:
1, 主進程:ns_interface_listenudp <--ns_interface_setup <-- do_scan <-- ns_interfacemgr_scan0 <-- ns_interfacemgr_scan <-- scan_interfaces <--load_configuration <-- run_server <-- ns_server_create <-- setup <-- main
其中ns_interface_listenudp的作用是在給定的IP地址和端口上監聽UDP事件:
1),調用socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);創建socket. 2),調用 flags = fcntl(fd, F_GETFL, 0); flags |= PORT_NONBLOCK; ret = fcntl(fd, F_SETFL, flags);將socket設置為非阻塞模式 3),調用setsockopt(sock->fd, SOL_SOCKET, SO_REUSEADDR, (void *)&on, sizeof(on))設置套接字選項 4),調用bind(sock->fd, &sockaddr->type.sa, sockaddr->length) 為該socket綁定IP地址和端口
工作線程:
上面的isc_task_send(client->task, &ev)被調用后,主進程就把任務丟給了工作線程去做。
示意圖如下所示:
從上圖中可以看到,當線程通過recvmsg接收到數據后會交給隊列中的線程執行client_request()函數;client_request就是解包處理DNS請求。
如果線程通過recvmsg沒有接收到數據,則將這個fd加入到epoll的監聽隊列中(BIND9有一個單獨的線程執行epoll循環),當這個fd上有讀事件的時候,epoll線程會調用internal_recv()處理UDP數據,internal_recv的核心依然是doio_recv函數。