2010-10-15 34 views
37

Trong hệ thống UNIX, chúng tôi biết malloc() là một chức năng không reentrant (gọi hệ thống). Tại sao vậy?Tại sao malloc() và printf() được gọi là không reentrant?

Tương tự, printf() cũng được cho là không tái phạm; tại sao?

Tôi biết định nghĩa về ủy thác lại, nhưng tôi muốn biết lý do tại sao nó áp dụng cho các chức năng này. Điều gì ngăn cản họ được đảm bảo reentrant?

+1

@ ripunjay-tripathi: printf, nếu đang in tài nguyên chung, ví dụ: stdio. malloc bởi vì nó dựa vào ổ khóa. Hãy nhớ rằng có một sự khác biệt giữa Reentrant và an toàn thread. nơi malloc an toàn chỉ. nhìn vào bài đăng này. http://stackoverflow.com/questions/855763/malloc-thread-safe. – yadab

+0

Bạn đã tìm thấy "triển khai chuẩn" ở đâu? Chức năng là reentrant nếu các nhà phát triển nói rằng họ đang có. nhà phát triển glibc không nói malloc hoặc printf là reentrant: vì vậy họ không. – pmg

+0

@pmg, các từ về "triển khai chính tắc" đã được tôi thêm vào. Đây là ý tôi. Rõ ràng là reentrancy là một thuộc tính của việc thực thi, không phải của một giao diện. Tuy nhiên, ví dụ, POSIX không liệt kê 'malloc' và' printf' là hàm reentrant, và đây là lý do. Trong quesiton này, OP muốn biết lý do là gì. –

Trả lời

48

mallocprintf thường sử dụng cấu trúc toàn cầu và sử dụng đồng bộ hóa dựa trên khóa trong nội bộ. Đó là lý do tại sao họ không reentrant.

Chức năng malloc có thể là an toàn chỉ hoặc không an toàn theo chuỗi. Cả hai đều không reentrant:

  1. Malloc hoạt động trên một đống toàn cầu, và nó có thể là hai lời gọi khác nhau của malloc điều đó xảy ra cùng một lúc, trở về cùng một khối nhớ.(Cuộc gọi malloc thứ 2 sẽ xảy ra trước khi một địa chỉ của đoạn được lấy, nhưng đoạn không được đánh dấu là không có sẵn). Điều này vi phạm điều kiện của malloc, vì vậy việc triển khai này sẽ không được thực hiện lại.

  2. Để ngăn chặn hiệu ứng này, việc triển khai an toàn theo chủ đề malloc sẽ sử dụng đồng bộ hóa dựa trên khóa. Tuy nhiên, nếu malloc được gọi từ xử lý tín hiệu, tình hình sau đây có thể xảy ra:

    malloc();   //initial call 
        lock(memory_lock); //acquire lock inside malloc implementation 
    signal_handler(); //interrupt and process signal 
    malloc();   //call malloc() inside signal handler 
        lock(memory_lock); //try to acquire lock in malloc implementation 
        // DEADLOCK! We wait for release of memory_lock, but 
        // it won't be released because the original malloc call is interrupted 
    

    Tình trạng này sẽ không xảy ra khi malloc được gọi đơn giản là từ chủ đề khác nhau. Thật vậy, khái niệm reentrancy vượt ra ngoài thread-an toàn và cũng đòi hỏi chức năng để hoạt động đúng ngay cả khi một trong các yêu cầu của nó không bao giờ chấm dứt. Đó là cơ bản lý do tại sao bất kỳ chức năng với ổ khóa sẽ không tái nhập.

Chức năng printf cũng hoạt động trên dữ liệu toàn cầu. Bất kỳ luồng đầu ra nào thường sử dụng một bộ đệm toàn cầu gắn liền với dữ liệu tài nguyên được gửi đến (một bộ đệm cho thiết bị đầu cuối hoặc cho một tệp). Quá trình in thường là một chuỗi sao chép dữ liệu vào bộ đệm và xả bộ đệm sau đó. Bộ đệm này nên được bảo vệ bằng các khóa theo cách tương tự malloc. Do đó, printf cũng không phải là reentrant.

+0

Cuối cùng, cảm ơn bạn. Tôi sắp viết nhiều hơn hoặc ít hơn như nhau, nhưng câu trả lời của bạn xuất hiện ngay khi tôi bắt đầu đánh tôi. +1. – janneb

+1

