2012-01-29 9 views
18

Hãy nói rằng tôi đang viết một thư viện nhỏ trong C - một số cấu trúc dữ liệu, nói. Tôi nên làm gì nếu tôi không thể cấp phát bộ nhớ?Thiết kế API C: phải làm gì khi malloc trả về NULL?

Điều này có thể khá quan trọng, ví dụ: Tôi cần một số bộ nhớ để khởi tạo cấu trúc dữ liệu ở nơi đầu tiên, hoặc tôi đang chèn một cặp khóa-giá trị và muốn bọc nó trong một cấu trúc nhỏ. Nó cũng có thể ít quan trọng hơn, ví dụ một cái gì đó giống như một hàm pretty_print xây dựng một biểu diễn chuỗi đẹp của nội dung. Tuy nhiên, nó thường nghiêm trọng hơn so với lỗi trung bình của bạn - có thể không có một điểm nào trong việc tiếp tục. Một tấn mẫu sử dụng của malloc trực tuyến chỉ cần thẳng lên thoát khỏi chương trình nếu nó trả về NULL. Tôi đoán rất nhiều mã khách hàng thực hiện điều đó quá - chỉ cần bật lên một số lỗi, hoặc viết nó vào stderr, và hủy bỏ. (Và rất nhiều mã thực sự có thể không kiểm tra giá trị trả về của malloc ở tất cả.)

Đôi khi nó có ý nghĩa để trả lại NULL, nhưng không phải lúc nào. Mã lỗi (hoặc chỉ một số giá trị boolean success), hoặc là giá trị trả về hoặc tham số hoạt động tốt, nhưng có vẻ như chúng có thể lộn xộn hoặc làm tổn thương khả năng đọc của API (sau đó lại có thể được mong đợi bằng ngôn ngữ như C?). Một tùy chọn khác là có một số loại trạng thái lỗi nội bộ mà người gọi có thể truy vấn sau đó, ví dụ: với chức năng get_error, nhưng sau đó bạn phải cẩn thận về an toàn luồng và có thể dễ dàng bỏ lỡ; mọi người có xu hướng không hài lòng về việc kiểm tra lỗi, và nếu đó là một chức năng riêng biệt, họ có thể không biết về nó, hoặc họ có thể không bận tâm (nhưng sau đó tôi đoán đó là vấn đề của họ).

(Tôi đã đôi khi thấy malloc bọc trong một chức năng mà chỉ cố gắng một lần nữa cho đến khi bộ nhớ có sẵn ...

void *my_malloc(size_t size) 
{ 
    void *result = NULL; 
    while (result == NULL) 
     result = malloc(size); 
    return result; 
} 

Nhưng điều đó dường như loại ngớ ngẩn và có thể nguy hiểm.)

một là gì cách thích hợp để xử lý điều này?

+0

Lối ra duyên dáng sẽ là lựa chọn của tôi. – Till

+3

@Till: Điều đó hoàn toàn không được chấp nhận đối với mã thư viện. –

+0

@R. whoopy, tôi hoàn toàn bỏ qua dòng đầu tiên khi đọc câu hỏi này. Trong trường hợp đó, trả về FALSE/NULL và thêm một hàm trạng thái lỗi để biết chi tiết. – Till

Trả lời

12

Nếu phân bổ không thành công ngăn cản tiến trình chuyển tiếp, giải pháp chấp nhận duy nhất cho mã thư viện là để trả lại bất kỳ phân bổ nào và các thay đổi khác đã được thực hiện trong hoạt động được hoàn thành một phần và trả lại mã lỗi người gọi.Chỉ có ứng dụng gọi điện mới biết được cách nào là đúng. Một số ví dụ:

  1. Trình phát nhạc có thể chỉ hủy hoặc quay lại trạng thái ban đầu/dừng và đợi người dùng nhập lại.
  2. Trình xử lý văn bản có thể cần phải lưu trữ kết xuất khẩn cấp của trạng thái tài liệu hiện tại vào tệp khôi phục rồi hủy bỏ.
  3. Máy chủ cơ sở dữ liệu cao cấp có thể cần từ chối và trả lại toàn bộ giao dịch và báo cáo cho khách hàng.

Nếu bạn làm theo ý tưởng được khuyên nhưng ngược lại thư viện của bạn chỉ nên hủy bỏ người gọi khi lỗi phân bổ, bạn sẽ có nhiều chương trình xác định họ không thể sử dụng thư viện của bạn vì lý do này, người dùng của bạn các chương trình sử dụng thư viện của bạn sẽ là cực kỳ tức giận khi lỗi phân bổ khiến dữ liệu có giá trị của chúng bị vứt bỏ.

