golang從內核到epoll

引子:在之前的文章裏 golang netpoll的實現與分析 講了一些,對於golang netpoll的實現,但是,數據是怎麼通過硬件到達golang的這塊不是太明確,今天就主要分析下這一塊。

linux的網絡的基本實現

在 TCP/IP ⽹絡分層模型⾥,整個協議棧被分成了物理層、鏈路層、⽹絡層,傳輸層和應⽤層。物理層對應的是⽹卡和⽹線,應⽤層對應的是我們常⻅的 Nginx,FTP 等等各種應⽤。Linux 實現的是鏈路層、⽹絡層和傳輸層這三層。

在 Linux 內核實現中,鏈路層協議靠⽹卡驅動來實現,內核協議棧來實現⽹絡層和傳輸層。內核對更上層的應⽤層提供 socket 接⼝來供⽤戶進程訪問。我們⽤ Linux 的視⻆來看到的 TCP/IP ⽹絡分層模型應該是下⾯這個樣⼦的。

如何網絡事件

當設備上有數據到達的時候,會給 CPU 的相關引腳上觸發⼀個電壓變化,以通知 CPU 來處理數據。

也可以把這個叫 硬中斷

但是我們知道,cpu運行速度很快,但是網絡讀取數據會很慢,這時候就會長期佔用cpu,導致cpu無法處理其他事件,比如,鼠標移動。

那麼在linux中是怎麼解決掉這個問題的呢?

linux內核將中斷處理拆分開,拆分爲了2個部分,一個是上面提到的 硬中斷,另外就是 軟中斷

第一部分接收到cpu電壓變化,產生硬中斷,然後只做最簡單的處理,然後異步的交給硬件去接收信息到緩衝區。這個時候,cpu就已經可以接收其他中斷信息過來了。

第二部分就是軟中斷部分,軟中斷是怎麼做的呢?其實就是對內存的二進制位進行變更,類似於我們平常寫業務常用的到的status字段一樣,比如網絡Io中,當緩衝區接收數據完畢,會將當前狀態改爲完成。舉個例子,epoll讀取某個io時間讀取完數據時,並不會直接進入就緒態,而是等下次循環遍歷判斷狀態,纔會將這個fd塞入就緒列表(當然,這個時間很短,不過相對於cpu來說,這個時間就很長了)。

2.4 以後的內核版本採⽤的下半部實現⽅式是軟中斷,由 ksoftirqd 內核線程全權處理。和硬中斷不同的是,硬中斷是通過給 CPU 物理引腳施加電壓變化,⽽軟中斷是通過給內存中的⼀個變量的⼆進制值以通知軟中斷處理程序。

這也就是爲什麼知道2.6纔有epoll(正式引入)使用的原因,2.4以前內核都不支持這種方式。

總體的數據流轉圖如下:

一個數據從到達網卡,要經歷以下步驟纔會完成一次數據接收:

  • 數據包從外面的網絡進入物理網卡。如果目的地址不是該網卡,且該網卡沒有開啓混雜模式,該包會被網卡丟棄。
  • 網卡將數據包通過DMA的方式寫入到指定的內存地址,該地址由網卡驅動分配並初始化。注: 老的網卡可能不支持DMA,不過新的網卡一般都支持。
  • 網卡通過硬件中斷(IRQ)通知CPU,告訴它有數據來了
  • CPU根據中斷表,調用已經註冊的中斷函數,這個中斷函數會調到驅動程序(NIC Driver)中相應的函數
  • 驅動先禁用網卡的中斷,表示驅動程序已經知道內存中有數據了,告訴網卡下次再收到數據包直接寫內存就可以了,不要再通知CPU了,這樣可以提高效率,避免CPU不停的被中斷。
  • 啓動軟中斷。這步結束後,硬件中斷處理函數就結束返回了。由於硬中斷處理程序執行的過程中不能被中斷,所以如果它執行時間過長,會導致CPU沒法響應其它硬件的中斷,於是內核引入軟中斷,這樣可以將硬中斷處理函數中耗時的部分移到軟中斷處理函數裏面來慢慢處理。
  • 內核中的ksoftirqd進程專門負責軟中斷的處理,當它收到軟中斷後,就會調用相應軟中斷所對應的處理函數,對於上面第6步中是網卡驅動模塊拋出的軟中斷,ksoftirqd會調用網絡模塊的net_rx_action函數
  • net_rx_action調用網卡驅動裏的poll函數來一個一個的處理數據包
  • 在pool函數中,驅動會一個接一個的讀取網卡寫到內存中的數據包,內存中數據包的格式只有驅動知道
  • 驅動程序將內存中的數據包轉換成內核網絡模塊能識別的skb格式,然後調用napi_gro_receive函數
  • napi_gro_receive會處理GRO相關的內容,也就是將可以合併的數據包進行合併,這樣就只需要調用一次協議棧。然後判斷是否開啓了RPS,如果開啓了,將會調用enqueue_to_backlog
  • 在enqueue_to_backlog函數中,會將數據包放入CPU的softnet_data結構體的input_pkt_queue中,然後返回,如果input_pkt_queue滿了的話,該數據包將會被丟棄,queue的大小可以通過net.core.netdev_max_backlog來配置
  • CPU會接着在自己的軟中斷上下文中處理自己input_pkt_queue裏的網絡數據(調用__netif_receive_skb_core)
  • 如果沒開啓RPS,napi_gro_receive會直接調用__netif_receive_skb_core
  • 看是不是有AF_PACKET類型的socket(也就是我們常說的原始套接字),如果有的話,拷貝一份數據給它。tcpdump抓包就是抓的這裏的包。
  • 調用協議棧相應的函數,將數據包交給協議棧處理。
  • 待內存中的所有數據包被處理完成後(即poll函數執行完成),啓用網卡的硬中斷,這樣下次網卡再收到數據的時候就會通知CPU

epoll

poll函數

這裏的poll函數是說註冊的回調函數,在軟中斷中進行處理的。比如epoll程序,會註冊一個“ep_poll_callback”

以go epoll爲例:

go: accept –> pollDesc.Init -> poll_runtime_pollOpen –> runtime.netpollopen(epoll_create) -> epollctl(EPOLL_CTL_ADD)

go: netpollblock(gopark),讓出cpu->調度回來,netpoll(0)將協程寫入就緒態->其他操作......

epoll thread: epoll_create(ep_ptable_queue_proc,註冊軟中斷到ksoftirqd,將方法ep_poll_callback註冊到)->epoll_add->epoll_wait(ep_poll讓出cpu)

core: 網卡接收到數據->dma+硬中斷->軟中斷->系統調度到ksoftirqd,處理ep_poll_callback(這裏要注意,新的連接進入到程序,不是通過callback,而是走accept)->獲取到之前註冊的fd句柄->copy網卡數據到句柄->根據事件類型,對fd進行操作(就緒列表)

部分代碼

go: accept

// accept阻塞,等待系統事件(等待有客戶端進來)
func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
	if err := fd.readLock(); err != nil {
		return -1, nil, "", err
	}
	defer fd.readUnlock()

	if err := fd.pd.prepareRead(fd.isFile); err != nil {
		return -1, nil, "", err
	}
	for {
		s, rsa, errcall, err := accept(fd.Sysfd)
		if err == nil {
			return s, rsa, "", err
		}
		switch err {
		case syscall.EAGAIN:
			if fd.pd.pollable() {
				if err = fd.pd.waitRead(fd.isFile); err == nil {
					continue
				}
			}
		case syscall.ECONNABORTED:
			// This means that a socket on the listen
			// queue was closed before we Accept()ed it;
			// it's a silly error, so try again.
			continue
		}
		return -1, nil, errcall, err
	}
}
//accept創建netpoll
func (fd *netFD) accept() (netfd *netFD, err error) {
	d, rsa, errcall, err := fd.pfd.Accept()
	if err != nil {
		if errcall != "" {
			err = wrapSyscallError(errcall, err)
		}
		return nil, err
	}

	if netfd, err = newFD(d, fd.family, fd.sotype, fd.net); err != nil {
		poll.CloseFunc(d)
		return nil, err
	}
	if err = netfd.init(); err != nil {  //open 創建 ctl_add
		fd.Close()
		return nil, err
	}
	lsa, _ := syscall.Getsockname(netfd.pfd.Sysfd)
	netfd.setAddr(netfd.addrFunc()(lsa), netfd.addrFunc()(rsa))
	return netfd, nil
}

