errno เป็นเธรดที่ปลอดภัยหรือไม่


175

ในerrno.hตัวแปรนี้มีการประกาศตามextern int errno;คำถามของฉันคือมันปลอดภัยที่จะตรวจสอบerrnoค่าหลังจากการโทรหรือใช้ perror () ในรหัสแบบมัลติเธรด นี่เป็นตัวแปรที่ปลอดภัยหรือไม่ ถ้าไม่เช่นนั้นทางเลือกคืออะไร?

ฉันใช้ linux กับ gcc บนสถาปัตยกรรม x86


5
2 คำตอบที่ตรงข้ามแน่นอนน่าสนใจ !! ;)
vinit dhatrak

ตรวจสอบคำตอบของฉันอีกครั้ง ฉันได้เพิ่มการสาธิตที่อาจจะน่าเชื่อถือ :-)
DigitalRoss

คำตอบ:


176

ใช่มันปลอดภัยสำหรับเธรด บน Linux ตัวแปรโกลบอล errno เป็นแบบเฉพาะเธรด POSIX ต้องการให้ errno นั้นเป็น threadsafe

ดูhttp://www.unix.org/whitepapers/reentrant.html

ใน POSIX.1 errno ถูกกำหนดเป็นตัวแปรโกลบอลภายนอก แต่คำจำกัดความนี้ไม่สามารถยอมรับได้ในสภาพแวดล้อมแบบมัลติเธรดเนื่องจากการใช้งานอาจส่งผลให้เกิดผลลัพธ์แบบไม่เป็นทางการ ปัญหาคือเธรดตั้งแต่สองเธรดขึ้นไปสามารถพบข้อผิดพลาดได้ทั้งหมดทำให้มีการตั้งค่า errno เดียวกัน ภายใต้สถานการณ์เหล่านี้เธรดอาจสิ้นสุดการตรวจสอบ errno หลังจากเธรดอื่นได้รับการอัพเดตแล้ว

หากต้องการหลีกเลี่ยง nondeterminism ที่เกิดขึ้น POSIX.1c นิยามใหม่ errno เป็นบริการที่สามารถเข้าถึงหมายเลขข้อผิดพลาดต่อเธรดดังนี้ (ISO / IEC 9945: 1-1996, §2.4):

บางฟังก์ชั่นอาจมีหมายเลขข้อผิดพลาดในตัวแปรที่เข้าถึงผ่าน errno สัญลักษณ์ errno สัญลักษณ์ถูกกำหนดโดยการรวมส่วนหัวตามที่ระบุโดย C Standard ... สำหรับแต่ละเธรดของกระบวนการค่าของ errno จะไม่ได้รับผลกระทบจากการเรียกใช้ฟังก์ชันหรือการมอบหมายให้ errno โดยเธรดอื่น

ดูhttp://linux.die.net/man/3/errno ด้วย

errno เป็นเธรดแบบโลคัล การตั้งค่าในหนึ่งเธรดไม่ส่งผลกระทบต่อค่าในเธรดอื่น ๆ


9
จริงๆ? พวกเขาทำอย่างนั้นเมื่อใด เมื่อฉันทำการเขียนโปรแกรม C การไว้วางใจ errno เป็นปัญหาใหญ่
Paul Tomblin

7
ชายนั่นจะช่วยประหยัดความยุ่งยากมากมายในสมัยของฉัน
Paul Tomblin

4
@ Winit: errno ถูกกำหนดไว้ใน bits / errno.h อ่านความคิดเห็นในไฟล์ include มันบอกว่า: "ประกาศตัวแปร` errno 'ยกเว้นว่ามันถูกกำหนดให้เป็นแมโครโดย bits / errno.h นี่เป็นกรณีใน GNU ซึ่งเป็นตัวแปรต่อเธรดการประกาศนี้โดยใช้แมโครยังคงใช้งานได้ แต่มัน จะเป็นการประกาศฟังก์ชันโดยไม่มีต้นแบบและอาจเรียกใช้คำเตือน -Wstrict-prototypes
Charles Salvia

2
หากคุณใช้ Linux 2.6 คุณไม่จำเป็นต้องทำอะไรเลย เพียงแค่เริ่มเขียนโปรแกรม :-)
Charles Salvia

