Vấn đề đồng bộ hóa nguyên mẫu (An Archetypal Synchronization Problem )
Giả thiết về sau điều đó bạn tăng dần biến này khi bạn nhận được một yêu cầu và sự giảm bớt nó khi bạn sau đó hoàn thành yêu cầu: NTSTATUS DispatchPnp(PDEVICE_OBJECT fdo, PIRP Irp) { ++lActiveRequests; ... // process PNP ...
Giả thiết về sau điều đó bạn tăng dần biến này khi bạn nhận được một yêu cầu và sự giảm bớt nó khi bạn sau đó hoàn thành yêu cầu:
NTSTATUS DispatchPnp(PDEVICE_OBJECT fdo, PIRP Irp)
{
++lActiveRequests;
... // process PNP request
--lActiveRequests;
}
Tôi chắc chắn là bạn đoán nhận đã một máy đếm đó như nó phải không phải là một biến tĩnh: nó cần phải là một bộ phận của thiết bị mở rộng của các bạn vì thế mỗi đối tượng thiết bị đó có máy đếm duy nhất của nó. Rẽ sang với tôi, và giả vờ rằng trình điều khiển của các bạn luôn luôn chỉ quản lý một thiết bị đơn. Để làm ví dụ đầy đủ ý nghĩa hơn, giả thiết cuối cùng một chức năng đó trong trình điều khiển của các bạn sẽ được gọi là khi nào thời gian của nó để xóa đối tượng thiết bị của các bạn. Bạn có lẽ đã muốn trì hoãn thao tác cho đến khi không có nhiều yêu cầu là nổi bật hơn, vì thế bạn có lẽ đã chèn một sự thử của máy đếm:
NTSTATUS HandleRemoveDevice(PDEVICE_OBJECT fdo, PIRP Irp)
{
if (lActiveRequests)
<wait for all requests to complete>
IoDeleteDevice(fdo);
}
Ví dụ này mô tả một vấn đề thực sự, nhân tiện, chúng tôi sẽ dùng để thảo luận vấn đề này ở chương 6 trong những yêu cầu của chúng ta của những yêu cầu Plug and Play (PnP). Quản lý vào/ra có thể cố gắng để loại bỏ một trong số những thiết bị của chúng ta tại một thời điểm khi những yêu cầu được kích hoạt, và chúng tôi cần đề phòng điều đó bằng việc giữ loại nào đó của máy đếm. Tôi sẽ trình bày với bạn ở chương 6 sử dụng IoAcquireRemoveLock như thế và một số chức năng liên quan để giải quyết vấn đề.
Một chỗ ẩn núp vấn đề đồng bộ hóa kinh khủng trong những đoạn mã ti chỉ cho bạn thấy, nhưng nó trở nên hiển nhiên chỉ khi bạn nhìn đằng sau sự tăng dần và giảm những thao tác bên trong DispatchPnp. Trên một bộ xử lý x86, người biên soạn có lẽ đã thực hiện họ sử dụng những chỉ dẫn này:
; ++lActiveRequests;
mov eax, lActiveRequests
add eax, 1
mov lActiveRequests, eax
; --lActiveRequests;
mov eax, lActiveRequests
sub eax, 1
mov lActiveRequests, eax
Để làm rõ vấn đề đồng bộ hóa, giả sử cho rằng đầu tiên có một trực trặc gì đó trong một CPU đơn. Hình dung hai luồng đó là cả hai đang cố gắng thử để tiến tới xuyên qua DispatchPnp tại cùng một thời gian. Chúng tôi biết họ không phải là cả hai thực hiện đúng đồng thời bởi vì chúng tôi chỉ có một CPU đơn cho họ tới chia sẻ. Nhưng hình dung một trong những luồng đó thực hiện gần kết thúc chức năng và quản lý để tải nội dung hiện thời của lActiveRequests vào trong thanh ghi EAX chỉ trước khi luồng khác chặn trước nó. Giả thiết lActiveRequests cân bằng với 2 tại lúc đó. Như sự chuyển đổi bộ phận của luồng, hệ điều hành lưa giữ thanh ghi EAX (chứa đựng giá trị 2) như phần của luồng đang rời khỏi ảnh văn cảnh ở đâu đó trong bộ nhớ.
Chú ý
Bây giờ hình dung rằng luồng khác quản lý để đưa mã tăng lên qua tại sự bắt đầu của DispatchPnp. Nó sẽ tăng dần lActiveRequests từ 2 đến 3 (bởi vì luồng đầu tiên chưa bao giờ được tới cập nhật biến). Nếu luồng đầu tiên chặn trước luồng khác này, hệ điều hành sẽ khôi phục đầu tiên ngữ cảnh của luồng, mà bao gồm giá trị 2 trong thanh ghi EAX. Luồng đầu tiên bây giờ thu được trừ 1 từ EAX và cất giữ kết quả sau ở lActiveRequests. Tại điểm này, lActiveRequests chứa đựng giá trị 1, mà sai. ở đâu đó xuống là con đường, chúng tôi có lẽ đã hấp tấp xóa đối tượng thiết bị của chúng ta vì chúng tôi có rãnh ghi bị mất thực sự của của một yêu cầu vào/ra.
Việc giải quyết vấn đề đặc biệt này thì dễ dàng trên một máy tính x86 — chúng tôi chỉ thay thế nhập/ thêm / cất giữ và nhập/ trừ/ cất giữ những dãy lệnh với những chỉ dẫn nguyên tử:
; ++lActiveRequests;
inc lActiveRequests
; --lActiveRequests;
dec lActiveRequests
Trên một Intel x86, những chỉ dẫn INC và DEC không thể được ngắt, ở đó như vậy sẽ chưa bao giờ là một trường hợp trong đó một luồng có thể được chặn trước trong điểm giữa của việc cập nhật máy đếm. Trong khi nó đứng, mặc dù, mã này còn là không chắc chắn trong một môi trường bộ đa xử lý bởi vì INC và DEC được thực hiện trong vài bước vi mã. Khả dĩ của nó cho 2 CPU khác nhau để đang thực hiện vi mã của họ chỉ yếu ớt ra khỏi bước sao cho một trong số họ kết thúc lên trên cập nhật một giá trị cũ. Vấn đề Multi-CPU có thể cũng được tránh trong kiến trúc x86 bằng cách sử dụng một tiền tố LOCK:
; ++lActiveRequests;
lock inc lActiveRequests
; --lActiveRequests;
lock dec lActiveRequests
Chỉ dẫn LOCK thêm vào đầu những sự khóa ngoài tất cả CPU khác trong khi vi mã cho chỉ dẫn hiện thời thực hiện, do đó bảo đảm sự toàn vẹn dữ liệu.
Một cách đáng tiếc, không phải tất cả vấn đề đồng bộ hóa có một giải pháp dễ dàng như vậy. Điểm của ví dụ này là không có cách giải quyết như thế nào giải quyết một vấn đề đơn giản trên một trong những nền tảng nơi Windows XP chạy nhưng phần nào để minh họa hai nguồn của khó khăn: quyền ưu tiên mua của một luồng bởi kẻ khác trong điểm giữa của một sự thay đổi trạng thái và sự thực hiện đồng thời của những thao tác state-change tương phản. Chúng tôi có thể tránh khó khăn bằng việc thận trọng sử dụng những nguyên thủy đồng bộ hóa, những đối tượng loại trừ như lẫn nhau như vậy, để ngăn chặn những luồng khác trong khi dữ liệu dùng chung những truy nhập luồng của chúng ta. Đôi khi khi sự khóa luồng là không cho phép, chúng tôi có thể tránh quyền ưu tiên bằng cách sử dụng sơ đồ quyền ưu tiên IRQL, và chúng tôi có thể ngăn ngừa sự thực hiện đồng thời bằng việc thận trọng sử dụng những sự khóa xuay vòng.