Hệ điều hành (Part 5): CPU Synchronization
Ngày đăng: 2026-04-17
Lời mở đầu
Trong hệ điều hành đa chương thì sẽ có rất nhiều Process chạy cùng lúc (Concurrently). Chúng có thể phối hợp và chia sẻ dữ liệu với nhau để thực thi hiệu quả.
Các tiến trình phối hợp Cooperating Process này có thể ảnh hưởng hoặc bị ảnh hưởng bởi các tiến trình khác trong hệ thống. Chúng có thể chia sẻ tài nguyên một cách trực tiếp bằng cách dùng chung một vùng nhớ luận lý bao gồm code và cả data. Hoặc chúng sẽ truyền dữ liệu trung gian thông qua file hoặc các message.
Việc truy cập cùng lúc vào một tài nguyên dùng chung sẽ dẫn đến sự không nhất quán.
Producer-Consumer Problem
Hay còn gọi là bài toán Bounded-Buffer, là bài toán kinh điển trong đồng bộ hóa tiến trình liên quan đến sự không nhất quán nêu trên.
Đơn giản hóa: Một tiến trình sẽ cung cấp thông tin, một tiến trình sẽ tiêu thụ thông tin đó.
Để có thể cho chúng giao tiếp hiệu quả ta sẽ tạo ra một buffer of items đặt trong vùng nhớ dùng chung cho cả 2 tiến trình đó.
- Producer Process có thể làm đầy bộ đệm này bằng cách thêm vào thông tin.
- Consumer Process có thể làm trống bộ đệm này bằng cách lấy ra thông tin.
Ta sẽ đùng một biến số nguyên gọi là counter để theo dõi số lượng item trong buffer.
Cốt lõi của vấn đề:
- Producer không được phép thêm dữ liệu khi bộ đệm đã đầy.
- Consumer không được phép lấy dữ liệu khi bộ đệm đang trống.
- Cả hai không được thao tác (thêm/lấy) trên bộ đệm cùng một lúc. Nếu không, hệ thống sẽ rơi vào trạng thái xung đột luồng (race condition) làm sai lệch dữ liệu.
Ví dụ: Cho counter = 5
Nếu cả hai cùng thao tác cập nhật biến đếm số lượng dữ liệu (Producer gọi counter++ và Consumer gọi counter-- cùng lúc), kết quả của biến đếm có thể bị sai lệch (ra 4 hoặc 6 thay vì 5).
Để giải quyết, chúng ta cần các công cụ đồng bộ hóa như Semaphore hoặc Monitor.
Critical-Section Problem
Một hệ thống sẽ có các Process khác nhau cùng chạy.
Mỗi Process sẽ có các phân đoạn code khác nhau trong nó. Và chúng đều sẽ có một phân đoạn code gọi là Critical Section.
Đây là phân đoạn mà trong đó các Process sẽ chứa các câu lệnh liên quan đến chỉnh sửa, thay đổi các giá trị tài nguyên chung như biến, files,...
Vấn đề cũng giống như bài toán Producer-Consumer nêu trên. Để giải quyết bài toán này thì ta cần phải đảm bảo ba tiêu chí sau:
-
Mutual exclusion (Loại trừ tương hỗ)
-
Ý nghĩa: Khi một tiến trình đang thực thi các câu lệnh bên trong miền găng của nó, thì tuyệt đối không có bất kỳ tiến trình nào khác được phép thực thi trong miền găng của chúng.
-
Giải thích thực tế: Điều kiện này giống như một phòng vệ sinh chỉ có một buồng duy nhất. Khi một người đang ở bên trong và chốt cửa, tất cả những người khác bắt buộc phải đứng đợi ở ngoài cho đến khi người đó dùng xong.
-
-
Progress (Tính tiến triển / Diễn tiến)
- Ý nghĩa: Nếu miền găng đang trống (không có tiến trình nào đang dùng) và có một vài tiến trình đang muốn bước vào, thì chỉ những tiến trình không nằm trong "phần còn lại" (remainder section) mới được tham gia vào việc ra quyết định xem ai sẽ là người tiếp theo được vào. Quyết định này không được phép trì hoãn vô thời hạn.
-
Bounded waiting (Chờ đợi có giới hạn)
-
Ý nghĩa: Phải có một giới hạn cụ thể về số lần các tiến trình khác được phép chen ngang vào miền găng tính từ lúc một tiến trình đã gửi yêu cầu xin vào cho đến khi yêu cầu đó được chấp nhận. Mỗi Process chỉ có số lần hữu hạn đi vào Critical Section.
-
Giải thích thực tế: Điều kiện này nhằm ngăn chặn tình trạng "chết đói tài nguyên" (Starvation). Ví dụ: Nếu tiến trình A đang xếp hàng đợi, thì các tiến trình B, C khác dù có độ ưu tiên cao cũng chỉ được phép vào miền găng một số lần nhất định. Sau số lần đó, bắt buộc phải nhường lượt cho tiến trình A để đảm bảo A không bị bắt đợi mãi mãi.
-
Giải pháp Semaphore
Đây là giải pháp được Edsger Dijkstra nghĩ ra nhằm giải quyết các vấn đề ở phía trên thông qua một số nguyên đơn giản gọi là Semaphore.
Semaphore là một biến số nguyên dùng để quản lý truy cập tài nguyên dùng chung, thông qua 2 thao tác nguyên tử automic operation là wait() và signal()
- wait() còn được ký hiệu là P (bắt nguồn từ proberen trong tiếng Hà Lan có nghĩa là thử). Đây là thao tác bắt tiến trình phải chờ cho đến khi semaphore không âm, sau đó mới trừ nó đi.
- signal() còn được ký hiệu là V (bắt nguồn từ verhogen trong tiếng Hà Lan có nghĩa là tăng). Đây là thao tác làm tăng semaphore lên 1 đơn vị.
Nói dễ hiểu thì khi bạn đi vệ sinh:
- Bạn sẽ chờ cửa mở (gọi hàm wait()).
- Kiểm tra xem cửa có mở hay không? (kiểm tra xem S có lớn hơn 0 hay không?).
- Sau khi cửa mở thì bạn ngừng chờ, đi vào và đóng cửa lại (giảm biến S đi một đơn vị).
- Sau khi bạn đi vệ sinh xong, bạn sẽ phát ra tính hiệu cho người khác chuẩn bị đi vào (gọi hàm signal()) và mở cửa (tăng biến S lên một đơn vị).
Có 2 loại Semaphore:
- Semaphore nhị phân (Binary Semaphore) chỉ có giá trị 0 hoặc 1 và thường được gọi là mutex lock bởi vì nó thực cơ chế loại trừ tương hỗ mutex exclusion.
- Semaphore tăng dần (Counting Semaphore) có giá trị như một số nguyên thông thường. Thường được dùng để điều khiển các loại tài nguyên có nhiều thể hiện trong hệ thống thay vì một như Binary Semaphore.
Áp dụng cho bài toán Producer-Consumer, ta sẽ sử dụng 3 Semaphore khác nhau:
- mutex (Binary Semaphore): Khởi tạo bằng 1, đóng vai trò như một chiếc "chìa khóa" khóa bộ đệm lại, đảm bảo tại một thời điểm chỉ có 1 người được truy cập vào bộ đệm.
- empty (Counting Semaphore): Khởi tạo bằng n, dùng để đếm số lượng chỗ trống trong bộ đệm.
- full (Counting Semaphore): Khởi tạo bằng 0, dùng để đếm số lượng chỗ đã có dữ liệu.
Hoạt động:
- wait(full): Kiểm tra xem có dữ liệu không? Nếu không có, Consumer đứng chờ. Nếu có, trừ số dữ liệu đi 1.
- wait(mutex): Lấy chìa khóa khóa bộ đệm lại.
- Tiến hành lấy dữ liệu ra khỏi bộ đệm.
- signal(mutex): Trả chìa khóa, mở khóa bộ đệm.
- signal(empty): Tăng số chỗ trống lên 1 (đồng thời đánh thức Producer nếu nó đang ngủ chờ chỗ trống).
Và ngược lại bên Consumer cũng hoạt động tương tự.
Nhược điểm của Semaphore truyền thống là có thể gây ra tình trạng busy waiting (chờ bận) làm lãng phí CPU vì trong lúc chờ nó sẽ phải dùng vòng lặp while và thực hiện phép toán điều kiện để kiểm tra liên tục.
Tuy nhiên hệ điều hành có thể khắc phục bằng cách chặn (block) tiến trình này và đưa vào hàng đợi waiting thay vì bắt nó lặp liên tục.
Giải pháp Monitor
Monitor là một cấu trúc đồng bộ hóa cấp cao hơn, kết hợp giữa cơ chế loại trừ tương hỗ (mutual exclusion) và các biến điều kiện (condition variables). Khác với Semaphore buộc lập trình viên phải tự quản lý việc khóa/mở thủ công rất dễ lỗi, Monitor tự động đảm bảo chỉ có duy nhất một tiến trình được hoạt động bên trong nó tại một thời điểm.
1. Tính chất:
-
Sự trừu tượng bậc cao (High-level abstraction): Nếu Semaphore hay Mutex yêu cầu lập trình viên phải tự viết code khóa/mở (lock/unlock) rất cẩn thận, thì Monitor giống như một "chiếc hộp đen" thông minh. Nó giúp việc đồng bộ hóa các tiến trình (process synchronization) trở nên tiện lợi và hiệu quả hơn nhiều.
-
Tự động loại trừ tương hỗ (Mutual exclusion): Monitor tự động đảm bảo rằng tại bất kỳ thời điểm nào, chỉ có duy nhất một tiến trình được phép thực thi các lệnh bên trong nó. Lập trình viên không cần phải tự viết code để khóa cửa nữa.
-
Tính đóng gói: Monitor gom chung tất cả các biến (để lưu trạng thái) và các hàm/thủ tục (để thao tác với các biến đó) vào chung một khối.
2. Cú pháp và điều kiện:
Quy tắc truy cập nghiêm ngặt: Các hàm (procedure) bên trong Monitor chỉ được phép sử dụng các biến được khai báo cục bộ bên trong Monitor đó.
Ngược lại, các biến bên trong Monitor chỉ có thể bị thay đổi bởi các hàm của chính Monitor đó. Các tiến trình bên ngoài không thể "chạm" trực tiếp vào dữ liệu này.
Biến điều kiện (Condition Construct): Đây là công cụ cốt lõi giúp Monitor điều phối các tiến trình. Khi khai báo một biến điều kiện (ví dụ: condition x, y;), ta chỉ có thể dùng 2 lệnh với nó:
-
x.wait(): Khi một tiến trình gọi lệnh này, nó sẽ tự đưa mình vào trạng thái tạm ngưng (suspended/ngủ). Nghĩa là: "Tôi đang ở trong Monitor nhưng chưa đủ điều kiện làm việc, tôi xin phép ra góc ngủ để nhường Monitor cho người khác dùng".
-
x.signal(): Khi một tiến trình gọi lệnh này, nó sẽ đánh thức đúng một tiến trình đang bị ngủ (do gọi x.wait() trước đó) dậy để làm việc tiếp.
3. Góc nhìn trực quan về Monitor:
-
Vòng tròn bao quanh (Khối Monitor): Nó đóng vai trò như một bức tường bảo vệ. Bất kỳ thứ gì nằm bên trong vòng tròn này đều được Monitor kiểm soát nghiêm ngặt.
-
Các thành phần bên trong Monitor:
-
Shared data (Dữ liệu dùng chung): Nằm ở phía trên cùng bên trong Monitor. Đây là các biến, cấu trúc dữ liệu hoặc tài nguyên mà các tiến trình muốn truy cập chung. Các tiến trình bên ngoài tuyệt đối không thể truy cập trực tiếp vào các dữ liệu này.
-
Operations (Các thao tác/Thủ tục): Các khối hình chữ nhật đứng đại diện cho các hàm (functions/methods). Đây là "cửa ngõ" duy nhất để thao tác với shared data. Các tiến trình từ bên ngoài muốn dùng dữ liệu thì bắt buộc phải gọi các hàm này.
-
Initialization code (Mã khởi tạo): Phần dưới cùng chứa các đoạn code dùng để khởi tạo giá trị ban đầu cho các biến shared data khi Monitor vừa được tạo ra.
-
-
Hai loại Hàng đợi (Queues): Đây là phần quan trọng nhất để hiểu cách Monitor đồng bộ hóa các tiến trình:
-
Entry queue (Hàng đợi đầu vào):
-
Vị trí: Nằm bên ngoài, chỉ mũi tên đi vào Monitor.
-
Chức năng: Vì Monitor chỉ cho phép 1 tiến trình chạy bên trong nó cùng lúc, nên nếu có nhiều tiến trình cùng lúc gọi các hàm operations, những tiến trình đến sau sẽ phải đứng xếp hàng chờ ở entry queue. Khi tiến trình đang chạy bên trong kết thúc và rời đi, một tiến trình trong entry queue mới được mở cửa cho vào.
-
-
Queues associated with x, y conditions (Hàng đợi gắn với biến điều kiện x, y):
-
Vị trí: Nằm bên trong Monitor, bên dưới phần shared data.
-
Chức năng: Giả sử một tiến trình đã qua được entry queue, đang chạy ngon lành bên trong Monitor, nhưng đột nhiên phát hiện ra nó chưa thể làm tiếp vì thiếu một điều kiện nào đó (ví dụ: muốn đọc file nhưng file chưa có dữ liệu).
-
Cách xử lý: Nếu nó cứ đứng lỳ ở đó chờ thì các tiến trình khác ở entry queue sẽ bị nghẽn mãi mãi. Do đó, tiến trình này sẽ gọi lệnh wait() để tự chuyển mình vào một trong các hàng đợi điều kiện (x hoặc y), đưa bản thân vào trạng thái ngủ và trả lại quyền truy cập Monitor cho người khác. Khi một tiến trình khác vào Monitor và tạo ra dữ liệu phù hợp, nó sẽ gọi lệnh signal() để đánh thức tiến trình đang ngủ ở hàng đợi x hoặc y kia dậy để chạy tiếp.
-
-
Tóm lại: Bạn có thể tưởng tượng Monitor giống như một trạm ATM có cửa bảo vệ.
-
Shared data là tiền trong máy.
-
Operations là các nút bấm rút/nạp tiền.
-
Entry queue là hàng người đứng ngoài nắng chờ đến lượt.
-
Condition queues là những người đã vào trong phòng ATM nhưng thẻ bị lỗi mạng, phải đứng tạm ra một góc để nhường máy cho người khác rút trước, khi nào tổng đài báo mạng có lại thì mới quay lại máy rút tiếp.
4. Áp dụng vào giải quyết bài toán Producer-Consumer:
-
Khai báo trạng thái:
-
buffer[n]: Kho chứa n sản phẩm.
-
full = 0: Biến đếm số sản phẩm hiện có (ban đầu kho trống = 0).
-
buffer_full và buffer_empty: Hai biến điều kiện để báo hiệu kho đầy/kho rỗng.
-
-
Hàm produce_info() (Dành cho người sản xuất):
-
If (full == n) wait(buffer_empty);: Kiểm tra nếu kho đã đầy tràn (n món), Producer lập tức gọi wait() để đi ngủ, chờ đến khi kho rỗng bớt.
-
produce(); full++;: Nếu kho chưa đầy (hoặc vừa được đánh thức), tiến hành sản xuất 1 món và tăng biến đếm lên.
-
signal(buffer_full);: Sau khi tạo ra 1 món hàng mới, gọi signal() để đánh thức Consumer (nếu Consumer đang ngủ vì kho trống): "Có hàng rồi, dậy mua đi!".
-
-
Hàm consume_info() (Dành cho người tiêu dùng):
-
if (full == 0) wait(buffer_full);: Kiểm tra nếu kho không có món nào (full = 0), Consumer lập tức gọi wait() để đi ngủ, chờ đến khi có hàng.
-
Consume(); full--;: Nếu có hàng, tiến hành lấy 1 món ra xài và giảm biến đếm đi.
-
signal(buffer_empty);: Sau khi lấy bớt 1 món, kho sẽ có chỗ trống. Gọi signal() để đánh thức Producer (nếu Producer đang ngủ vì kho đầy): "Có chỗ trống rồi, dậy sản xuất tiếp đi!".
-
-
Khối code bên dưới cùng:
- Hai khối Producer() và Consumer() bên ngoài chỉ việc chạy vòng lặp vô hạn while(true) và gọi các hàm của Monitor.
- Nhờ cơ chế đóng gói và tự động khóa của Monitor, dữ liệu không bao giờ bị sai lệch dù hai bên có chạy nhanh chậm ra sao.
Tài liệu tham khảo
- Abraham Silberschartz, Peter Baer Galvin, Greg Gagne, Operating System Concepts, 9th Edition.
- Geekforgeeks, Introduction to Process Synchronization. [Trực tuyến]. Có sẵn tại Geekforgeeks (Truy cập 2026).
- Geekforgeeks, Solutions to Process Synchronization Problems. [Trực tuyến]. Có sẵn tại Geekforgeeks (Truy cập 2026).
← Quay lại trang chủ