Ngắt cuộc gọi hệ thống khi bắt được tín hiệu


29

Từ việc đọc các trang man trên read()write()gọi, có vẻ như các cuộc gọi này bị gián đoạn bởi các tín hiệu bất kể chúng có phải chặn hay không.

Đặc biệt, giả sử

  • một quá trình thiết lập một xử lý cho một số tín hiệu.
  • một thiết bị được mở (giả sử, một thiết bị đầu cuối) O_NONBLOCK không được đặt (tức là hoạt động ở chế độ chặn)
  • quá trình sau đó thực hiện một read()cuộc gọi hệ thống để đọc từ thiết bị và kết quả là thực thi một đường dẫn điều khiển kernel trong không gian kernel.
  • trong khi điều kiện tiên quyết đang thực hiện read()trong không gian kernel, tín hiệu mà trình xử lý được cài đặt trước đó được gửi đến quá trình đó và trình xử lý tín hiệu của nó được gọi.

Đọc các trang hướng dẫn và các phần thích hợp trong SUSv3 volume Âm lượng giao diện hệ thống (XSH) ' , người ta thấy rằng:

tôi. Nếu a read()bị gián đoạn bởi tín hiệu trước khi nó đọc bất kỳ dữ liệu nào (nghĩa là nó phải chặn vì không có dữ liệu nào), nó sẽ trả về -1 khi errnođược đặt thành [EINTR].

ii. Nếu a read()bị gián đoạn bởi tín hiệu sau khi nó đã đọc thành công một số dữ liệu (nghĩa là có thể bắt đầu phục vụ yêu cầu ngay lập tức), nó sẽ trả về số byte đã đọc.

Câu hỏi A): Tôi có đúng không khi cho rằng trong cả hai trường hợp (khối / không chặn) việc phân phối và xử lý tín hiệu không hoàn toàn trong suốt đối với read()?

Trường hợp i. có vẻ dễ hiểu vì việc chặn read()thông thường sẽ đặt tiến trình ở TASK_INTERRUPTIBLEtrạng thái để khi tín hiệu được phát, hạt nhân đặt tiến trình vào TASK_RUNNINGtrạng thái.

Tuy nhiên, khi read()không cần chặn (trường hợp ii.) Và đang xử lý yêu cầu trong không gian hạt nhân, tôi đã nghĩ rằng sự xuất hiện của tín hiệu và việc xử lý nó sẽ minh bạch giống như việc đến và xử lý đúng cách một CTNH gián đoạn sẽ là. Cụ thể, tôi đã giả định rằng khi phát tín hiệu, quá trình sẽ tạm thời được đưa vào chế độ người dùng để thực thi trình xử lý tín hiệu của nó, từ đó cuối cùng nó sẽ quay trở lại để xử lý gián đoạn read()(trong không gian kernel) để read()chạy nó Tất nhiên để hoàn thành sau đó quá trình quay trở lại điểm ngay sau khi gọi đến read()(trong không gian người dùng), với tất cả các byte có sẵn được đọc như là kết quả.

Nhưng ii. dường như ngụ ý rằng read()nó bị gián đoạn, vì dữ liệu có sẵn ngay lập tức, nhưng nó chỉ trả về một số dữ liệu (thay vì tất cả).

Điều này đưa tôi đến câu hỏi thứ hai (và cuối cùng):

Câu hỏi B): Nếu giả định của tôi theo A) là chính xác, tại sao việc này read()bị gián đoạn, mặc dù không cần phải chặn vì có sẵn dữ liệu để đáp ứng yêu cầu ngay lập tức? Nói cách khác, tại sao việc read()không được nối lại sau khi thực hiện trình xử lý tín hiệu, cuối cùng dẫn đến tất cả các dữ liệu có sẵn (có sẵn sau khi tất cả) được trả về?

Câu trả lời:


29

Tóm tắt: bạn chính xác rằng việc nhận tín hiệu không minh bạch, trong trường hợp i (bị gián đoạn mà không đọc được gì) cũng như trong trường hợp ii (bị gián đoạn sau khi đọc một phần). Để làm khác trong trường hợp tôi sẽ yêu cầu thực hiện các thay đổi cơ bản cả về kiến ​​trúc của hệ điều hành và kiến ​​trúc của các ứng dụng.

