Hệ điều hành (Part 6): Deadlock

Ngày đăng: 2026-04-18

Lời mở đầu

Ở phần trước chúng ta đã tìm hiểu về Đồng bộ hóa tiến trình trong hệ điều hành. Trong đó có 2 giải pháp là SemaphoreMonitor. Dù dùng bất cứ giải pháp nào cũng sẽ có tỉ lệ xẩy ra vấn đề tắc nghẽn hệ thống (deadlock).

Ví dụ về việc hai tiến trình cùng chờ đợi nhau

Hai tiến trình trong ảnh trên đang bị deadlock bởi vì sau lượt chạy đầu thì $P_0$ đang nắm giữ $S$, $P_1$ thì nắm giữ $Q$.

Đến lượt chạy thứ hai thì $P_0$ chờ $Q (wait(Q))$, còn $P_1$ chờ $S (wait(S))$. Trong khi cả hai đang nắm giữ phần mà đối phương đang cần.

Thế là hai tiến trình rơi vào trang thái chờ đợi vô hạn và đây gọi là deadlock.

Đây là ví dụ tiêu biểu và dễ hiểu nhất về deadlock trong OS. Mục tiêu của bài viết này sẽ là giới thiệu các khái niệm liên quan và các giải pháp cho vấn đề này.

Các nguyên nhân dẫn đến Deadlock

Các tiến trình sẽ sử dụng một tài nguyên theo cách sau:

Và tình trạng deadlock sẽ diễn ra giống như trong phần mở đầu của bài viết có nói.

Deadlock sẽ xẩy ra nếu 4 điều kiện sau đồng thời thỏa mãn:

  1. Mutual Exclusion: Chỉ có một tiến trình được sử dụng một loại tài nguyên nhất định trong một lúc nhất định, tài nguyên sẽ không thể chia sẻ.
  2. Hold and Wait: Có một tiến trình đang giữ một tài nguyên và nó đang chờ đợi một tài nguyên nào đó.
  3. No Preemption: Tài nguyên đang được một tiến trình nắm giữ thì sẽ không thể bị hệ thống thu hồi và trưng dụng giữa chừng.
  4. Circular Wait: Xuất hiện chu trình trong đồ thị cấp phát tài nguyên như trên ảnh sau:
Đồ thị cấp phát tài nguyên

Những phương pháp để xử lý khi gặp Deadlock:

  1. Phòng ngừa việc deadlock cho hệ thống.
  2. Cho phép deadlock xẩy ra và ta sẽ bắt đầu chữa bệnh trong hệ thống.
  3. Mặc kệ việc deadlock xẩy ra và giao việc xử lý cho người dùng hệ thống. Đây là cách hiệu quả và đơn giản nhất được áp dụng cho nhiều hệ điều hành, bao gồm cả UNIX.

1. Ngăn ngừa Deadlock:

Ta sẽ bảo đảm hệ thống không đáp ứng đủ 4 điều kiện xẩy ra deadlock nêu ở trên.

  1. Mutual Exclusion: Không yêu cầu cho các tài nguyên dùng chung, cần phải có với các tài nguyên không dùng chung.
  2. Hold and Wait: Phải đảm bảo rằng một tiến trình không được nắm bất cứ tài nguyên nào trước khi nó thực hiện một yêu cầu. Điều này dẫn đến hiệu quả kém và có thể gây ra hiện tượng starvation.
  3. No Preemption: Hệ thống phải có cơ chế chống độc quyền và có thể thu hồi tài nguyên từ bất cứ tiến trình nào.
  4. Circular Wait: Hệ thống phải tìm ra một thứ tự cấp phát tài nguyên mà không xẩy tạo ra một chu trình.

Do đó ta có một số kết luận cơ bản sau:

Banker's Algorithm:

Thuật toán này do Edsger Dijkstra nghĩ ra và lấy ý tưởng từ cách hoạt động của nhà Bank.

Yêu cầu hệ thống phải có "thông tin tiên nghiệm" (biết trước tương lai). Mỗi tiến trình khi bắt đầu chạy phải khai báo số lượng tài nguyên tối đa (Maximum) mà nó sẽ cần. Hệ thống sẽ liên tục kiểm tra trạng thái phân bổ tài nguyên để đảm bảo không bao giờ rơi vào tình trạng "chờ đợi vòng tròn" (mọi người cùng khóa tài nguyên của nhau).

Thuật toán Nhà băng: Được đặt tên dựa trên cách một ngân hàng cấp tín dụng. Ngân hàng (Hệ điều hành) chỉ cho vay tiền (cấp tài nguyên) nếu khách hàng (Tiến trình) chứng minh được họ có thể hoàn thành dự án và trả lại toàn bộ tiền. Nếu việc cấp tài nguyên có nguy cơ làm ngân hàng cạn vốn và khách hàng không thể chạy xong, hệ thống sẽ bắt tiến trình đó phải đợi.

Để thuật toán chạy được, hệ thống quản lý 4 cấu trúc dữ liệu chính (giả sử có n tiến trình và m loại tài nguyên):

Thuật toán An toàn - Safety Algorithm

