Lưu ý
Mục đích của file này là để độc giả tiếng Việt có thể đọc và hiểu tài liệu nhân kernel dễ dàng hơn, không phải để tạo ra một nhánh tài liệu riêng. Nếu bạn có bất kỳ nhận xét hoặc cập nhật nào cho file này, vui lòng thử cập nhật file tiếng Anh gốc trước. Nếu bạn thấy có sự khác biệt giữa bản dịch và bản gốc, hoặc có vấn đề về bản dịch, vui lòng gửi góp ý hoặc patch cho người dịch của file này, hoặc nhờ người bảo trì và người review tài liệu tiếng Việt giúp đỡ.
- Bản gốc:
- Người dịch:
Google Translate (machine translation)
- Phiên bản gốc:
8541d8f725c6
Cảnh báo
Tài liệu này được dịch tự động bằng máy và chưa được review bởi người dịch. Nội dung có thể không chính xác hoặc khó hiểu ở một số chỗ. Khi có sự khác biệt với bản gốc, bản gốc luôn là chuẩn. Bản dịch chất lượng cao (được review) được đặt trong thư mục vi_VN/.
RCU và các mô-đun không thể tải được¶
[Được xuất bản lần đầu trên LWN ngày 14 tháng 1 năm 2007: ZZ0000ZZ
Trình cập nhật RCU đôi khi sử dụng call_rcu() để bắt đầu chờ đợi không đồng bộ
một thời gian ân hạn sẽ trôi qua. Nguyên thủy này lấy một con trỏ tới rcu_head
struct được đặt trong cấu trúc dữ liệu được bảo vệ RCU và một con trỏ khác
tới một chức năng có thể được gọi sau này để giải phóng cấu trúc đó. Mã tới
khi đó việc xóa phần tử p khỏi danh sách liên kết khỏi ngữ cảnh IRQ có thể là
như sau:
- list_del_rcu(p);
call_rcu(&p->rcu, p_callback);
Vì call_rcu() không bao giờ chặn nên mã này có thể được sử dụng một cách an toàn từ bên trong
Bối cảnh IRQ. Hàm p_callback() có thể được định nghĩa như sau:
- static void p_callback(struct rcu_head *rp)
- {
struct pstruct*p = container_of(rp,struct pstruct, rcu);
- kfree(p);
}
Đang tải các mô-đun sử dụng call_rcu()¶
Nhưng điều gì sẽ xảy ra nếu hàm p_callback() được xác định trong một mô-đun không thể tải được?
Nếu chúng tôi dỡ mô-đun trong khi một số lệnh gọi lại RCU đang chờ xử lý, các CPU thực hiện các cuộc gọi lại này sẽ bị ảnh hưởng nghiêm trọng thất vọng khi sau này chúng được viện dẫn, như được mô tả một cách huyền ảo tại ZZ0000ZZ
Chúng ta có thể thử đặt một sync_rcu() trong đường dẫn mã thoát mô-đun,
nhưng điều này là không đủ. Mặc dù sync_rcu() có chờ một
thời gian gia hạn đã trôi qua, nó không đợi lệnh gọi lại hoàn tất.
Người ta có thể muốn thử vài lần sync_rcu() liên tiếp
cuộc gọi, nhưng điều này vẫn không được đảm bảo để hoạt động. Nếu có rất
tải gọi lại RCU nặng, thì một số lệnh gọi lại có thể bị trì hoãn trong
để cho phép quá trình xử lý khác tiếp tục. Chỉ có một ví dụ, chẳng hạn
trì hoãn là cần thiết trong hạt nhân thời gian thực để tránh quá nhiều
lập kế hoạch độ trễ.
rcu_barrier()¶
Tình huống này có thể được xử lý bằng hàm nguyên thủy rcu_barrier(). đúng hơn
thay vì chờ thời gian gia hạn trôi qua, rcu_barrier() sẽ đợi tất cả
các cuộc gọi lại RCU chưa hoàn thành. Xin lưu ý rằng rcu_barrier()
ZZ0000ZZ có ngụ ý đồng bộ hóa_rcu() không, đặc biệt, nếu không có RCU
các cuộc gọi lại được xếp hàng đợi ở bất cứ đâu, rcu_barrier() có quyền trả lại
ngay lập tức, không cần chờ đợi điều gì chứ đừng nói đến thời gian ân hạn.
Mã giả sử dụng rcu_barrier() như sau:
Ngăn không cho đăng bất kỳ lệnh gọi lại RCU mới nào. 2. Thực thi
rcu_barrier(). 3. Cho phép dỡ mô-đun xuống.
Ngoài ra còn có hàm srcu_barrier() cho SRCU, và tất nhiên là bạn
phải phù hợp với hương vị của srcu_barrier() với call_srcu().
Nếu mô-đun của bạn sử dụng nhiều cấu trúc srcu_struct thì nó cũng phải
sử dụng nhiều lệnh gọi srcu_barrier() khi dỡ bỏ mô-đun đó.
Ví dụ: nếu nó sử dụng call_rcu(), call_srcu() trên srcu_struct_1 và
call_srcu() trên srcu_struct_2, sau đó là ba dòng mã sau
sẽ được yêu cầu khi dỡ hàng:
- 1 rcu_barrier();
2 srcu_barrier(&srcu_struct_1); 3 srcu_barrier(&srcu_struct_2);
Nếu độ trễ là điều cốt yếu thì hàng đợi công việc có thể được sử dụng để chạy những đồng thời ba chức năng.
Một phiên bản cổ của mô-đun rcutorture sử dụng rcu_barrier()
trong chức năng thoát của nó như sau
- 1 khoảng trống tĩnh
2 rcu_torture_cleanup(void) 3 { 4 int tôi; 5 6 dấu chấm = 1; 7 if (shuffler_task != NULL) { 8 VERBOSE_PRINTK_STRING(“Dừng tác vụ rcu_torture_shuffle”); 9 kthread_stop(shuffler_task);
10 } 11 nhiệm vụ xáo trộn = NULL; 12 13 nếu (writer_task != NULL) { 14 VERBOSE_PRINTK_STRING(“Dừng tác vụ rcu_torture_writer”); 15 kthread_stop(writer_task); 16 } 17 writer_task = NULL; 18 19 if (reader_tasks != NULL) { 20 cho (i = 0; i < nrealreaders; i++) { 21 if (reader_tasks[i] != NULL) { 22 VERBOSE_PRINTK_STRING( 23 “Dừng tác vụ rcu_torture_reader”); 24 kthread_stop(reader_tasks[i]); 25 } 26 reader_tasks[i] = NULL; 27 } 28 kfree(reader_tasks); 29 reader_tasks = NULL; 30 } 31 rcu_torture_current = NULL; 32 33 if (fakewriter_tasks != NULL) { 34 cho (i = 0; i < nfakewriters; i++) { 35 if (fakewriter_tasks[i] != NULL) { 36 VERBOSE_PRINTK_STRING( 37 “Dừng tác vụ rcu_torture_fakewriter”); 38 kthread_stop(fakewriter_tasks[i]); 39 } 40 fakewriter_tasks[i] = NULL; 41 } 42 kfree(fakewriter_tasks); 43 fakewriter_tasks = NULL; 44 } 45 46 nếu (stats_task != NULL) { 47 VERBOSE_PRINTK_STRING(“Dừng nhiệm vụ rcu_torture_stats”); 48 kthread_stop(stats_task); 49 } 50 số liệu thống kê_task = NULL; 51 52 /* Đợi tất cả lệnh gọi lại RCU kích hoạt. / 53
rcu_barrier(); 54 55rcu_torture_stats_print(); / -Sau- luồng thống kê bị dừng! */ 56 57 if (cur_ops->dọn dẹp != NULL) 58 cur_ops->dọn dẹp(); 59 if (atomic_read(&n_rcu_torture_error)) 60 rcu_torture_print_module_parms(“Kết thúc kiểm tra: FAILURE”); 61 cái khác 62 rcu_torture_print_module_parms(“Kết thúc kiểm tra: SUCCESS”); 63 }
Dòng 6 đặt một biến toàn cục để ngăn chặn bất kỳ cuộc gọi lại RCU nào từ
tự đăng lại. Điều này sẽ không cần thiết trong hầu hết các trường hợp, vì
Lệnh gọi lại RCU hiếm khi bao gồm lệnh gọi tới call_rcu(). Tuy nhiên, cơ cấu
mô-đun là một ngoại lệ đối với quy tắc này và do đó cần phải đặt quy tắc này
biến toàn cục.
Các dòng 7-50 dừng tất cả các tác vụ kernel liên quan đến rcutorture
mô-đun. Do đó, khi thực thi đến dòng 53, sẽ không còn lệnh xử lý nữa
Các cuộc gọi lại RCU sẽ được đăng. Cuộc gọi rcu_barrier() trên dòng 53 đang chờ
để hoàn tất mọi lệnh gọi lại có sẵn.
Sau đó, dòng 55-62 in trạng thái và thực hiện dọn dẹp theo hoạt động cụ thể, đồng thời sau đó quay lại, cho phép hoàn thành thao tác dỡ mô-đun.
- Câu đố nhanh #1:
Có tình huống nào khác mà
rcu_barrier()có thể được yêu cầu?
ZZ0000ZZ
Mô-đun của bạn có thể có thêm sự phức tạp. Ví dụ, nếu bạn
mô-đun gọi call_rcu() từ bộ tính giờ, trước tiên bạn cần phải kiềm chế
từ việc đăng bộ hẹn giờ mới, hãy hủy (hoặc chờ) tất cả những bộ tính giờ đã được đăng
bộ hẹn giờ và chỉ sau đó gọi rcu_barrier() để đợi mọi thứ còn lại
Cuộc gọi lại RCU đã hoàn tất.
Tất nhiên, nếu mô-đun của bạn sử dụng call_rcu(), bạn sẽ cần gọi
rcu_barrier() trước khi dỡ tải. Tương tự, nếu mô-đun của bạn sử dụng
call_srcu(), bạn sẽ cần gọi srcu_barrier() trước khi dỡ tải,
và trên cùng cấu trúc srcu_struct. Nếu mô-đun của bạn sử dụng call_rcu()
ZZ0000ZZ call_srcu(), thì (như đã lưu ý ở trên) bạn sẽ cần gọi
rcu_barrier() ZZ0001ZZ srcu_barrier().
Triển khai rcu_barrier()¶
Việc triển khai rcu_barrier() của Dipankar Sarma tận dụng thực tế
các cuộc gọi lại RCU không bao giờ được sắp xếp lại sau khi được xếp hàng đợi trên một trong các CPU
hàng đợi. Việc triển khai của anh ấy xếp hàng một lệnh gọi lại RCU trên mỗi CPU
hàng đợi gọi lại và sau đó đợi cho đến khi tất cả chúng bắt đầu thực thi, tại
tại thời điểm đó, tất cả các cuộc gọi lại RCU trước đó được đảm bảo đã hoàn thành.
Mã ban đầu cho rcu_barrier() đại khái như sau:
- 1 khoảng trống rcu_barrier(void)
2 { 3 BUG_ON(
in_interrupt()); 4 /* Sử dụng cpucontrol mutex để bảo vệ khỏi hotplug CPU */ 5 mutex_lock(&rcu_barrier_mutex); 6 init_completion(&rcu_barrier_completion); 7 Atomic_set(&rcu_barrier_cpu_count, 1); 8 on_each_cpu(rcu_barrier_func, NULL, 0, 1); 9 nếu (atomic_dec_and_test(&rcu_barrier_cpu_count))10 hoàn thành(&rcu_barrier_completion); 11 wait_for_completion(&rcu_barrier_completion); 12 mutex_unlock(&rcu_barrier_mutex); 13 }
Dòng 3 xác minh rằng người gọi đang ở trong ngữ cảnh quá trình và dòng 5 và 12
sử dụng rcu_barrier_mutex để đảm bảo rằng chỉ có một rcu_barrier() đang sử dụng
hoàn thành toàn cục và bộ đếm tại một thời điểm, được khởi tạo trên các dòng
6 và 7. Dòng 8 khiến mỗi CPU gọi rcu_barrier_func(), nghĩa là
hiển thị dưới đây. Lưu ý rằng số “1” cuối cùng trong danh sách đối số của on_each_cpu()
đảm bảo rằng tất cả các lệnh gọi tới rcu_barrier_func() sẽ hoàn thành
trước khi on_each_cpu() trả về. Dòng 9 xóa số đếm ban đầu khỏi
rcu_barrier_cpu_count và nếu số lượng này bây giờ bằng 0, dòng 10 sẽ hoàn tất
việc hoàn thành, ngăn dòng 11 bị chặn. Dù thế nào đi nữa,
dòng 11 sau đó đợi (nếu cần) để hoàn thành.
- Câu đố nhanh #2:
Tại sao dòng 8 không khởi tạo rcu_barrier_cpu_count về 0, do đó tránh được sự cần thiết của dòng 9 và 10?
ZZ0000ZZ
Mã này đã được viết lại vào năm 2008 và nhiều lần sau đó, nhưng mã này vẫn đưa ra ý tưởng chung.
rcu_barrier_func() chạy trên mỗi CPU, nơi nó gọi call_rcu()
để đăng lệnh gọi lại RCU, như sau:
- 1 khoảng trống tĩnh rcu_barrier_func(void *không được sử dụng)
10 }
Dòng 3 và 4 định vị cấu trúc per-CPU rcu_data bên trong của RCU,
chứa cấu trúc rcu_head cần thiết cho cuộc gọi sau tới
call_rcu(). Dòng 7 chọn một con trỏ tới cấu trúc rcu_head này và dòng
8 tăng bộ đếm toàn cầu. Bộ đếm này sau đó sẽ được giảm đi
bởi cuộc gọi lại. Dòng 9 sau đó đăng ký rcu_barrier_callback() trên
hàng đợi của CPU hiện tại.
Hàm rcu_barrier_callback() chỉ đơn giản là giảm giá trị nguyên tử
biến rcu_barrier_cpu_count và hoàn tất quá trình hoàn thành khi nó
đạt tới 0, như sau:
- 1 khoảng trống tĩnh rcu_barrier_callback(struct rcu_head *notused)
2 { 3 nếu (atomic_dec_and_test(&rcu_barrier_cpu_count)) 4 hoàn thành(&rcu_barrier_completion); 5 }
- Câu đố nhanh #3:
Điều gì xảy ra nếu
rcu_barrier_func()của CPU 0 thực thi ngay lập tức (do đó tăng rcu_barrier_cpu_count lên giá trị một), nhưng các lời gọircu_barrier_func()khác của CPU có bị trì hoãn trong thời gian gia hạn đầy đủ không? Điều này không thể dẫn đếnrcu_barrier()quay lại sớm?
ZZ0000ZZ
Việc triển khai rcu_barrier() hiện tại phức tạp hơn do nhu cầu
để tránh làm phiền các CPU nhàn rỗi (đặc biệt là trên các hệ thống chạy bằng pin)
và nhu cầu làm phiền tối thiểu các CPU không hoạt động trong các hệ thống thời gian thực.
Ngoài ra, rất nhiều tối ưu hóa đã được áp dụng. Tuy nhiên,
đoạn mã trên minh họa các khái niệm.
rcu_barrier() Tóm tắt¶
Hàm nguyên thủy rcu_barrier() được sử dụng tương đối ít, vì hầu hết
mã sử dụng RCU nằm trong kernel lõi chứ không phải trong các mô-đun. Tuy nhiên, nếu
bạn đang sử dụng RCU từ mô-đun không thể tải được, bạn cần sử dụng rcu_barrier()
để mô-đun của bạn có thể được dỡ xuống một cách an toàn.
Đáp án các câu đố nhanh¶
- Câu đố nhanh #1:
Có tình huống nào khác mà
rcu_barrier()có thể được yêu cầu?- Trả lời:
Điều thú vị là
rcu_barrier()ban đầu không phải được thực hiện để dỡ tải mô-đun. Nikita Danilov đã sử dụng RCU trong một hệ thống tập tin, dẫn đến tình huống tương tự tại thời gian ngắt kết nối hệ thống tập tin. Dipankar Sarma đã mã hóarcu_barrier()để đáp lại, để Nikita có thể gọi nó trong suốt quá trình quá trình ngắt kết nối hệ thống tập tin.- Mãi về sau, máy của bạn mới thực sự gặp phải sự cố dỡ mô-đun RCU khi
triển khai rcutorture và thấy rằng
rcu_barrier()giải quyết được vấn đề này nữa.
ZZ0000ZZ
- Câu đố nhanh #2:
Tại sao dòng 8 không khởi tạo rcu_barrier_cpu_count về 0, do đó tránh được sự cần thiết của dòng 9 và 10?
- Trả lời:
Giả sử hàm
on_each_cpu()hiển thị ở dòng 8 là bị trì hoãn, do đórcu_barrier_func()của CPU 0 được thực thi và thời gian gia hạn tương ứng đã trôi qua, tất cả đều trước CPU 1rcu_barrier_func()đã bắt đầu thực thi. Điều này sẽ dẫn đến rcu_barrier_cpu_count bị giảm xuống 0, vì vậy dòng đóWait_for_completion()của số 11 sẽ quay trở lại ngay lập tức, không thành công đợi lệnh gọi lại của CPU 1 được gọi.- Lưu ý rằng đây không phải là vấn đề khi mã rcu_barrier()
được thêm vào lần đầu tiên vào năm 2005. Điều này là do
on_each_cpu()vô hiệu hóa quyền ưu tiên, hoạt động như một thông báo quan trọng phía đọc RCU phần này, do đó ngăn không cho thời gian gia hạn của CPU 0 hoàn thành cho đến khion_each_cpu()xử lý xong tất cả CPU.- Tuy nhiên, với việc hợp nhất hương vị RCU vào khoảng v4.20, điều này
khả năng một lần nữa bị loại trừ, bởi vì hợp nhất RCU một lần nữa chờ đợi các vùng mã không thể chiếm được.
- Tuy nhiên, số lượng bổ sung đó vẫn có thể là một ý tưởng hay.
Dựa vào những loại tai nạn thực hiện này có thể dẫn đến về các lỗi bất ngờ sau này khi việc triển khai thay đổi.
ZZ0000ZZ
- Câu đố nhanh #3:
Điều gì xảy ra nếu
rcu_barrier_func()của CPU 0 thực thi ngay lập tức (do đó tăng rcu_barrier_cpu_count lên giá trị một), nhưng các lời gọircu_barrier_func()khác của CPU có bị trì hoãn trong thời gian gia hạn đầy đủ không? Điều này không thể dẫn đếnrcu_barrier()quay lại sớm?- Trả lời:
Điều này không thể xảy ra. Lý do là
on_each_cpu()có cái cuối cùng đối số, cờ chờ, được đặt thành “1”. Cờ này được chuyển qua tớismp_call_function()và xa hơn nữa làsmp_call_function_on_cpu(), làm cho cái sau này quay cho đến khi lệnh gọi CPU chéo củarcu_barrier_func()đã hoàn thành. Điều này tự nó sẽ ngăn cản thời gian gia hạn kể từ khi hoàn thành trên các hạt nhân không phải CONFIG_PREEMPTION, vì mỗi CPU phải trải qua quá trình chuyển đổi ngữ cảnh (hoặc chuyển đổi ngữ cảnh khác state) trước khi thời gian gia hạn có thể hoàn tất. Tuy nhiên, đây là không được sử dụng trong hạt nhân CONFIG_PREEMPTION.- Do đó, on_each_cpu() vô hiệu hóa quyền ưu tiên trong cuộc gọi của nó
tới
smp_call_function()và cả cuộc gọi cục bộ tớircu_barrier_func(). Bởi vì việc triển khai RCU gần đây xử lý các vùng mã bị vô hiệu hóa quyền ưu tiên dưới dạng quan trọng phía đọc RCU các phần, điều này ngăn không cho thời gian gia hạn hoàn thành. Cái này có nghĩa là tất cả các CPU đã thực thircu_barrier_func()trước đó lần lượtrcu_barrier_callback()đầu tiên có thể thực thi ngăn rcu_barrier_cpu_count sớm về 0.- Nhưng nếu on_each_cpu() quyết định từ bỏ việc vô hiệu hóa quyền ưu tiên,
điều này cũng có thể xảy ra do cân nhắc về độ trễ theo thời gian thực, khởi tạo rcu_barrier_cpu_count thành một sẽ tiết kiệm được thời gian.
ZZ0000ZZ