Edit: Một phản đối một số các "hủy bỏ" trại sẽ tăng so với câu trả lời của tôi là, trên các hệ thống với overcommit, ngay cả các cuộc gọi đến malloc có vẻ thành công có thể thất bại khi kernel cố gắng để nhanh chóng lưu trữ vật lý cho bộ nhớ ảo được cấp phát. Điều này bỏ qua thực tế rằng bất kỳ ai cần độ tin cậy cao sẽ bị vô hiệu hóa quá mức, cũng như thực tế là (ít nhất là trên hệ thống 32 bit) bị lỗi có nhiều khả năng do cạn kiệt không gian địa chỉ ảo hơn cạn kiệt bộ nhớ vật lý.

4

Chỉ cần trả về lỗi theo bất kỳ cách nào bạn thường làm. Vì chúng ta đang nói về một API, bạn không biết môi trường nào bạn đang được gọi, vì vậy chỉ cần trả về NULL hoặc làm theo bất kỳ thủ tục xử lý lỗi nào khác mà bạn đã sử dụng. Bạn không muốn lặp lại mãi mãi, vì người gọi có thể không thực sự cần bộ nhớ đó và họ chỉ muốn biết rằng bạn không thể xử lý nó, hoặc có lẽ người gọi có giao diện người dùng mà họ có thể gửi lỗi.

Hầu hết các API sẽ có một số loại giá trị trả về cho biết lỗi trên tất cả các hàm, các API khác yêu cầu người gọi gọi hàm "check_error" đặc biệt để xác định xem có lỗi hay không. Bạn cũng có thể muốn một hàm "get_error" trả về một chuỗi lỗi mà người gọi có thể tùy chọn hiển thị cho người dùng hoặc bao gồm trong nhật ký. Nó phải có tính mô tả: "API quá hạn đã gặp lỗi trong hàm bất cứ điều gì: không thể cấp phát bộ nhớ". Hay bất cứ cái gì. Đủ để khi ai đó gặp lỗi, họ biết thành phần nào đã ném nó và khi anh ta gửi email cho bạn bằng thông điệp tường trình, bạn biết chính xác điều gì đã xảy ra. Tất nhiên bạn cũng có thể gặp sự cố, nhưng điều đó ngăn cản người gọi đóng bất kỳ thứ gì khác mà họ có thể đang làm và trông xấu xí, nếu mã của bạn có thói quen chết khi được gọi thay vì trả lại lỗi, mọi người sẽ tìm một thư viện không tích cực cố gắng giết chương trình của họ.

1

Rất khó thiết kế phần mềm để xử lý sạch các vấn đề về bộ nhớ và vẫn tiếp tục. Rất ít ứng dụng thực sự thực hiện một nỗ lực nghiêm túc để thực hiện việc này. Là một tác giả thư viện, điều duy nhất hợp lý cần làm là báo cáo lỗi cho người gọi. (trả lại lỗi; ném ngoại lệ; tùy thuộc vào ngôn ngữ, v.v.)

Bạn chắc chắn không muốn lặp lại và chặn cho một thư viện chung. @R có một điểm tốt, nếu một thất bại xảy ra cố gắng khôi phục trạng thái về tình trạng ban đầu của nó.

Xử lý các vấn đề ngoài bộ nhớ và không gian đĩa có thể yêu cầu sự phối hợp trong tất cả các phần của ứng dụng. Bạn có thể muốn có bộ nhớ dự phòng preallocated. Bạn có thể sẽ lặp lại/thử lại mallocs như bạn đang có ý định nhưng với một số sự chậm trễ ở giữa với một thời gian chờ. Nó thực sự nằm ngoài phạm vi của một thư viện điển hình.

2

Trong ngôn ngữ như Java hoặc C#, câu trả lời rõ ràng thường là "ném ngoại lệ!".

Trong C, một cách tiếp cận chung là xử lý lỗi đồng bộ (ví dụ: với mã kết quả và/hoặc giá trị cờ như "null").

