Là chủ đề errno an toàn?


175

Trong đó errno.h, biến này được khai báo vì extern int errno;vậy câu hỏi của tôi là, có an toàn không khi kiểm tra errnogiá trị sau một số cuộc gọi hoặc sử dụng perror () trong mã đa luồng. Đây có phải là một biến an toàn chủ đề? Nếu không, thì cái gì thay thế?

Tôi đang sử dụng linux với gcc trên kiến ​​trúc x86.


5
2 câu trả lời ngược lại chính xác, thú vị !! ;)
vinit dhatrak

Heh, kiểm tra lại câu trả lời của tôi. Tôi đã thêm một cuộc biểu tình có lẽ sẽ thuyết phục. :-)
DigitalRoss

Câu trả lời:


176

Vâng, nó là chủ đề an toàn. Trên Linux, biến errno toàn cầu là đặc trưng của luồng. POSIX yêu cầu errno là chủ đề an toàn.

Xem http://www.unix.org/whitepapers/reentrant.html

Trong POSIX.1, errno được định nghĩa là biến toàn cục bên ngoài. Nhưng định nghĩa này là không thể chấp nhận được trong một môi trường đa luồng, bởi vì việc sử dụng nó có thể dẫn đến kết quả không xác định. Vấn đề là hai hoặc nhiều luồng có thể gặp lỗi, tất cả gây ra lỗi tương tự được đặt. Trong những trường hợp này, một luồng có thể sẽ kiểm tra errno sau khi nó đã được cập nhật bởi một luồng khác.

Để tránh sự không xác định kết quả, POSIX.1c xác định lại errno là một dịch vụ có thể truy cập vào số lỗi trên mỗi luồng như sau (ISO / IEC 9945: 1-1996, §2.4):

Một số hàm có thể cung cấp số lỗi trong một biến được truy cập thông qua biểu tượng errno. Ký hiệu errno được xác định bằng cách bao gồm tiêu đề, như được chỉ định bởi Tiêu chuẩn C ... Đối với mỗi luồng của một quy trình, giá trị của errno sẽ không bị ảnh hưởng bởi các lệnh gọi hàm hoặc gán cho errno bởi các luồng khác.

Xem thêm http://linux.die.net/man/3/errno

errno là chủ đề cục bộ; thiết lập nó trong một luồng không ảnh hưởng đến giá trị của nó trong bất kỳ luồng nào khác.


9
Có thật không? Khi nào họ làm điều đó? Khi tôi đang làm lập trình C, tin tưởng errno là một vấn đề lớn.
Paul Tomblin

7
Man, điều đó sẽ tiết kiệm rất nhiều rắc rối trở lại trong ngày của tôi.
Paul Tomblin

4
@vinit: errno thực sự được định nghĩa theo bit / errno.h. Đọc các bình luận trong tập tin bao gồm. Nó nói: "Khai báo biến` errno ', trừ khi nó được định nghĩa là macro theo bit / errno.h. Đây là trường hợp trong GNU, trong đó nó là biến trên mỗi luồng. Việc khai báo này sử dụng macro vẫn hoạt động, nhưng nó sẽ là một khai báo hàm mà không có nguyên mẫu và có thể kích hoạt cảnh báo -Wstrict-nguyên mẫu. "
Charles Salvia

2
Nếu bạn đang sử dụng Linux 2.6, bạn không cần phải làm gì cả. Chỉ cần bắt đầu lập trình. :-)
Charles Salvia

3
@vinit dhatrak Nên có # if !defined _LIBC || defined _LIBC_REENTRANT, _LIBC không được xác định khi biên dịch các chương trình bình thường. Dù sao, chạy echo #include <errno.h>' | gcc -E -dM -xc - và nhìn vào sự khác biệt có và không có -pthread. errno là #define errno (*__errno_location ())trong cả hai trường hợp.
số

58

Đúng


Errno không phải là một biến đơn giản nữa, đó là một thứ phức tạp đằng sau hậu trường, đặc biệt là nó an toàn cho chủ đề.

Xem $ man 3 errno:

ERRNO(3)                   Linux Programmers Manual                  ERRNO(3)

NAME
       errno - number of last error

SYNOPSIS
       #include <errno.h>

DESCRIPTION

      ...
       errno is defined by the ISO C standard to be  a  modifiable  lvalue  of
       type  int,  and  must not be explicitly declared; errno may be a macro.
       errno is thread-local; setting it in one thread  does  not  affect  its
       value in any other thread.

Chúng tôi có thể kiểm tra lại:

$ cat > test.c
#include <errno.h>
f() { g(errno); }
$ cc -E test.c | grep ^f
f() { g((*__errno_location ())); }
$ 

