Apakah errno aman?

175

Dalam errno.h, variabel ini dinyatakan sebagai extern int errno;pertanyaan saya, apakah aman untuk memeriksa errnonilai setelah beberapa panggilan atau menggunakan perror () dalam kode multi-utas. Apakah ini variabel thread aman? Jika tidak, lalu apa alternatifnya?

Saya menggunakan linux dengan gcc pada arsitektur x86.

vinit dhatrak
sumber
5
2 jawaban sebaliknya, menarik !! ;)
vinit dhatrak
Heh, periksa kembali jawabanku. Saya telah menambahkan demonstrasi yang mungkin akan meyakinkan. :-)
DigitalRoss

Jawaban:

176

Ya, aman untuk thread. Di Linux, variabel errno global bersifat spesifik-utas. POSIX mengharuskan errno menjadi threadsafe.

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

Dalam POSIX.1, errno didefinisikan sebagai variabel global eksternal. Tetapi definisi ini tidak dapat diterima dalam lingkungan multithreaded, karena penggunaannya dapat menghasilkan hasil yang tidak ditentukan. Masalahnya adalah bahwa dua atau lebih utas dapat mengalami kesalahan, semua menyebabkan kesalahan yang sama diatur. Dalam keadaan ini, utas mungkin berakhir dengan memeriksa errno setelah utas tersebut diperbarui.

Untuk menghindari nondeterminisme yang dihasilkan, POSIX.1c mendefinisikan kembali errno sebagai layanan yang dapat mengakses nomor kesalahan per-utas sebagai berikut (ISO / IEC 9945: 1-1996, §2.4):

Beberapa fungsi dapat memberikan nomor kesalahan dalam variabel yang diakses melalui simbol errno. Simbol errno didefinisikan dengan memasukkan header, sebagaimana ditentukan oleh Standar C ... Untuk setiap utas proses, nilai errno tidak akan terpengaruh oleh pemanggilan fungsi atau penugasan untuk errno oleh utas lain.

Lihat juga http://linux.die.net/man/3/errno

errno adalah thread-local; mengaturnya di satu utas tidak memengaruhi nilainya di utas lainnya.

Charles Salvia
sumber
9
Betulkah? Kapan mereka melakukan itu? Ketika saya melakukan pemrograman C, mempercayai errno adalah masalah besar.
Paul Tomblin
7
Sobat, itu akan menyelamatkan banyak kerumitan di hari saya.
Paul Tomblin
4
@vinit: errno sebenarnya didefinisikan dalam bits / errno.h. Baca komentar di file sertakan. Ia mengatakan: "Deklarasikan variabel` errno ', kecuali jika itu didefinisikan sebagai makro oleh bit / errno.h. Ini adalah kasus di GNU, di mana itu adalah variabel per-thread. Deklarasi ulang ini menggunakan makro masih berfungsi, tapi itu akan menjadi deklarasi fungsi tanpa prototipe dan dapat memicu peringatan -Westrict-prototypes. "
Charles Salvia
2
Jika Anda menggunakan Linux 2.6, Anda tidak perlu melakukan apa pun. Mulai saja pemrograman. :-)
Charles Salvia
3
@vinit dhatrak Seharusnya ada # if !defined _LIBC || defined _LIBC_REENTRANT, _LIBC tidak didefinisikan ketika mengkompilasi program normal. Pokoknya, jalankan echo #include <errno.h>' | gcc -E -dM -xc - dan lihat perbedaannya dengan dan tanpa -pthread. errno #define errno (*__errno_location ())dalam kedua kasus.
no.
58

Iya


Errno bukan variabel sederhana lagi, itu sesuatu yang rumit di belakang layar, khusus untuk itu agar aman.

Lihat $ 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.

Kami dapat memeriksa ulang:

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

Di errno.h, variabel ini dideklarasikan sebagai extern int errno;

Inilah yang dikatakan standar C:

Makro errnotidak perlu menjadi pengidentifikasi objek. Mungkin diperluas ke nilai yang dapat dimodifikasi yang dihasilkan dari panggilan fungsi (misalnya, *errno()).

Secara umum, errnoadalah makro yang memanggil fungsi mengembalikan alamat nomor kesalahan untuk utas saat ini, lalu menyimpikannya.

Inilah yang saya miliki di Linux, di /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

Pada akhirnya, ini menghasilkan kode semacam ini:

> 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
Bastien Léonard
sumber
10

Pada banyak sistem Unix, kompilasi dengan -D_REENTRANTmemastikan bahwa errnothread-safe.

Sebagai contoh:

#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) */
Jonathan Leffler
sumber
1
Saya kira Anda tidak perlu mengkompilasi kode secara eksplisit -D_REENTRANT. Silakan merujuk pada diskusi tentang jawaban lain untuk pertanyaan yang sama.
vinit dhatrak
3
@Vinit: itu tergantung pada platform Anda - di Linux, Anda mungkin benar; pada Solaris, Anda hanya akan benar jika Anda telah menetapkan _POSIX_C_SOURCE ke 199506 atau versi yang lebih baru - mungkin dengan menggunakan -D_XOPEN_SOURCE=500atau -D_XOPEN_SOURCE=600. Tidak semua orang mengganggu untuk memastikan bahwa lingkungan POSIX ditentukan - dan kemudian -D_REENTRANTdapat menyimpan daging Anda. Tetapi Anda masih harus berhati-hati - pada setiap platform - untuk memastikan Anda mendapatkan perilaku yang diinginkan.
Jonathan Leffler
Apakah ada sepotong dokumentasi yang menunjukkan standar apa (yaitu: C99, ANSI, dll) atau paling tidak kompiler mana (yaitu: versi GCC dan seterusnya) yang mendukung fitur ini, dan apakah itu merupakan default atau tidak? Terima kasih.
Cloud
Anda dapat melihat standar C11, atau di POSIX 2008 (2013) untuk errno . Standar C11 mengatakan: ... dan errnoyang meluas ke nilai yang dapat dimodifikasi (201) yang memiliki jenis intdan durasi penyimpanan lokal, nilai yang ditetapkan ke angka kesalahan positif oleh beberapa fungsi perpustakaan. Jika definisi makro ditekan untuk mengakses objek yang sebenarnya, atau program mendefinisikan pengidentifikasi dengan nama errno, perilaku tidak terdefinisi. [... lanjutan ...]
Jonathan Leffler
[... lanjutan ...] Catatan kaki 201 mengatakan: Makro errnotidak perlu menjadi pengidentifikasi suatu objek. Mungkin diperluas ke nilai yang dapat dimodifikasi yang dihasilkan dari panggilan fungsi (misalnya, *errno()). Teks utama berlanjut: Nilai errno di utas awal adalah nol pada startup program (nilai awal errno di utas lain adalah nilai yang tidak ditentukan), tetapi tidak pernah disetel ke nol oleh fungsi pustaka apa pun. POSIX menggunakan standar C99 yang tidak mengenali utas. [... juga melanjutkan ...]
Jonathan Leffler
10

Ini dari <sys/errno.h>pada Mac saya:

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

Jadi errnosekarang fungsi __error(). Fungsi ini diimplementasikan agar aman.

ay32
sumber
9

ya , seperti yang dijelaskan di halaman manual errno dan balasan lainnya, errno adalah variabel lokal thread.

Namun , ada detail konyol yang bisa dengan mudah dilupakan. Program harus menyimpan dan mengembalikan kesalahan pada penangan sinyal yang menjalankan panggilan sistem. Ini karena sinyal akan ditangani oleh salah satu utas proses yang dapat menimpa nilainya.

Oleh karena itu, penangan sinyal harus menyimpan dan mengembalikan errno. Sesuatu seperti:

void sig_alarm(int signo)
{
 int errno_save;

 errno_save = errno;

 //whatever with a system call

 errno = errno_save;
}
marcmagransdeabril
sumber
terlarang → lupa saya kira. Bisakah Anda memberikan referensi untuk detail penyimpanan / pengembalian panggilan sistem ini?
Craig McQueen
Halo Craig, terima kasih atas info tentang kesalahan ketik, sekarang sudah diperbaiki. Mengenai masalah lain, saya tidak yakin apakah saya mengerti dengan benar apa yang Anda minta. Panggilan apa pun yang memodifikasi errno di penangan sinyal dapat mengganggu errno yang digunakan oleh utas yang sama yang terputus (misalnya menggunakan strtol di dalam sig_alarm). Baik?
marcmagransdeabril
6

Saya pikir jawabannya adalah "itu tergantung". Pustaka runtime C aman-thread biasanya menerapkan errno sebagai panggilan fungsi (makro meluas ke suatu fungsi) jika Anda membuat kode berulir dengan flag yang benar.

Timo Geusch
sumber
@Timo, Ya benar, silakan lihat diskusi tentang jawaban lain dan beri tahu jika ada yang kurang.
vinit dhatrak
3

Kita dapat memeriksa dengan menjalankan program sederhana pada mesin.

#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);                                                                                       
}

Menjalankan program ini dan Anda dapat melihat alamat yang berbeda untuk errno di setiap utas. Output dari pelarian pada mesin saya tampak seperti: -

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 

Perhatikan bahwa alamat berbeda untuk semua utas.

Rajat Paliwal
sumber
Meskipun mencari di halaman manual (atau di SO) lebih cepat, saya suka Anda telah meluangkan waktu untuk memverifikasinya. +1.
Bayou