### //syscall包。 最終調用的是linux的accept
func accept(s int, rsa *RawSockaddrAny, addrlen *_Socklen) (fd int, err error) {
	r0, _, e1 := syscall(funcPC(libc_accept_trampoline), uintptr(s), uintptr(unsafe.Pointer(rsa)), uintptr(unsafe.Pointer(addrlen)))
	fd = int(r0)
	if e1 != 0 {
		err = errnoErr(e1)
	}
	return
}

epoll源碼

 static int __init eventpoll_init(void)
{
   mutex_init(&pmutex);
   ep_poll_safewake_init(&psw);
   epi_cache = kmem_cache_create("eventpoll_epi", sizeof(struct epitem), 0, SLAB_HWCACHE_ALIGN|EPI_SLAB_DEBUG|SLAB_PANIC, NULL);
   pwq_cache = kmem_cache_create("eventpoll_pwq", sizeof(struct eppoll_entry), 0, EPI_SLAB_DEBUG|SLAB_PANIC, NULL);
   return 0;
}

基礎數據結構

epoll用kmem_cache_create(slab分配器)分配內存用來存放struct epitem和struct eppoll_entry。 當向系統中添加一個fd時,就創建一個epitem結構體,這是內核管理epoll的基本數據結構:

struct epitem {
	struct rb_node  rbn;        //用於主結構管理的紅黑樹
	struct list_head  rdllink;  //事件就緒隊列
	struct epitem  *next;       //用於主結構體中的鏈表
	struct epoll_filefd  ffd;   //這個結構體對應的被監聽的文件描述符信息
	int  nwait;                 //poll操作中事件的個數
	struct list_head  pwqlist;  //雙向鏈表,保存着被監視文件的等待隊列,功能類似於select/poll中的poll_table
	struct eventpoll  *ep;      //該項屬於哪個主結構體(多個epitm從屬於一個eventpoll)
	struct list_head  fllink;   //雙向鏈表,用來鏈接被監視的文件描述符對應的struct file。因爲file裏有f_ep_link,用來保存所有監視這個文件的epoll節點
	struct epoll_event  event;  //註冊的感興趣的事件,也就是用戶空間的epoll_event
}

而每個epoll fd(epfd)對應的主要數據結構爲:

struct eventpoll {
	spin_lock_t       lock;        //對本數據結構的訪問
	struct mutex      mtx;         //防止使用時被刪除
	wait_queue_head_t     wq;      //sys_epoll_wait() 使用的等待隊列
	wait_queue_head_t   poll_wait;       //file->poll()使用的等待隊列
	struct list_head    rdllist;        //事件滿足條件的鏈表
	struct rb_root      rbr;            //用於管理所有fd的紅黑樹(樹根)
	struct epitem      *ovflist;       //將事件到達的fd進行鏈接起來發送至用戶空間
}

struct eventpoll在epoll_create時創建。

long sys_epoll_create(int size) {

    struct eventpoll *ep;

   // ...

    ep_alloc(&ep); //爲ep分配內存並進行初始化

/* 調用anon_inode_getfd 新建一個file instance,

也就是epoll可以看成一個文件(匿名文件)。

因此我們可以看到epoll_create會返回一個fd。

           epoll所管理的所有的fd都是放在一個大的結構eventpoll(紅黑樹)中,

將主結構體struct eventpoll *ep放入file->private項中進行保存(sys_epoll_ctl會取用)*/

 fd = anon_inode_getfd("[eventpoll]", &eventpoll_fops, ep, O_RDWR | (flags & O_CLOEXEC));
     return fd;
}