Giao diện triển khai hệ điều hành

Xem xét những gì xảy ra nếu một cuộc gọi hệ thống bị gián đoạn bởi một tín hiệu. Trình xử lý tín hiệu sẽ thực thi mã chế độ người dùng. Nhưng trình xử lý s tòa nhà là mã hạt nhân và không tin tưởng bất kỳ mã chế độ người dùng nào. Vì vậy, hãy khám phá các lựa chọn cho trình xử lý tòa nhà:

  • Chấm dứt cuộc gọi hệ thống; báo cáo bao nhiêu đã được thực hiện cho mã người dùng. Theo mã, tùy thuộc vào mã ứng dụng để khởi động lại cuộc gọi hệ thống theo một cách nào đó, nếu muốn. Đó là cách unix hoạt động.
  • Lưu trạng thái của cuộc gọi hệ thống và cho phép mã người dùng tiếp tục cuộc gọi. Đây là vấn đề vì một số lý do:
    • Trong khi mã người dùng đang chạy, một cái gì đó có thể xảy ra để vô hiệu hóa trạng thái đã lưu. Ví dụ: nếu đọc từ một tệp, tệp có thể bị cắt ngắn. Vì vậy, mã hạt nhân sẽ cần rất nhiều logic để xử lý các trường hợp này.
    • Trạng thái đã lưu không được phép giữ bất kỳ khóa nào, bởi vì không có gì đảm bảo rằng mã người dùng sẽ tiếp tục tòa nhà, và sau đó khóa sẽ được giữ mãi mãi.
    • Nhân phải phơi bày các giao diện mới để tiếp tục hoặc hủy bỏ các tòa nhà đang diễn ra, ngoài giao diện bình thường để bắt đầu một tòa nhà. Đây là rất nhiều biến chứng cho một trường hợp hiếm gặp.
    • Trạng thái đã lưu sẽ cần sử dụng tài nguyên (ít nhất là bộ nhớ); những tài nguyên đó sẽ cần được phân bổ và nắm giữ bởi hạt nhân nhưng được tính vào sự phân bổ của quy trình. Điều này không phải là không thể vượt qua, nhưng nó là một biến chứng.
      • Lưu ý rằng bộ xử lý tín hiệu có thể khiến các cuộc gọi hệ thống bị gián đoạn; vì vậy bạn không thể có một sự phân bổ tài nguyên tĩnh bao gồm tất cả các tòa nhà có thể.
      • Và nếu tài nguyên không thể được phân bổ thì sao? Sau đó, tòa nhà sẽ phải thất bại. Điều đó có nghĩa là ứng dụng sẽ cần phải có mã để xử lý trường hợp này, vì vậy thiết kế này sẽ không đơn giản hóa mã ứng dụng.
  • Vẫn trong tiến trình (nhưng bị đình chỉ), tạo một luồng mới cho trình xử lý tín hiệu. Điều này, một lần nữa, là có vấn đề:
    • Việc triển khai unix sớm có một luồng duy nhất cho mỗi quy trình.
    • Người xử lý tín hiệu sẽ có nguy cơ vượt qua đôi giày của tòa nhà. Dù sao đây cũng là một vấn đề, nhưng trong thiết kế unix hiện tại, nó có chứa.
    • Tài nguyên sẽ cần phải được phân bổ cho chủ đề mới; xem ở trên.

Sự khác biệt chính với một ngắt là mã ngắt được tin cậy và bị ràng buộc cao. Nó thường không được phép phân bổ tài nguyên, hoặc chạy mãi mãi, hoặc khóa và không giải phóng chúng, hoặc làm bất kỳ loại điều khó chịu nào khác; do trình xử lý ngắt được viết bởi chính người triển khai HĐH, anh ta biết rằng nó sẽ không làm điều gì xấu. Mặt khác, mã ứng dụng có thể làm bất cứ điều gì.

Giao diện thiết kế ứng dụng

Khi một ứng dụng bị gián đoạn giữa cuộc gọi hệ thống, tòa nhà có nên tiếp tục hoàn thành? Không phải lúc nào. Ví dụ, hãy xem xét một chương trình như một cái vỏ đọc một dòng từ thiết bị đầu cuối và người dùng nhấn Ctrl+C, kích hoạt SIGINT. Việc đọc không được hoàn thành, đó là những gì tín hiệu là tất cả về. Lưu ý rằng ví dụ này cho thấy rằng tòa nhà readphải bị gián đoạn ngay cả khi chưa có byte nào được đọc.