"bất kỳ chức năng nào có khóa sẽ không được tái nhập". Tôi đã làm việc trên một hệ thống mà bạn có thể gắn cờ các mutexes để vô hiệu hóa tín hiệu, hoặc chỉ trong khi chờ đợi trên mutex, hoặc trong suốt thời gian mutex được tổ chức. Rõ ràng nó rất dễ sử dụng sai, và bạn phải đảm bảo chức năng sẽ quay trở lại, nhưng nó được sử dụng để truy cập các globals từ các hàm reentrant (thường trong kernel). Tôi cho rằng không có bằng chứng cho thấy các hạt nhân khác có các cơ chế tương đương, nhưng cũng có nghĩa là tiêu chuẩn không muốn yêu cầu những hành vi như vậy mà tránh được. –

+0

@Steve: Ví dụ, trong các nhân Linux spinlocks thường vô hiệu hóa ngắt khi khóa được thực hiện. Các biến thể ngủ "bình thường" OTOH chạy với các ngắt được bật. – janneb

1

Rất có thể vì bạn không thể bắt đầu viết đầu ra trong khi một lệnh gọi printf khác vẫn đang tự in. Điều tương tự cũng xảy ra đối với việc cấp phát bộ nhớ và deallocation.

+1

Điều này giải thích những gì "tái tham gia" là, nhưng không phải lý do tại sao các chức năng này không tái nhập cảnh. – ChrisF

+0

Rất tuyệt. Tôi đã ngu ngốc vì yêu cầu điều này về printf. Cảm ơn. Nhưng chúng ta không thể gọi malloc() từ hai luồng khác nhau cùng một lúc? –

+0

* "bạn không thể bắt đầu viết đầu ra trong khi một cuộc gọi khác tới printf vẫn đang tự in." * Tại sao? Những gì 'printf' gây ra điều đó? Nó không rõ ràng. Có lẽ kết quả sẽ là 'Hello, woSOMETHINGELSErld!', Nhưng tất cả những gì bạn hỏi sẽ vẫn được in? –

-2

Đó là vì cả hai đều hoạt động với các tài nguyên toàn cầu: cấu trúc bộ nhớ heap và bảng điều khiển.

CHỈNH SỬA: heap không có gì khác ngoài cấu trúc danh sách liên kết loại. Mỗi malloc hoặc free sửa đổi nó, do đó, có một số chủ đề trong cùng một thời điểm với quyền truy cập bằng văn bản vào nó sẽ làm hỏng tính nhất quán của nó.

EDIT2: một chi tiết khác: chúng có thể được đặt lại theo mặc định bằng cách sử dụng mutexes. Nhưng cách tiếp cận này là tốn kém, và không có bảo đảm rằng chúng sẽ luôn được sử dụng trong môi trường MT.

Vì vậy, có hai giải pháp: để tạo 2 hàm thư viện, một hàm reentrant và một không hoặc bỏ phần mutex cho người dùng. Họ đã chọn lựa thứ hai.

Ngoài ra, nó có thể là do các phiên bản gốc của các chức năng này không phải là reentrant, do đó, đã được tuyên bố để tương thích.

+0

Không phải là câu trả lời hay, điều đó hiển nhiên. –

+1

Vì vậy, bạn tuyên bố rằng 'malloc' không phải là chủ đề an toàn? Thú vị ... (-1) Và bạn cũng tuyên bố rằng bao gồm cả mutexes làm cho chức năng reentrant ... Thậm chí thú vị hơn! (-2) –

+0