其中,ep_alloc(struct eventpoll **pep)爲pep分配內存,並初始化。 其中,上面註冊的操作eventpoll_fops定義如下: static const struct file_operations eventpoll_fops = { .release= ep_eventpoll_release, .poll = ep_eventpoll_poll, }; 這樣說來,內核中維護了一棵紅黑樹,大致的結構如下: clip_image002 接着是epoll_ctl函數(省略了出錯檢查等代碼):

 asmlinkage long sys_epoll_ctl(int epfd,int op,int fd,struct epoll_event __user *event) {
    int error;
    struct file *file,*tfile;
    struct eventpoll *ep;
    struct epoll_event epds;
    error = -FAULT;
    //判斷參數的合法性,將 __user *event 複製給 epds。
    if(ep_op_has_event(op) && copy_from_user(&epds,event,sizeof(struct epoll_event)))
            goto error_return; //省略跳轉到的代碼
    file  = fget (epfd); // epoll fd 對應的文件對象
    tfile = fget(fd);    // fd 對應的文件對象
    //在create時存入進去的(anon_inode_getfd),現在取用。
    ep = file->private->data;
    mutex_lock(&ep->mtx);
    //防止重複添加(在ep的紅黑樹中查找是否已經存在這個fd)
    epi = epi_find(ep,tfile,fd);
    switch(op)
    {
        case EPOLL_CTL_ADD:  //增加監聽一個fd
            if(!epi)
            {
                epds.events |= EPOLLERR | POLLHUP;     //默認包含POLLERR和POLLHUP事件
                error = ep_insert(ep,&epds,tfile,fd);  //在ep的紅黑樹中插入這個fd對應的epitm結構體。
            } else  //重複添加(在ep的紅黑樹中查找已經存在這個fd)。
                error = -EEXIST;
            break;
        ...
    }
    return error;
}

ep_insert的實現如下:

static int ep_insert(struct eventpoll *ep, struct epoll_event *event, struct file *tfile, int fd)
{
   int error ,revents,pwake = 0;
   unsigned long flags ;
   struct epitem *epi;
   /*
      struct ep_queue{
         poll_table pt;
         struct epitem *epi;
      }   */
   struct ep_pqueue epq;
   //分配一個epitem結構體來保存每個加入的fd
   if(!(epi = kmem_cache_alloc(epi_cache,GFP_KERNEL)))
      goto error_return;
   //初始化該結構體
   ep_rb_initnode(&epi->rbn);
   INIT_LIST_HEAD(&epi->rdllink);
   INIT_LIST_HEAD(&epi->fllink);
   INIT_LIST_HEAD(&epi->pwqlist);
   epi->ep = ep;
   ep_set_ffd(&epi->ffd,tfile,fd);
   epi->event = *event;
   epi->nwait = 0;
   epi->next = EP_UNACTIVE_PTR;
   epq.epi = epi;
   //安裝poll回調函數
   init_poll_funcptr(&epq.pt, ep_ptable_queue_proc );
   /* 調用poll函數來獲取當前事件位,其實是利用它來調用註冊函數ep_ptable_queue_proc(poll_wait中調用)。
       如果fd是套接字,f_op爲socket_file_ops,poll函數是
       sock_poll()。如果是TCP套接字的話,進而會調用
       到tcp_poll()函數。此處調用poll函數查看當前
       文件描述符的狀態,存儲在revents中。
       在poll的處理函數(tcp_poll())中,會調用sock_poll_wait(),
       在sock_poll_wait()中會調用到epq.pt.qproc指向的函數,
       也就是ep_ptable_queue_proc()。  */
   revents = tfile->f_op->poll(tfile, &epq.pt);
   spin_lock(&tfile->f_ep_lock);
   list_add_tail(&epi->fllink,&tfile->f_ep_lilnks);
   spin_unlock(&tfile->f_ep_lock);
   ep_rbtree_insert(ep,epi); //將該epi插入到ep的紅黑樹中
   spin_lock_irqsave(&ep->lock,flags);
//  revents & event->events:剛纔fop->poll的返回值中標識的事件有用戶event關心的事件發生。
// !ep_is_linked(&epi->rdllink):epi的ready隊列中有數據。ep_is_linked用於判斷隊列是否爲空。
/*  如果要監視的文件狀態已經就緒並且還沒有加入到就緒隊列中,則將當前的
    epitem加入到就緒隊列中.如果有進程正在等待該文件的狀態就緒,則
    喚醒一個等待的進程。  */
if((revents & event->events) && !ep_is_linked(&epi->rdllink)) {
      list_add_tail(&epi->rdllink,&ep->rdllist); //將當前epi插入到ep->ready隊列中。
/* 如果有進程正在等待文件的狀態就緒,
也就是調用epoll_wait睡眠的進程正在等待,
則喚醒一個等待進程。
waitqueue_active(q) 等待隊列q中有等待的進程返回1,否則返回0。
*/
      if(waitqueue_active(&ep->wq))
         __wake_up_locked(&ep->wq,TAKS_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE);
/*  如果有進程等待eventpoll文件本身(???)的事件就緒,
           則增加臨時變量pwake的值,pwake的值不爲0時,
           在釋放lock後,會喚醒等待進程。 */ 
      if(waitqueue_active(&ep->poll_wait))
         pwake++;
   }
   spin_unlock_irqrestore(&ep->lock,flags);
if(pwake)
      ep_poll_safewake(&psw,&ep->poll_wait);//喚醒等待eventpoll文件狀態就緒的進程
   return 0;
}
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
revents = tfile->f_op->poll(tfile, &epq.pt);