Người ta cũng có thể tạo ra một tín hiệu không đồng bộ (giống như một "ngoại lệ" Java ... hoặc "abort()" nặng tay. Với phương pháp này, bạn cũng có thể cho phép người dùng cài đặt trình xử lý lỗi "tùy chỉnh".

Dưới đây là một ví dụ sử dụng setjmp/longjmp:

Và đây là một số ý tưởng thú vị:

Và đây là một dis tốt cussion trên những ưu/nhược điểm của việc sử dụng callbacks C để xử lý lỗi:

2

Bạn có thể viết phần mềm mà không cần malloc ở nơi đầu tiên - những thứ quan trọng an toàn.

Bạn chỉ cần đảm bảo khi bắt đầu thực thi mà nó phân bổ, xác định những gì cần và đảm bảo rằng các thuật toán sẽ không vượt quá các rào cản đó.

Khó, nhưng không phải là không thể.

3

API tiêu chuẩn BLAS cho các cuộc gọi hàm đại số tuyến tính sử dụng cách tiếp cận hơi khác với các đề xuất "trả lại mã lỗi" được đưa ra tại đây: Nó gọi hàm được ghi lại cụ thể và sau đó trả về.

Bây giờ, thư viện cũng cung cấp chức năng thực hiện chức năng được ghi tài liệu này, in thông báo lỗi hữu ích và (nếu có thể) theo dõi ngăn xếp và sau đó hủy bỏ. Đó là một cách để xử lý mọi thứ, và nó có nghĩa là người dùng bình thường sẽ không chạy vào các vấn đề kỳ quái vì họ quên kiểm tra mã lỗi.

Tuy nhiên, điểm quan trọng của việc này là chức năng được ghi lại cụ thể là người dùng có thể cũng chọn cung cấp chức năng thực hiện chức năng đó. Và việc triển khai đó có thể thực hiện được nhiều thứ - nó có thể thiết lập một mã lỗi toàn cục mà người dùng sau đó kiểm tra, hoặc nó có thể làm một thứ gì đó cố gắng xóa một số bộ nhớ và tiếp tục.

Đó là loại giải pháp nặng, cả cho việc triển khai và người dùng, nhưng trong trường hợp mã lỗi rõ ràng không phù hợp, nó cung cấp nhiều tính linh hoạt.

Chỉnh sửa để thêm một vài chi tiết khác: Chức năng BLAS là xerbla (hoặc cblas_xerbla, trong giao diện C) với kỳ vọng một người sẽ ghi đè nó tại thời gian liên kết - giả định là liên kết tĩnh. Ngoài ra còn có một số lưu ý liên quan về cách điều chỉnh này cho thư viện động trong this header from Apple (xem chú thích trên SetBLASParamErrorProc, gần cuối tệp) - trong trường hợp liên kết động, một cuộc gọi lại cần phải được đăng ký khi chạy.

Xem thêm ghi chú hữu ích bằng "R." trong phần bình luận bên dưới về cách ghi đè này không may là toàn cầu, điều này có thể gây ra vấn đề nếu người dùng sử dụng thư viện của bạn cả trực tiếp và gián tiếp thông qua thư viện thứ hai, và cả người dùng và thư viện thứ hai đều muốn ghi đè trình xử lý.

+0

Đó là một ý tưởng thực sự gọn gàng. Giữ nguyên ví dụ về cấu trúc dữ liệu, người gọi có thể đã cung cấp gọi lại cho ví dụ: giải phóng, so sánh, băm hoặc chuyển đổi các giá trị thành chuỗi - thêm vào đó một lời gọi xử lý lỗi có ý nghĩa. –

+1

Điều này thực sự là một thiết kế thực sự tồi vì nó có trạng thái (liên quan đến các biến toàn cục/trạng thái), trừ khi tất cả các hàm thư viện của bạn lấy một đối số "con trỏ tới ngữ cảnh". Hãy tưởng tượng trường hợp của một ứng dụng sử dụng cả libfoo và libbar gián tiếp sử dụng libfoo. Ứng dụng của bạn thiết lập hàm hook cho cách xử lý các lỗi phân bổ, và sau đó libbar sẽ thay thế nó bằng móc riêng của nó. Bây giờ trình xử lý của ứng dụng của bạn không được gọi. Không mát mẻ. Thư viện không bao giờ nên có loại trạng thái toàn cầu này; thiết kế cực kỳ tệ. –

+0

Ah, tôi đã không nghĩ về nó theo cách đó; Tôi đã suy nghĩ của các thư viện có chức năng hoạt động nhiều hơn hoặc ít hơn như các phương pháp về cấu trúc mờ đục (ví dụ: 'mylib_append (mylib_list * l, void * element)'). Trong trường hợp đó, gọi lại sẽ là một thuộc tính của cấu trúc, cũng sẽ hoạt động như "ngữ cảnh" mà bạn đề cập đến. –

3

Đối với thư viện, bạn có hai lựa chọn. Không có sự hợp tác ứng dụng, khá nhiều tất cả những gì bạn có thể làm là chuyển một lỗi về ứng dụng.

Với sự hợp tác ứng dụng, bạn có thể làm được nhiều hơn thế. Ví dụ: bạn có thể cung cấp ứng dụng để đăng ký cuộc gọi lại mà thư viện của bạn gọi khi malloc trả lại NULL. Bạn có thể chuyển cuộc gọi lại số byte bạn cần và mức độ khẩn cấp bạn cần. (Ví dụ, "hoàn toàn sẽ không hoạt động", "sẽ phải hủy bỏ một hoạt động", và như vậy.) Sau đó, tác giả ứng dụng có thể quyết định có cung cấp cho bạn bộ nhớ hay không.

Ở cấp ứng dụng, bạn có thể làm được nhiều hơn thế. Ví dụ: bạn có thể malloc một loạt các khối bộ nhớ để sử dụng làm "hồ bơi khẩn cấp". Nếu malloc không thành công, bạn có thể chặn các khối khỏi hồ bơi và bắt đầu tải xuống, giảm bộ nhớ cache hoặc bất kỳ lựa chọn nào khác mà bạn phải giảm mức tiêu thụ bộ nhớ.

Nhưng bạn thường không thể thực hiện nhiều việc trong thư viện ngoại trừ thông báo cho đơn đăng ký hợp tác.