Implementasi Epoll, bagian 2

Saat menerbitkan terjemahan artikel pertama dari seri implementasi epoll, kami melakukan survei tentang kemungkinan melanjutkan penerjemahan siklus. Lebih dari 90% peserta survei mendukung penerjemahan artikel lainnya. Oleh karena itu, hari ini kami menerbitkan terjemahan materi kedua dari siklus ini.







Fungsi Ep_insert ()



Fungsi ep_insert()merupakan salah satu fungsi terpenting dalam sebuah implementasi epoll. Memahami cara kerjanya sangat penting untuk memahami bagaimana tepatnya ia epollmendapatkan informasi tentang peristiwa baru dari file yang ditontonnya.



Deklarasi tersebut ep_insert()dapat ditemukan pada baris 1267 file fs/eventpoll.c. Mari kita lihat beberapa cuplikan kode untuk fungsi ini:



user_watches = atomic_long_read(&ep->user->epoll_watches);
if (unlikely(user_watches >= max_user_watches))
  return -ENOSPC;


Dalam cuplikan kode ini, fungsi tersebut ep_insert()pertama-tama memeriksa untuk melihat apakah jumlah total file yang ditonton pengguna saat ini tidak lebih dari nilai yang ditentukan di /proc/sys/fs/epoll/max_user_watches. Jika user_watches >= max_user_watches, maka fungsi tersebut segera berakhir dengan errnoset ke ENOSPC.



Kemudian ep_insert()mengalokasikan memori menggunakan mekanisme manajemen memori slab kernel Linux:



if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
  return -ENOMEM;


Jika fungsi tersebut dapat mengalokasikan cukup memori struct epitem, proses inisialisasi berikut akan dilakukan:



/*  ... */
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;


Setelah itu, ia ep_insert()akan mencoba mendaftarkan callback di deskriptor file. Tetapi sebelum kita dapat membicarakannya, kita perlu mengenal beberapa struktur data penting.



Kerangka kerja poll_tableadalah entitas penting yang digunakan oleh implementasi poll()VFS. (Saya mengerti bahwa ini bisa membingungkan, tetapi di sini saya ingin menjelaskan bahwa fungsi yang poll()saya sebutkan di sini adalah implementasi dari operasi file poll(), bukan panggilan sistem poll()). Dia diumumkan di include/linux/poll.h:



typedef struct poll_table_struct {
  poll_queue_proc _qproc;
  unsigned long _key;
} poll_table;


Suatu entitas poll_queue_procmerepresentasikan jenis fungsi panggilan balik yang terlihat seperti ini:



typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);


Anggota _keytabel poll_tablesebenarnya tidak seperti yang terlihat pertama kali. Yakni, meskipun namanya menunjukkan "kunci" tertentu, pada _keykenyataannya, topeng peristiwa yang menarik bagi kita disimpan. Dalam implementasinya, ini epoll _keydiatur ke ~0(melengkapi 0). Ini berarti ia epollberusaha menerima informasi tentang peristiwa apa pun. Ini masuk akal, karena aplikasi ruang pengguna dapat mengubah topeng kejadian kapan saja menggunakan epoll_ctl(), mengambil semua kejadian dari VFS dan kemudian memfilternya dalam implementasi epoll, yang membuat segalanya lebih mudah.



Untuk memudahkan pemulihan poll_queue_procstruktur aslinya epitem, maka epolldigunakan struktur sederhana yang disebutep_pqueueyang berfungsi sebagai pembungkus poll_tabledengan penunjuk ke struktur yang sesuai epitem(file fs/eventpoll.c, baris 243):



/* -,    */
struct ep_pqueue {
  poll_table pt;
  struct epitem *epi;
};


Kemudian itu ep_insert()menginisialisasi struct ep_pqueue. Kode berikut pertama kali menulis ke anggota epistruktur sebuah ep_pqueuepointer ke struktur yang epitemsesuai dengan file yang kita coba tambahkan, dan kemudian menulis ep_ptable_queue_proc()ke anggota _qprocstruktur ep_pqueuedan _keymenulis padanya ~0.



/*      */
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);