這兩個函數將ep_ptable_queue_proc註冊到epq.pt中的qproc。 typedef struct poll_table_struct { poll_queue_proc qproc; unsigned long key; }poll_table; 執行f_op->poll(tfile, &epq.pt)時,XXX_poll(tfile, &epq.pt)函數會執行poll_wait(),poll_wait()會調用epq.pt.qproc函數,即ep_ptable_queue_proc。 ep_ptable_queue_proc函數如下:

/*  在文件操作中的poll函數中調用,將epoll的回調函數加入到目標文件的喚醒隊列中。
    如果監視的文件是套接字,參數whead則是sock結構的sk_sleep成員的地址。  */
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead, poll_table *pt) {
/* struct ep_queue{
         poll_table pt;
         struct epitem *epi;
      } */
    struct epitem *epi = ep_item_from_epqueue(pt); //pt獲取struct ep_queue的epi字段。
    struct eppoll_entry *pwq;
    if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {
        init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
        pwq->whead = whead;
        pwq->base = epi;
        add_wait_queue(whead, &pwq->wait);
        list_add_tail(&pwq->llink, &epi->pwqlist);
        epi->nwait++;
    } else {
        /* We have to signal that an error occurred */
        /*
         * 如果分配內存失敗,則將nwait置爲-1,表示
         * 發生錯誤,即內存分配失敗,或者已發生錯誤
         */
        epi->nwait = -1;
    }
}

ep_ptable_queue_proc

其中struct eppoll_entry定義如下:

struct eppoll_entry {
   struct list_head llink;
   struct epitem *base;
   wait_queue_t wait;
   wait_queue_head_t *whead;
};
ep_ptable_queue_proc 函數完成 epitem 加入到特定文件的wait隊列任務。
ep_ptable_queue_proc有三個參數:
struct file *file;              該fd對應的文件對象
wait_queue_head_t *whead;      該fd對應的設備等待隊列(同select中的mydev->wait_address)
poll_table *pt;                 f_op->poll(tfile, &epq.pt)中的epq.pt

在ep_ptable_queue_proc函數中,引入了另外一個非常重要的數據結構eppoll_entry。eppoll_entry主要完成epitem和epitem事件發生時的callback(ep_poll_callback)函數之間的關聯。首先將eppoll_entry的whead指向fd的設備等待隊列(同select中的wait_address),然後初始化eppoll_entry的base變量指向epitem,最後通過add_wait_queue將epoll_entry掛載到fd的設備等待隊列上。完成這個動作後,epoll_entry已經被掛載到fd的設備等待隊列。

由於ep_ptable_queue_proc函數設置了等待隊列的ep_poll_callback回調函數。所以在設備硬件數據到來時,硬件中斷處理函數中會喚醒該等待隊列上等待的進程時,會調用喚醒函數ep_poll_callback(參見博文http://www.cnblogs.com/apprentice89/archive/2013/05/09/3068274.html)。

static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key) {
   int pwake = 0;
   unsigned long flags;
   struct epitem *epi = ep_item_from_wait(wait);
   struct eventpoll *ep = epi->ep;
   spin_lock_irqsave(&ep->lock, flags);
   //判斷註冊的感興趣事件
//#define EP_PRIVATE_BITS  (EPOLLONESHOT | EPOLLET)
//有非EPOLLONESHONT或EPOLLET事件
   if (!(epi->event.events & ~EP_PRIVATE_BITS))
      goto out_unlock;
   if (unlikely(ep->ovflist != EP_UNACTIVE_PTR)) {
      if (epi->next == EP_UNACTIVE_PTR) {
         epi->next = ep->ovflist;
         ep->ovflist = epi;
      }
      goto out_unlock;
   }
   if (ep_is_linked(&epi->rdllink))
      goto is_linked;
    //***關鍵***,將該fd加入到epoll監聽的就緒鏈表中
   list_add_tail(&epi->rdllink, &ep->rdllist);
   //喚醒調用epoll_wait()函數時睡眠的進程。用戶層epoll_wait(...) 超時前返回。
if (waitqueue_active(&ep->wq))
      __wake_up_locked(&ep->wq, TASK_UNINTERRUPTIBLE | TASK_INTERRUPTIBLE);
   if (waitqueue_active(&ep->poll_wait))
      pwake++;
   out_unlock: spin_unlock_irqrestore(&ep->lock, flags);
   if (pwake)
      ep_poll_safewake(&psw, &ep->poll_wait);
   return 1;
}