12

Trong errno.h, biến này được khai báo là extern int errno;

Đây là những gì tiêu chuẩn C nói:

Macro errnokhông cần phải là định danh của một đối tượng. Nó có thể mở rộng thành một giá trị có thể sửa đổi do một lệnh gọi hàm (ví dụ *errno():).

Nói chung, errnolà một macro gọi một hàm trả về địa chỉ của số lỗi cho luồng hiện tại, sau đó hủy bỏ nó.

Đây là những gì tôi có trên Linux, trong /usr/include/bits/errno.h:

/* Function to get address of global `errno' variable.  */
extern int *__errno_location (void) __THROW __attribute__ ((__const__));

#  if !defined _LIBC || defined _LIBC_REENTRANT
/* When using threads, errno is a per-thread value.  */
#   define errno (*__errno_location ())
#  endif

Cuối cùng, nó tạo ra loại mã này:

> cat essai.c
#include <errno.h>

int
main(void)
{
    errno = 0;

    return 0;
}
> gcc -c -Wall -Wextra -pedantic essai.c
> objdump -d -M intel essai.o

essai.o:     file format elf32-i386


Disassembly of section .text:

00000000 <main>:
   0: 55                    push   ebp
   1: 89 e5                 mov    ebp,esp
   3: 83 e4 f0              and    esp,0xfffffff0
   6: e8 fc ff ff ff        call   7 <main+0x7>  ; get address of errno in EAX
   b: c7 00 00 00 00 00     mov    DWORD PTR [eax],0x0  ; store 0 in errno
  11: b8 00 00 00 00        mov    eax,0x0
  16: 89 ec                 mov    esp,ebp
  18: 5d                    pop    ebp
  19: c3                    ret

10

Trên nhiều hệ thống Unix, biên dịch với -D_REENTRANTđảm bảo errnoan toàn cho luồng.

Ví dụ:

#if defined(_REENTRANT) || _POSIX_C_SOURCE - 0 >= 199506L
extern int *___errno();
#define errno (*(___errno()))
#else
extern int errno;
/* ANSI C++ requires that errno be a macro */
#if __cplusplus >= 199711L
#define errno errno
#endif
#endif  /* defined(_REENTRANT) */

1
Tôi đoán bạn không cần phải biên dịch mã rõ ràng với -D_REENTRANT. Vui lòng tham khảo thảo luận về câu trả lời khác cho cùng một câu hỏi.
vinit dhatrak

3
@Vinit: tùy thuộc vào nền tảng của bạn - trên Linux, bạn có thể đúng; trên Solaris, bạn sẽ chỉ đúng nếu bạn đã đặt _POSIX_C_SOURCE thành 199506 hoặc phiên bản mới hơn - có thể bằng cách sử dụng -D_XOPEN_SOURCE=500hoặc -D_XOPEN_SOURCE=600. Không phải ai cũng bận tâm để đảm bảo rằng môi trường POSIX được chỉ định - và sau đó -D_REENTRANTcó thể lưu thịt xông khói của bạn. Nhưng bạn vẫn phải cẩn thận - trên mỗi nền tảng - để đảm bảo bạn có được hành vi mong muốn.
Jonathan Leffler

Có một tài liệu nào chỉ ra tiêu chuẩn nào (ví dụ: C99, ANSI, v.v.) hoặc ít nhất là trình biên dịch nào (tức là: phiên bản GCC trở đi) hỗ trợ tính năng này hay không và có phải là mặc định không? Cảm ơn bạn.
Đám mây

Bạn có thể xem tiêu chuẩn C11 hoặc tại POSIX 2008 (2013) để biết lỗi . Tiêu chuẩn C11 cho biết: ... và errnomở rộng thành một giá trị có thể sửa đổi (201) có intthời lượng lưu trữ cục bộ kiểu và luồng, giá trị được đặt thành một số lỗi dương bởi một số hàm thư viện. Nếu một định nghĩa vĩ mô bị loại bỏ để truy cập một đối tượng thực tế hoặc một chương trình xác định một định danh với tên errno, hành vi không được xác định. [... tiếp tục ...]
Jonathan Leffler

[... Tiếp tục ...] Chú thích 201 nói: Macro errnokhông cần phải là định danh của một đối tượng. Nó có thể mở rộng thành một giá trị có thể sửa đổi do một lệnh gọi hàm (ví dụ *errno():). Văn bản chính tiếp tục: Giá trị của errno trong luồng ban đầu bằng 0 khi khởi động chương trình (giá trị ban đầu của errno trong các luồng khác là giá trị không xác định), nhưng không bao giờ được đặt thành 0 bởi bất kỳ hàm thư viện nào. POSIX sử dụng tiêu chuẩn C99 không nhận ra chủ đề. [... cũng tiếp tục ...]
Jonathan Leffler