Làm sao để biết hệ thống có an toàn hay không? Thuật toán sẽ thử tìm một Chuỗi an toàn (Safe Sequence).

  1. Hệ thống dùng một vectơ Work (ban đầu bằng Available) và mảng Finish (ban đầu đều là false - chưa ai chạy xong).
  2. Hệ thống đi tìm một tiến trình i thỏa mãn 2 điều kiện:
    • Chưa chạy xong (Finish[i] = false).
    • Tài nguyên nó cần thêm nhỏ hơn hoặc bằng tài nguyên đang rảnh: Need[i] <= Work.
  3. Nếu tìm thấy, hệ thống cho phép tiến trình đó chạy (giả lập). Chạy xong, nó sẽ trả lại toàn bộ tài nguyên đã giữ.
    • Work = Work + Allocation[i]
    • Finish[i] = true.
  4. Lặp lại bước 2. Nếu cuối cùng tất cả tiến trình đều Finish = true, hệ thống ở Trạng thái an toàn.

Ví dụ:

Tài nguyên tổng cộng: A(10), B(5), C(7).

Tài nguyên rảnh (Available): A(3), B(3), C(2).

Ví dụ về thuật toán Banker

2. Chữa bệnh Deadlock:

Thay vì cẩn thận phòng tránh từ đầu như Thuật toán Nhà băng (Banker's Algorithm), chiến lược này cứ để cho hệ thống chạy tự do, cấp phát tài nguyên thoải mái. Tuy nhiên, định kỳ hệ thống sẽ chạy một thuật toán để "khám sức khỏe" xem: Liệu ngay lúc này có bế tắc nào đang xảy ra không? Và nếu có thì những tiến trình nào đang bị kẹt?

1. Trường hợp mỗi loại tài nguyên chỉ có 1 bản sao

Nếu hệ thống rất đơn giản (mỗi loại tài nguyên như máy in, ổ cứng... chỉ có đúng 1 cái), ta không cần dùng ma trận phức tạp.

2. Trường hợp tài nguyên có nhiều bản sao

Lúc này, đồ thị không còn chính xác nữa, ta phải dùng Thuật toán Phát hiện (Detection Algorithm). Thuật toán này nhìn thoáng qua thì gần như y hệt thuật toán Safety của Banker's Algorithm, nhưng có một điểm khác biệt cốt lõi:

Cách thuật toán chạy:

  1. Khởi tạo Work = Available (Tài nguyên đang rảnh rỗi).
  2. Đánh dấu Finish: Nếu tiến trình nào chưa được cấp cái gì cả (Allocation = 0), coi như nó xong (Finish = true). Còn lại để false.
  3. Tìm một tiến trình chưa xong (Finish = false) và yêu cầu hiện tại của nó hệ thống có thể đáp ứng được ngay (Request <= Work).
  4. Nếu tìm thấy, giả sử cấp cho nó, đợi nó chạy xong rồi thu hồi lại toàn bộ tài nguyên nó đang giữ: Work = Work + Allocation.
  5. Lặp lại cho đến khi không tìm được ai nữa. Nếu cuối cùng vẫn còn tiến trình có Finish = false, thì hệ thống đang bị Deadlock (và các tiến trình false đó chính là thủ phạm).

Ví dụ:

Ví dụ về thuật toán Detection

Kết luận: Chuỗi $$ giúp mọi Finish đều bằng true. Hệ thống không có Deadlock.

Khi nào nên dùng Thuật toán Phát hiện?

Như bạn đã thấy ở phần trước, thuật toán phát hiện Deadlock tốn khá nhiều tài nguyên để tính toán (như duyệt các ma trận). Do đó, Hệ điều hành không thể cứ chạy nó liên tục từng giây được. Việc quyết định khi nào chạy dựa vào:

Các phương án Phục hồi sau Bế tắc (Recovery from Deadlock)

Khi đã bắt được Deadlock, Hệ điều hành phải hành động để phá vỡ vòng lặp chờ đợi này. Có 2 cách tiếp cận chính: Giết tiến trình hoặc Cướp tài nguyên.

A. Đình chỉ/Hủy bỏ Tiến trình (Process Termination)

Đây là cách bạo lực. Chúng ta sẽ "giết" (abort) tiến trình để nó nhả tài nguyên ra. Có 2 lựa chọn:

  1. Giết TẤT CẢ các tiến trình đang bị bế tắc: Rất nhanh, dứt điểm ngay lập tức nhưng cực kỳ lãng phí. Bao nhiêu công sức tính toán của các tiến trình từ lúc bắt đầu xem như đổ sông đổ biển.
  2. Giết TỪNG TIẾN TRÌNH một: Giết 1 cái $\rightarrow$ quét lại xem hết bế tắc chưa $\rightarrow$ nếu chưa thì giết tiếp. Cách này ít lãng phí công sức hơn, nhưng lại tốn thời gian CPU vì phải chạy lại thuật toán dò tìm nhiều lần.

Vậy nếu giết từng cái, chọn ai làm "nạn nhân" trước? Hệ điều hành sẽ xét các tiêu chí:

B. Tước đoạt Tài nguyên (Resource Preemption)

Đây là cách "nhân đạo" hơn. Tha mạng cho tiến trình, chỉ tạm thời lấy lại (cướp/tước đoạt) tài nguyên đang cấp cho nó để đưa cho đứa khác mượn xài đỡ.

Tài liệu tham khảo


← Quay lại trang chủ