所以ep_poll_callback函數主要的功能是將被監視文件的等待事件就緒時,將文件對應的epitem實例添加到就緒隊列中,當用戶調用epoll_wait()時,內核會將就緒隊列中的事件報告給用戶。

epoll_wait實現如下:

SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events, int, maxevents, int, timeout)  {
   int error;
   struct file *file;
   struct eventpoll *ep;
    /* 檢查maxevents參數。 */
   if (maxevents <= 0 || maxevents > EP_MAX_EVENTS)
      return -EINVAL;
    /* 檢查用戶空間傳入的events指向的內存是否可寫。參見__range_not_ok()。 */
   if (!access_ok(VERIFY_WRITE, events, maxevents * sizeof(struct epoll_event))) {
      error = -EFAULT;
      goto error_return;
   }
    /* 獲取epfd對應的eventpoll文件的file實例,file結構是在epoll_create中創建。 */
   error = -EBADF;
   file = fget(epfd);
   if (!file)
      goto error_return;
    /* 通過檢查epfd對應的文件操作是不是eventpoll_fops 來判斷epfd是否是一個eventpoll文件。如果不是則返回EINVAL錯誤。 */
   error = -EINVAL;
   if (!is_file_epoll(file))
      goto error_fput;
    /* At this point it is safe to assume that the "private_data" contains  */
   ep = file->private_data;
    /* Time to fish for events ... */
   error = ep_poll(ep, events, maxevents, timeout);
    error_fput:
   fput(file);
error_return:
   return error;
}

epoll_wait調用ep_poll,ep_poll實現如下:

 static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events, int maxevents, long timeout) {
    int res, eavail;
   unsigned long flags;
   long jtimeout;
   wait_queue_t wait;
    /* timeout是以毫秒爲單位,這裏是要轉換爲jiffies時間。這裏加上999(即1000-1),是爲了向上取整。 */
   jtimeout = (timeout < 0 || timeout >= EP_MAX_MSTIMEO) ?MAX_SCHEDULE_TIMEOUT : (timeout * HZ + 999) / 1000;
 retry:
   spin_lock_irqsave(&ep->lock, flags);
    res = 0;
   if (list_empty(&ep->rdllist)) {
      /* 沒有事件,所以需要睡眠。當有事件到來時,睡眠會被ep_poll_callback函數喚醒。*/
      init_waitqueue_entry(&wait, current); //將current進程放在wait這個等待隊列中。
      wait.flags |= WQ_FLAG_EXCLUSIVE;
      /* 將當前進程加入到eventpoll的等待隊列中,等待文件狀態就緒或直到超時,或被信號中斷。 */
      __add_wait_queue(&ep->wq, &wait);
       for (;;) {
         /* 執行ep_poll_callback()喚醒時應當需要將當前進程喚醒,所以當前進程狀態應該爲“可喚醒”TASK_INTERRUPTIBLE  */
         set_current_state(TASK_INTERRUPTIBLE);
         /* 如果就緒隊列不爲空,也就是說已經有文件的狀態就緒或者超時,則退出循環。*/
         if (!list_empty(&ep->rdllist) || !jtimeout)
            break;
         /* 如果當前進程接收到信號,則退出循環,返回EINTR錯誤 */
         if (signal_pending(current)) {
            res = -EINTR;
            break;
         }
          spin_unlock_irqrestore(&ep->lock, flags);
         /* 主動讓出處理器,等待ep_poll_callback()將當前進程喚醒或者超時,返回值是剩餘的時間。
從這裏開始當前進程會進入睡眠狀態,直到某些文件的狀態就緒或者超時。
當文件狀態就緒時,eventpoll的回調函數ep_poll_callback()會喚醒在ep->wq指向的等待隊列中的進程。*/
         jtimeout = schedule_timeout(jtimeout);
         spin_lock_irqsave(&ep->lock, flags);
      }
      __remove_wait_queue(&ep->wq, &wait);
       set_current_state(TASK_RUNNING);
   }
    /* ep->ovflist鏈表存儲的向用戶傳遞事件時暫存就緒的文件。
    * 所以不管是就緒隊列ep->rdllist不爲空,或者ep->ovflist不等於
    * EP_UNACTIVE_PTR,都有可能現在已經有文件的狀態就緒。
    * ep->ovflist不等於EP_UNACTIVE_PTR有兩種情況,一種是NULL,此時
    * 可能正在向用戶傳遞事件,不一定就有文件狀態就緒,
    * 一種情況時不爲NULL,此時可以肯定有文件狀態就緒,
    * 參見ep_send_events()。
    */
   eavail = !list_empty(&ep->rdllist) || ep->ovflist != EP_UNACTIVE_PTR;
    spin_unlock_irqrestore(&ep->lock, flags);
    /* Try to transfer events to user space. In case we get 0 events and there's still timeout left over, we go trying again in search of more luck. */
   /* 如果沒有被信號中斷,並且有事件就緒,但是沒有獲取到事件(有可能被其他進程獲取到了),並且沒有超時,則跳轉到retry標籤處,重新等待文件狀態就緒。 */
   if (!res && eavail && !(res = ep_send_events(ep, events, maxevents)) && jtimeout)
      goto retry;
    /* 返回獲取到的事件的個數或者錯誤碼 */
   return res;
}