@PavelShved - cho dù malloc là an toàn thread hay không là một *** [arguable point] (http://stackoverflow.com/q/855763/645128) ***. Ít nhất cung cấp tham chiếu để giúp giải quyết vấn đề. *** [Từ bài viết của riêng bạn] (http://stackoverflow.com/a/3941563/645128) *** bạn không nói rằng có thể malloc có thể là chủ đề *** không an toàn? – ryyker

-4

Nếu bạn thử gọi malloc từ hai luồng riêng biệt (trừ khi bạn có phiên bản an toàn chỉ, không được đảm bảo bằng tiêu chuẩn C), những điều xấu xảy ra, bởi vì chỉ có một đống cho hai luồng. Tương tự cho printf- hành vi là không xác định. Đó là những gì làm cho họ trong thực tế không reentrant.

+0

OK, nhưng nó có thể thất bại ở đâu? Sẽ tốt nếu tôi nhận được một số ví dụ. –

+0

Đó là hành vi không xác định trong đặc điểm kỹ thuật C, không có ví dụ về nơi nó có thể thất bại, gây ra tại thời điểm khi viết đặc tả C này là một vấn đề không (không có chủ đề nào cả). Nó có thể làm việc tìm thấy trên một số triển khai và không phải ở tất cả trên những người khác trong khi cả hai triển khai có thể thực hiện theo các đặc điểm kỹ thuật. – UnixShadow

+0

@RIPUNJAY: Có thể cả hai cuộc gọi tới malloc đều trả về cùng một con trỏ, bởi vì cả hai lệnh gọi malloc đều xác định rằng khối có sẵn (lời gọi đầu tiên sẽ bị gián đoạn giữa việc xác định khối đó là miễn phí và đánh dấu nó là được phân bổ). –

10

Hãy hiểu ý chúng tôi là gì bởi re-entrant. Một hàm re-entrant có thể được gọi trước khi một lời gọi trước đã kết thúc. Điều này có thể xảy ra nếu

  • một hàm được gọi trong một xử lý tín hiệu (hoặc thường hơn Unix một số xử lý ngắt) cho một tín hiệu được đưa ra trong thực hiện các chức năng
  • một hàm được gọi đệ quy

malloc không phải là người tham gia lại vì nó đang quản lý một số cấu trúc dữ liệu toàn cầu theo dõi các khối bộ nhớ miễn phí.

printf không được tái nhập vì nó sửa đổi biến toàn cục tức là nội dung của tệp FILE * stout.

3

Có ít nhất ba khái niệm ở đây, tất cả đều được viết bằng ngôn ngữ thông tục, có thể là lý do bạn nhầm lẫn.

  • thread-safe
  • phần quan trọng
  • góc lõm

Chịu cách đơn giản nhất đầu tiên: Cả mallocprintfthread-safe. Họ đã được đảm bảo an toàn thread trong tiêu chuẩn C kể từ năm 2011, trong POSIX từ năm 2001, và trong thực tế từ lâu trước đó. Điều này có nghĩa là các chương trình sau đây được đảm bảo không để sụp đổ hoặc thể hiện hành vi xấu:

#include <pthread.h> 
#include <stdio.h> 

void *printme(void *msg) { 
    while (1) 
    printf("%s\r", (char*)msg); 
} 

int main() { 
    pthread_t thr; 
    pthread_create(&thr, NULL, printme, "hello");   
    pthread_create(&thr, NULL, printme, "goodbye");   
    pthread_join(thr, NULL); 
} 

Một ví dụ về chức năng mà là không thread-safestrtok. Nếu bạn gọi strtok từ hai luồng khác nhau cùng lúc, kết quả là hành vi không xác định - bởi vì strtok sử dụng bộ đệm tĩnh để theo dõi trạng thái của nó. glibc thêm strtok_r để khắc phục sự cố này và C11 đã thêm cùng một điều (nhưng tùy chọn và dưới tên khác, vì Không được phát minh tại đây) là strtok_s.

Được rồi, nhưng không sử dụng nguồn lực toàn cầu để xây dựng đầu ra của nó? Trong thực tế, nó sẽ là gì ngay cả có nghĩa là để in tới stdout từ hai luồng cùng một lúc? Điều đó đưa chúng ta đến chủ đề tiếp theo. Rõ ràng là printf sẽ là critical section trong bất kỳ chương trình nào sử dụng nó. Chỉ có một luồng thực thi được phép ở bên trong phần quan trọng cùng một lúc.

Ít nhất trong các hệ thống POSIX-compliant, điều này được thực hiện bằng cách printf bắt đầu với một cuộc gọi đến flockfile(stdout) và kết thúc bằng một cuộc gọi đến funlockfile(stdout), đó là cơ bản giống như tham gia một mutex toàn cầu liên quan đến thiết bị xuất chuẩn.

Tuy nhiên, mỗi khác biệt FILE trong chương trình được phép có mutex riêng. Điều này có nghĩa là một sợi có thể gọi fprintf(f1,...) cùng một lúc mà luồng thứ hai đang ở giữa cuộc gọi đến fprintf(f2,...). Không có điều kiện chủng tộc ở đây. (Cho dù libc của bạn thực sự chạy hai cuộc gọi song song là một vấn đề QoI. Tôi không thực sự biết glibc làm gì.)

Tương tự, malloc không phải là một phần quan trọng trong bất kỳ hệ thống hiện đại nào, bởi vì hệ thống hiện đại smart enough to keep one pool of memory for each thread in the system, thay vì có tất cả các chủ đề N chiến đấu trên một hồ bơi duy nhất. (Cuộc gọi sbrk hệ thống sẽ vẫn có thể là một phần quan trọng, nhưng malloc dành rất ít thời gian của mình trong sbrk. Hoặc mmap, hoặc bất cứ điều gì những đứa trẻ mát mẻ đang sử dụng những ngày này.)

Được rồi, vì vậy những gì hiện re-entrancy thực nghĩa là? Về cơ bản, nó có nghĩa là chức năng có thể được gọi một cách an toàn một cách đệ quy - lời gọi hiện tại là "tạm dừng" trong khi lệnh gọi thứ hai chạy, và sau đó lời gọi đầu tiên vẫn có thể "bắt đầu từ nơi nó dừng lại". (Về mặt kỹ thuật, có thể là không phải do cuộc gọi đệ quy: lời gọi đầu tiên có thể nằm trong Chủ đề A, bị gián đoạn ở giữa bởi Chủ đề B, thực hiện lệnh gọi thứ 2. Nhưng kịch bản đó chỉ là trường hợp đặc biệt là -safety, vì vậy chúng ta có thể quên nó ở đoạn này.)

Cả printf cũng không malloc có thể có thể được gọi đệ quy bằng một chủ đề duy nhất, bởi vì họ là những chức năng lá (họ không gọi mình cũng không gọi ra cho bất kỳ mã do người dùng kiểm soát nào có thể thực hiện cuộc gọi đệ quy). Và, như chúng ta đã thấy ở trên, họ đã được an toàn thread chống lại các cuộc gọi lại đa luồng * đa luồng từ năm 2001 (bằng cách sử dụng khóa).

Vì vậy, bất cứ ai đã nói với bạn rằng printfmalloc là không phải là reentrant đã sai; những gì họ có nghĩa là để nói có lẽ là cả hai người trong số họ có khả năng được phần quan trọng trong chương trình của bạn - tắc nghẽn, nơi chỉ có một sợi có thể nhận được thông qua tại một thời điểm.


lưu ý pedantic: glibc không cung cấp một phần mở rộng mà printf có thể được thực hiện để gọi mã người dùng tùy ý, bao gồm tái tự xưng. Điều này là hoàn toàn an toàn trong tất cả các hoán vị của nó - ít nhất là theo như thread-an toàn là có liên quan. (Rõ ràng là nó mở ra cánh cửa hoàn toàn có lỗ hổng điên.) Có hai biến thể: register_printf_function (được ghi lại và hợp lý lành mạnh nhưng chính thức "không được chấp nhận") và register_printf_specifier (là gần như giống hệt nhau tham số không có giấy tờ và total lack of user-facing documentation). Tôi sẽ không đề nghị một trong số họ, và đề cập đến họ ở đây chỉ là một thú vị sang một bên.

#include <stdio.h> 
#include <printf.h> // glibc extension 

int widget(FILE *fp, const struct printf_info *info, const void *const *args) { 
    static int count = 5; 
    int w = *((const int *) args[0]); 
    printf("boo!"); // direct recursive call 
    return fprintf(fp, --count ? "<%W>" : "<%d>", w); // indirect recursive call 
} 
int widget_arginfo(const struct printf_info *info, size_t n, int *argtypes) { 
    argtypes[0] = PA_INT; 
    return 1; 
} 
int main() { 
    register_printf_function('W', widget, widget_arginfo); 
    printf("|%W|\n", 42); 
} 
+4

Câu trả lời này là đúng _almost_; nhưng thiếu khái niệm chính từ POSIX, an toàn tín hiệu _async_. Mã ứng dụng _can_ thực hiện ở giữa cuộc gọi đến 'printf' hoặc' malloc' như là kết quả của tín hiệu không đồng bộ. Không cần chức năng nào là an toàn-tín hiệu không đồng bộ, do đó, không an toàn khi gọi chúng là _from trình xử lý cho tín hiệu không đồng bộ_. Đó là những gì các lập trình viên hệ thống POSIX có nghĩa là khi họ nói rằng 'printf' và' malloc' là "không reentrant". – zwol

+0

"Vì vậy, bất cứ ai nói với bạn rằng printf và malloc là không reentrant là sai" Điều gì về xử lý tín hiệu? Với chủ đề các chủ đề ban đầu cuối cùng sẽ nhận được CPU trở lại và tiếp tục nhưng với xử lý tín hiệu không có gì xảy ra cho đến khi xử lý tín hiệu trở về ... –

+0

Jerry: Khi tôi đưa ra câu trả lời này, tôi đã được ấn tượng rằng "reentrant" và "async-tín hiệu an toàn "không phải là từ đồng nghĩa, và malloc/printf là" reentrant nhưng không async-signal-safe ". Thực ra, tôi vẫn còn dưới ấn tượng đó; nhưng nhận xét của @ zwol cho thấy rằng có một phần đáng kể dân số tin rằng * là * từ đồng nghĩa và do đó bất kỳ chức năng nào không an toàn-tín hiệu an toàn đều không thể * có thể là "reentrant" theo định nghĩa. Có vẻ như Jerry cũng ở trong trại đó. Tôi đoán đạo đức là: khi sử dụng biệt ngữ, người ta nên biết khán giả của mình và giả định của họ. :) – Quuxplusone