Vì vậy, phải có một cách để ứng dụng báo cho kernel hủy cuộc gọi hệ thống. Theo thiết kế unix, điều đó sẽ tự động xảy ra: tín hiệu làm cho tòa nhà trở lại. Các thiết kế khác sẽ yêu cầu một cách để ứng dụng tiếp tục hoặc hủy bỏ tòa nhà chọc trời.

Cuộc readgọi hệ thống là như vậy bởi vì nó là nguyên thủy có ý nghĩa, được thiết kế chung của hệ điều hành. Điều đó có nghĩa là, đại khái là, đọc càng nhiều càng tốt, đến giới hạn (kích thước bộ đệm), nhưng dừng lại nếu có điều gì khác xảy ra. Để thực sự đọc một bộ đệm đầy đủ liên quan đến việc chạy readtrong một vòng lặp cho đến khi đọc được càng nhiều byte càng tốt; đây là một chức năng cấp cao hơn, fread(3). Không giống như read(2)một cuộc gọi hệ thống, freadlà một chức năng thư viện, được thực hiện trong không gian người dùng trên đầu trang read. Nó phù hợp cho một ứng dụng đọc tệp hoặc chết khi thử; nó không phù hợp với trình thông dịch dòng lệnh hoặc cho chương trình nối mạng phải điều tiết các kết nối sạch sẽ, cũng như chương trình nối mạng có kết nối đồng thời và không sử dụng các luồng.

Ví dụ về đọc trong một vòng lặp được cung cấp trong Lập trình hệ thống Linux của Robert Love:

ssize_t ret;
while (len != 0 && (ret = read (fd, buf, len)) != 0) {
  if (ret == -1) {
    if (errno == EINTR)
      continue;
    perror ("read");
    break;
  }
  len -= ret;
  buf += ret;
}

Nó chăm sóc case icase iivà nhiều hơn nữa.


Cảm ơn Gilles rất nhiều vì một câu trả lời rất súc tích và rõ ràng, điều đó chứng thực cho những quan điểm tương tự được đưa ra trong một bài viết về triết lý thiết kế UNIX. Có vẻ rất thuyết phục tôi rằng hành vi gián đoạn tòa nhà có liên quan đến triết lý thiết kế UNIX chứ không phải là những hạn chế hoặc trở ngại kỹ thuật
darbehdar

@darbehdar Đó là cả ba: triết lý thiết kế unix (ở đây chủ yếu là các quy trình ít tin cậy hơn kernel và có thể chạy mã tùy ý, cũng như các quy trình và luồng không được tạo ra), các ràng buộc kỹ thuật (về phân bổ tài nguyên) và thiết kế ứng dụng (ở đó là những trường hợp khi tín hiệu phải hủy bỏ tòa nhà).
Gilles 'SO- đừng trở nên xấu xa'

2

Để trả lời câu hỏi A :

Có, việc phân phối và xử lý tín hiệu không hoàn toàn minh bạch đối với read().

Các read()nửa đường chạy chưa chiếm một số tài nguyên trong khi nó đang bị gián đoạn bởi các tín hiệu. Và bộ xử lý tín hiệu của tín hiệu cũng có thể gọi một tòa nhà khác read()(hoặc bất kỳ tòa nhà an toàn tín hiệu không đồng bộ nào khác ). Vì vậy, read()trước tiên tín hiệu bị gián đoạn phải được dừng lại để giải phóng các tài nguyên mà nó sử dụng, nếu không, lệnh read()được gọi từ bộ xử lý tín hiệu sẽ truy cập vào cùng các tài nguyên đó và gây ra sự cố reentrant.

Bởi vì các cuộc gọi hệ thống khác ngoài read()có thể được gọi từ bộ xử lý tín hiệu và chúng cũng có thể chiếm bộ tài nguyên giống hệt như read()vậy. Để tránh các vấn đề tái phát ở trên, thiết kế đơn giản, an toàn nhất là dừng ngắt quãng read()mỗi khi tín hiệu xảy ra trong quá trình chạy.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.