3
@vinit dhatrak ควรมี# if !defined _LIBC || defined _LIBC_REENTRANT_LIBC ไม่ได้ถูกกำหนดไว้เมื่อรวบรวมโปรแกรมปกติ อย่างไรก็ตามให้รันเสียงสะท้อน#include <errno.h>' | gcc -E -dM -xc - และดูความแตกต่างด้วยและไม่มี -pthread errno อยู่#define errno (*__errno_location ())ในทั้งสองกรณี
nos

58

ใช่


Errno ไม่ใช่ตัวแปรง่ายๆอีกต่อไปมันเป็นสิ่งที่ซับซ้อนอยู่เบื้องหลังโดยเฉพาะเพื่อให้ปลอดภัยต่อเธรด

ดู$ 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.

เราสามารถตรวจสอบอีกครั้ง:

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

12

ใน errno.h ตัวแปรนี้ถูกประกาศเป็น extern int ภายนอก

นี่คือสิ่งที่มาตรฐาน C บอกว่า:

แมโครerrnoไม่จำเป็นต้องเป็นตัวระบุของวัตถุ มันอาจขยายเป็น lvalue ที่แก้ไขได้ซึ่งเป็นผลมาจากการเรียกใช้ฟังก์ชัน (ตัวอย่างเช่น*errno())

โดยทั่วไปแล้วerrnoเป็นแมโครที่เรียกใช้ฟังก์ชันที่ส่งคืนที่อยู่ของหมายเลขข้อผิดพลาดสำหรับเธรดปัจจุบันจากนั้นยกเลิกการกำหนด

นี่คือสิ่งที่ฉันมีบน Linux ใน /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

ในท้ายที่สุดมันสร้างรหัสประเภทนี้:

> 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

ในระบบ Unix หลายระบบการคอมไพล์ด้วยการ-D_REENTRANTทำให้แน่ใจว่าerrnoปลอดภัยต่อเธรด

ตัวอย่างเช่น:

#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
-D_REENTRANTผมคิดว่าคุณไม่ได้มีการรวบรวมรหัสอย่างชัดเจนด้วย โปรดอ้างอิงการอภิปรายเกี่ยวกับคำตอบอื่น ๆ สำหรับคำถามเดียวกัน
vinit dhatrak

3
@Vinit: ขึ้นอยู่กับแพลตฟอร์มของคุณ - บน Linux คุณอาจถูกต้อง บน Solaris คุณจะถูกต้อง แต่ถ้าคุณได้ตั้ง _POSIX_C_SOURCE ไป 199,506 หรือรุ่นที่ใหม่กว่า - อาจโดยการใช้หรือ-D_XOPEN_SOURCE=500 -D_XOPEN_SOURCE=600ไม่ใช่ทุกคนที่รบกวนจิตใจเพื่อให้แน่ใจว่ามีการระบุสภาพแวดล้อม POSIX แล้ว-D_REENTRANTสามารถบันทึกเบคอนของคุณได้ แต่คุณยังต้องระวังในแต่ละแพลตฟอร์มเพื่อให้แน่ใจว่าคุณได้รับพฤติกรรมที่ต้องการ
Jonathan Leffler

มีเอกสารบางส่วนที่ระบุว่ามาตรฐาน (เช่น: C99, ANSI ฯลฯ ) หรืออย่างน้อยที่สุดคอมไพเลอร์ (เช่น: รุ่น GCC และเป็นต้นไป) ที่รองรับคุณสมบัตินี้และไม่ว่าจะเป็นค่าเริ่มต้นหรือไม่ ขอบคุณ.
เมฆบน

คุณสามารถดูมาตรฐาน C11 หรือที่ POSIX 2008 (2013) สำหรับerrno มาตรฐาน C11 กล่าวว่า: ... และerrnoสิ่งใดที่ขยายเป็น lvalue ที่แก้ไขได้ (201) ซึ่งมีintระยะเวลาการเก็บข้อมูลแบบโลคัลชนิดและเธรดค่าที่ตั้งค่าเป็นหมายเลขข้อผิดพลาดเชิงบวกโดยฟังก์ชันไลบรารีต่างๆ หากคำจำกัดความของแมโครถูกระงับเพื่อเข้าถึงวัตถุจริงหรือโปรแกรมกำหนดตัวระบุด้วยชื่อerrnoพฤติกรรมจะไม่ได้กำหนด [... ดำเนินการต่อ ... ]
Jonathan Leffler