Ini kemudian ep_insert()akan memanggil ep_item_poll(epi, &epq.pt);, yang akan menghasilkan panggilan ke implementasi yang poll()terkait dengan file tersebut.



Mari kita lihat contoh yang menggunakan implementasi poll()stack TCP Linux dan memahami apa sebenarnya yang dilakukan implementasi tersebut poll_table.



Fungsi tcp_poll()adalah implementasi poll()untuk soket TCP. Kodenya dapat ditemukan di file net/ipv4/tcp.c, di baris 436. Berikut potongan kode ini:



unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
{
  unsigned int mask;
  struct sock *sk = sock->sk;
  const struct tcp_sock *tp = tcp_sk(sk);

  sock_rps_record_flow(sk);

  sock_poll_wait(file, sk_sleep(sk), wait);

  //  
}


Fungsi tersebut tcp_poll()memanggil sock_poll_wait(), meneruskan, sebagai argumen kedua, sk_sleep(sk)dan sebagai yang ketiga - wait(ini adalah tcp_poll()tabel yang sebelumnya diteruskan ke fungsi tersebut poll_table).



Apa itu sk_sleep()? Ternyata, ini hanyalah getter untuk mengakses antrian tunggu acara untuk struktur tertentu sock(file include/net/sock.h, baris 1685):



static inline wait_queue_head_t *sk_sleep(struct sock *sk)
{
  BUILD_BUG_ON(offsetof(struct socket_wq, wait) != 0);
  return &rcu_dereference_raw(sk->sk_wq)->wait;
}


Apa yang sock_poll_wait()akan dilakukan dengan antrian tunggu acara? Ternyata fungsi ini akan melakukan beberapa pemeriksaan sederhana dan kemudian memanggil poll_wait()dengan parameter yang sama. Fungsi poll_wait()ini kemudian akan memanggil callback yang kami tentukan dan meneruskannya ke antrian acara menunggu (file include/linux/poll.h, baris 42):



static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
  if (p && p->_qproc && wait_address)
    p->_qproc(filp, wait_address, p);
}


Dalam kasus epollentitas, itu _qprocakan menjadi fungsi yang ep_ptable_queue_proc()dideklarasikan dalam file fs/eventpoll.cdi baris 1091.



/*
*  - ,       
*     ,    .
*/
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
       poll_table *pt)
{
  struct epitem *epi = ep_item_from_epqueue(pt);
  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 {
    /*       */
    epi->nwait = -1;
  }
}


Pertama, ia ep_ptable_queue_proc()mencoba memulihkan struktur epitemyang sesuai dengan file dari antrian tunggu yang kami kerjakan. Karena epollmenggunakan struktur pembungkus ep_pqueue, memulihkan epitemdari penunjuk poll_tableadalah operasi penunjuk yang sederhana.



Setelah itu, ia ep_ptable_queue_proc()hanya mengalokasikan memori sebanyak yang dibutuhkan struct eppoll_entry. Struktur ini bertindak sebagai "perekat" antara antrian tunggu untuk file yang sedang diawasi dan struktur yang sesuai epitemuntuk file itu. Sangat epollpenting untuk mengetahui di mana kepala antrian tunggu untuk file yang sedang diawasi. Jika tidak, epollitu tidak akan dapat membatalkan pendaftaran antrian tunggu nanti. Struktureppoll_entryjuga menyertakan antrian wait ( pwq->wait) dengan fungsi melanjutkan proses yang disediakan ep_poll_callback(). Mungkin pwq->waitini adalah bagian terpenting dalam keseluruhan implementasi epoll, karena entitas ini digunakan untuk menyelesaikan tugas-tugas berikut:



  1. Pantau peristiwa yang terjadi dengan file tertentu yang sedang dipantau.
  2. Melanjutkan pekerjaan proses lain jika kebutuhan seperti itu muncul.


Kemudian itu akan ep_ptable_queue_proc()melampirkan pwq->waitantrian tunggu dari file target ( whead). Fungsi ini juga akan menambah struct eppoll_entrydaftar tertaut dari struct epitem( epi->pwqlist) dan menambah nilai yang epi->nwaitmewakili panjang daftar epi->pwqlist.