10

Đây là từ <sys/errno.h>trên máy Mac của tôi:

#include <sys/cdefs.h>
__BEGIN_DECLS
extern int * __error(void);
#define errno (*__error())
__END_DECLS

Vì vậy, errnobây giờ là một chức năng __error(). Các chức năng được thực hiện để an toàn chủ đề.


9

vâng , như được giải thích trong trang man errno và các phản hồi khác, errno là một biến cục bộ của luồng.

Tuy nhiên , có một chi tiết ngớ ngẩn có thể dễ dàng bị lãng quên. Các chương trình nên lưu và khôi phục lỗi trên bất kỳ trình xử lý tín hiệu nào thực hiện cuộc gọi hệ thống. Điều này là do tín hiệu sẽ được xử lý bởi một trong các luồng xử lý có thể ghi đè lên giá trị của nó.

Do đó, các trình xử lý tín hiệu nên lưu và khôi phục errno. Cái gì đó như:

void sig_alarm(int signo)
{
 int errno_save;

 errno_save = errno;

 //whatever with a system call

 errno = errno_save;
}

Cấm → quên tôi đoán. Bạn có thể cung cấp một tài liệu tham khảo cho chi tiết lưu / khôi phục cuộc gọi hệ thống này không?
Craig McQueen

Xin chào Craig, cảm ơn thông tin về lỗi đánh máy, hiện đã được sửa. Về vấn đề khác, tôi không chắc chắn nếu tôi hiểu chính xác những gì bạn đang yêu cầu. Bất cứ lệnh gọi nào sửa đổi errno trong trình xử lý tín hiệu đều có thể can thiệp vào lỗi được sử dụng bởi cùng một luồng bị gián đoạn (ví dụ: sử dụng strtol bên trong sig_alarm). đúng?
marcmagransdeabril

6

Tôi nghĩ câu trả lời là "nó phụ thuộc". Các thư viện thời gian chạy C an toàn cho luồng thường triển khai errno dưới dạng gọi hàm (macro mở rộng thành hàm) nếu bạn đang xây dựng mã luồng với các cờ chính xác.


@Timo, Vâng bạn đúng, vui lòng tham khảo thảo luận về câu trả lời khác và cho biết nếu thiếu bất cứ điều gì.
vinit dhatrak

3

Chúng ta có thể kiểm tra bằng cách chạy một chương trình đơn giản trên máy.

#include <stdio.h>                                                                                                                                             
#include <pthread.h>                                                                                                                                           
#include <errno.h>                                                                                                                                             
#define NTHREADS 5                                                                                                                                             
void *thread_function(void *);                                                                                                                                 

int                                                                                                                                                            
main()                                                                                                                                                         
{                                                                                                                                                              
   pthread_t thread_id[NTHREADS];                                                                                                                              
   int i, j;                                                                                                                                                   

   for(i=0; i < NTHREADS; i++)                                                                                                                                 
   {
      pthread_create( &thread_id[i], NULL, thread_function, NULL );                                                                                            
   }                                                                                                                                                           

   for(j=0; j < NTHREADS; j++)                                                                                                                                 
   {                                                                                                                                                           
      pthread_join( thread_id[j], NULL);                                                                                                                       
   }                                                                                                                                                           
   return 0;                                                                                                                                                   
}                                                                                                                                                              

void *thread_function(void *dummyPtr)                                                                                                                          
{                                                                                                                                                              
   printf("Thread number %ld addr(errno):%p\n", pthread_self(), &errno);                                                                                       
}

Chạy chương trình này và bạn có thể thấy các địa chỉ khác nhau cho errno trong mỗi luồng. Đầu ra của một lần chạy trên máy của tôi trông giống như: -

Thread number 140672336922368 addr(errno):0x7ff0d4ac0698                                                                                                       
Thread number 140672345315072 addr(errno):0x7ff0d52c1698                                                                                                       
Thread number 140672328529664 addr(errno):0x7ff0d42bf698                                                                                                       
Thread number 140672320136960 addr(errno):0x7ff0d3abe698                                                                                                       
Thread number 140672311744256 addr(errno):0x7ff0d32bd698 

Lưu ý rằng địa chỉ là khác nhau cho tất cả các chủ đề.


Mặc dù việc tìm kiếm nó trong một trang nam (hoặc trên SO) nhanh hơn, tôi muốn bạn đã dành thời gian để xác minh nó. +1.
Bayou
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.