[... ความต่อเนื่อง ... ] เชิงอรรถ 201 พูดว่า: มาโครerrnoไม่จำเป็นต้องเป็นตัวบ่งชี้ของวัตถุ มันอาจขยายเป็น lvalue ที่แก้ไขได้ซึ่งเป็นผลมาจากการเรียกใช้ฟังก์ชัน (ตัวอย่างเช่น*errno()) ข้อความหลักยังคงดำเนินต่อไป: ค่าของ errno ในเธรดเริ่มต้นเป็นศูนย์เมื่อเริ่มต้นโปรแกรม (ค่าเริ่มต้นของ errno ในเธรดอื่นเป็นค่าที่ไม่แน่นอน) แต่ไม่เคยถูกตั้งค่าเป็นศูนย์โดยฟังก์ชันไลบรารีใด ๆ POSIX ใช้มาตรฐาน C99 ซึ่งไม่รู้จักเธรด [... ยังดำเนินการต่อ ... ]
Jonathan Leffler

10

นี่คือจาก<sys/errno.h>บน Mac ของฉัน:

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

ดังนั้นในขณะนี้คือฟังก์ชั่นerrno __error()มีการใช้งานฟังก์ชันเพื่อให้มีความปลอดภัยต่อเธรด


9

ใช่ตามที่อธิบายไว้ในหน้า man errnoและคำตอบอื่น ๆ errno เป็นตัวแปรโลคัลเธรด

อย่างไรก็ตามมีรายละเอียดโง่ ๆ ซึ่งอาจลืมได้ง่าย โปรแกรมควรบันทึกและกู้คืน errno บนตัวจัดการสัญญาณใด ๆ ที่เรียกใช้การเรียกใช้ระบบ นี่เป็นเพราะสัญญาณจะถูกจัดการโดยหนึ่งในเธรดกระบวนการซึ่งสามารถเขียนทับค่าของมัน

ดังนั้นตัวจัดการสัญญาณควรบันทึกและเรียกคืน errno สิ่งที่ต้องการ:

void sig_alarm(int signo)
{
 int errno_save;

 errno_save = errno;

 //whatever with a system call

 errno = errno_save;
}

ต้องห้าม→ลืมฉันเข้าใจ คุณสามารถให้ข้อมูลอ้างอิงสำหรับรายละเอียดการบันทึก / เรียกคืนระบบนี้ได้หรือไม่?
Craig McQueen

สวัสดีเครกขอบคุณสำหรับข้อมูลเกี่ยวกับการพิมพ์ผิดตอนนี้ได้รับการแก้ไขแล้ว เกี่ยวกับปัญหาอื่น ๆ ฉันไม่แน่ใจว่าฉันเข้าใจถูกต้องหรือไม่สิ่งที่คุณขอ สิ่งที่เรียกว่าแก้ไข errno ในตัวจัดการสัญญาณอาจรบกวนการทำงานของ errno ที่ใช้โดยเธรดเดียวกันที่ถูกขัดจังหวะ (เช่นการใช้ strtol ภายใน sig_alarm) ขวา?
marcmagransdeabril

6

ฉันคิดว่าคำตอบคือ "มันขึ้นอยู่กับ" ไลบรารีรันไทม์ C ที่ปลอดภัยสำหรับเธรดมักจะใช้ errno เป็นการเรียกใช้ฟังก์ชัน (แมโครขยายไปยังฟังก์ชัน) หากคุณกำลังสร้างโค้ดเธรดด้วยค่าสถานะที่ถูกต้อง


@Timo ใช่ถูกต้องโปรดอ้างอิงการสนทนาเกี่ยวกับคำตอบอื่น ๆ และช่วยให้รู้ว่ามีอะไรหายไป
vinit dhatrak

3

เราสามารถตรวจสอบได้ด้วยการรันโปรแกรมอย่างง่าย ๆ บนเครื่อง

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

เรียกใช้โปรแกรมนี้และคุณสามารถดูที่อยู่ที่แตกต่างกันสำหรับ errno ในแต่ละเธรด ผลลัพธ์ของการทำงานบนเครื่องของฉันดูเหมือนว่า: -

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 

ขอให้สังเกตว่าที่อยู่จะแตกต่างกันสำหรับกระทู้ทั้งหมด


แม้ว่าการค้นหาใน man page (หรือบน SO) นั้นเร็วกว่า แต่ฉันชอบให้คุณยืนยันเวลา +1
ลำธาร
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.