Dan di sini saya punya satu pertanyaan. Mengapa epollmenggunakan daftar tertaut untuk menyimpan struktur eppoll_entrydalam epitemsatu struktur file? Bukankah epitemhanya satu elemen yang eppoll_entrydibutuhkan?



Saya, bagaimanapun, tidak dapat menjawab pertanyaan ini dengan tepat. Sejauh yang saya tahu, kecuali seseorang akan menggunakan contoh epolldalam beberapa putaran gila, daftar epi->pwqlisthanya akan berisi satu elemen struct eppoll_entry, danepi->nwaituntuk sebagian besar file cenderung 1.



Hal yang baik adalah bahwa ambiguitas di sekitar epi->pwqlisttidak mempengaruhi apa pun yang akan saya bicarakan di bawah ini. Yaitu, kita akan berbicara tentang bagaimana Linux memberi tahu epollkejadian yang terjadi pada file yang sedang dipantau.



Ingat apa yang kita bicarakan di bagian sebelumnya? Itu tentang apa yang epollditambahkan wait_queue_tke daftar tunggu dari file target wait_queue_head_t. Meskipun wait_queue_tpaling umum digunakan sebagai mekanisme untuk melanjutkan proses, pada dasarnya ini hanyalah struktur yang menyimpan penunjuk ke fungsi yang akan dipanggil ketika Linux memutuskan untuk melanjutkan proses dari antrian yang wait_queue_tdilampirkan wait_queue_head_t. Dalam fungsi iniepolldapat memutuskan apa yang harus dilakukan dengan sinyal resume, tetapi epolltidak perlu melanjutkan proses apapun! Seperti yang akan Anda lihat nanti, biasanya ep_poll_callback()tidak ada yang terjadi saat Anda menelepon resume.



Saya kira perlu juga dicatat bahwa mekanisme melanjutkan proses yang digunakan poll()sepenuhnya bergantung pada implementasi. Dalam kasus file soket TCP, kepala antrian tunggu adalah anggota yang sk_wqdisimpan dalam struktur sock. Ini juga menjelaskan perlunya menggunakan callback ep_ptable_queue_proc()untuk bekerja dengan antrian tunggu. Karena dalam implementasi antrian untuk file yang berbeda, kepala antrian dapat muncul di tempat yang sangat berbeda, kita tidak memiliki cara untuk menemukan nilai yang kita butuhkan.wait_queue_head_ttanpa menggunakan panggilan balik.



Kapan tepatnya dimulainya kembali pekerjaan sk_wqdalam struktur dilakukan sock? Ternyata, sistem soket Linux mengikuti prinsip desain "OO" yang sama dengan VFS. Struktur sockmendeklarasikan hook berikut pada baris 2312 dari file net/core/sock.c:



void sock_init_data(struct socket *sock, struct sock *sk)
{
  //  ...
  sk->sk_data_ready  =   sock_def_readable;
  sk->sk_write_space =  sock_def_write_space;
  //  ...
}


B sock_def_readable()dan sock_def_write_space()panggilan itu wake_up_interruptible_sync_poll()untuk (struct sock)->sk_wqtujuan fungsi-panggilan balik, pekerjaan proses terbarukan.



Kapan sk->sk_data_ready()dan akan dipanggil sk->sk_write_space()? Itu tergantung implementasinya. Mari kita ambil soket TCP sebagai contoh. Fungsi sk->sk_data_ready()ini akan dipanggil di paruh kedua penangan interupsi saat koneksi TCP menyelesaikan prosedur jabat tangan tiga arah, atau saat buffer diterima untuk soket TCP tertentu. Fungsi sk->sk_write_space()ini akan dipanggil jika status buffer berubah dari fullmenjadi available. Jika Anda tetap mengingat hal ini saat menganalisis topik berikut, terutama topik tentang pemicuan depan, topik tersebut akan terlihat lebih menarik.



Hasil



Ini menyimpulkan artikel kedua dari serangkaian artikel tentang implementasi epoll. Lain kali, epollmari kita bicara tentang apa sebenarnya yang dilakukannya di callback yang terdaftar di antrian resume proses socket.



Sudahkah kamu menggunakan epoll?










All Articles