小知識

混雜模式

混雜模式(英語:promiscuous mode)是電腦網絡中的術語。是指一臺機器的網卡能夠接收所有經過它的數據流,而不論其目的地址是否是它。

混雜模式常用於網絡分析

DMA

DMA,全稱Direct Memory Access,即直接存儲器訪問。

DMA傳輸將數據從一個地址空間複製到另一個地址空間,提供在外設和存儲器之間或者存儲器和存儲器之間的高速數據傳輸。當CPU初始化這個傳輸動作,傳輸動作本身是由DMA控制器來實現和完成的。DMA傳輸方式無需CPU直接控制傳輸,也沒有中斷處理方式那樣保留現場和恢復現場過程,通過硬件爲RAM和IO設備開闢一條直接傳輸數據的通道,使得CPU的效率大大提高。

DMA的主要特徵:

  • 每個通道都直接連接專用的硬件DMA請求,每個通道都同樣支持軟件觸發,這些功能通過軟件來配置。
  • 在同一個DMA模塊上,多個請求間的優先權可以通過軟件編程設置(共有四級:很高、高、中等和低),優先權設置相等時由硬件決定(請求0優先於請求1,依此類推)。
  • 獨立數據源和目標數據區的傳輸寬度(字節、半字、全字),模擬打包和拆包的過程。源和目標地址必須按數據傳輸寬度對齊。
  • 支持循環的緩衝器管理。
  • 每個通道都有3個事件標誌(DMA半傳輸、DMA傳輸完成和DMA傳輸出錯),這3個事件標誌邏輯或成爲一個單獨的中斷請求。
  • 存儲器和存儲器間的傳輸、外設和存儲器、存儲器和外設之間的傳輸。
  • 閃存、SRAM、外設的SRAM、APB1、APB2和AHB外設均可作爲訪問的源和目標。
  • 可編程的數據傳輸數目:最大爲65535(0xFFFF)。

非阻塞socket編程處理EAGAIN錯誤

  在linux進行非阻塞的socket接收數據時經常出現Resource temporarily unavailable,errno代碼爲11(EAGAIN),這是什麼意思?   這表明你在非阻塞模式下調用了阻塞操作,在該操作沒有完成就返回這個錯誤,這個錯誤不會破壞socket的同步,不用管它,下次循環接着recv就可以。對非阻塞socket而言,EAGAIN不是一種錯誤。在VxWorks和Windows上,EAGAIN的名字叫做EWOULDBLOCK。

參考

epoll源碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章