table of contents
- unstable 4.31.0-1
| SELECT_TUT(2) | System Calls Manual | SELECT_TUT(2) |
الاسم¶
select, pselect - تعدد إدخال/إخراج متزامن
المكتبة¶
مكتبة سي المعيارية (libc، -lc)
موجز¶
انظر select(2)
الوصف¶
تُستخدم استدعاءات النظام select() وpselect() لمراقبة واصفات ملفات متعددة بكفاءة، لمعرفة ما إذا كان أي منها جاهزًا أو يصبح جاهزًا؛ أي لمعرفة ما إذا كان الإدخال/الإخراج ممكنًا، أو حدثت "حالة استثنائية" على أي من واصفات الملفات.
توفر هذه الصفحة معلومات أساسية وتعليمية حول استخدام استدعاءات النظام هذه. للحصول على تفاصيل الوسائط ودلالات select() وpselect()، انظر select(2).
دمج أحداث الإشارات والبيانات¶
يكون pselect() مفيدًا إذا كنت تنتظر إشارة بالإضافة إلى واصف (واصفات) ملف لتصبح جاهزة للإدخال/الإخراج. تستخدم البرامج التي تستقبل الإشارات عادةً معالج الإشارة فقط لرفع علامة عامة. ستشير العلامة العامة إلى أن الحدث يجب معالجته في الحلقة الرئيسة للبرنامج. ستتسبب الإشارة في عودة استدعاء select() (أو pselect()) مع تعيين errno إلى EINTR. هذا السلوك ضروري حتى يمكن معالجة الإشارات في الحلقة الرئيسة للبرنامج، وإلا فسيحظر select() إلى أجل غير مسمى.
الآن، سيكون هناك شرط في مكان ما في الحلقة الرئيسة للتحقق من العلامة العامة. لذا يجب أن نسأل: ماذا لو وصلت إشارة بعد الشرط، ولكن قبل استدعاء select()؟ الإجابة هي أن select() سيحظر إلى أجل غير مسمى، على الرغم من أن هناك حدثًا معلقًا بالفعل. تُحل حالة السباق هذه بواسطة استدعاء pselect(). يمكن استخدام هذا الاستدعاء لضبط قناع الإشارة إلى مجموعة من الإشارات التي ستُستقبل فقط داخل استدعاء pselect(). على سبيل المثال، لنفترض أن الحدث المعني هو خروج عملية تابعة. قبل بدء الحلقة الرئيسة، سنحظر SIGCHLD باستخدام sigprocmask(2). سيمكّن استدعاء pselect() الخاص بنا SIGCHLD باستخدام قناع إشارة فارغ. سيبدو برنامجنا كالتالي:
static volatile sig_atomic_t got_SIGCHLD = 0;
static void
child_sig_handler(int sig)
{
got_SIGCHLD = 1;
}
int
main(int argc, char *argv[])
{
sigset_t sigmask, empty_mask;
struct sigaction sa;
fd_set readfds, writefds, exceptfds;
int r;
sigemptyset(&sigmask);
sigaddset(&sigmask, SIGCHLD);
if (sigprocmask(SIG_BLOCK, &sigmask, NULL) == -1) {
perror("sigprocmask");
exit(EXIT_FAILURE);
}
sa.sa_flags = 0;
sa.sa_handler = child_sig_handler;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGCHLD, &sa, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}
sigemptyset(&empty_mask);
for (;;) { /* الحلقة الرئيسة */
/* هيئ readfds و writefds و exceptfds
قبل استدعاء pselect(). (الكود محذوف). */
r = pselect(nfds, &readfds, &writefds, &exceptfds,
NULL, &empty_mask);
if (r == -1 && errno != EINTR) {
/* عالج الخطأ */
}
if (got_SIGCHLD) {
got_SIGCHLD = 0;
/* عالج الحدث المؤشر عليه هنا؛ مثلاً wait() لكل
الأبناء المنتهين. (الكود محذوف). */
}
/* المتن الرئيس للبرنامج */
}
}
عملي¶
إذن ما الغاية من select()؟ ألا يمكنني ببساطة القراءة والكتابة إلى واصفات الملفات الخاصة بي متى شئت؟ الغاية من select() هي أنه يراقب واصفات متعددة في الوقت نفسه ويضع العملية في حالة سكون بشكل صحيح إذا لم يكن هناك نشاط. غالبًا ما يجد مبرمجو يونكس أنفسهم في موقف يضطرون فيه إلى التعامل مع إدخال/إخراج من أكثر من واصف ملف واحد حيث قد يكون تدفق البيانات متقطعًا. إذا قمت ببساطة بإنشاء تسلسل من استدعاءات read(2) وwrite(2)، فستجد أن أحد استدعاءاتك قد يحظر انتظار البيانات من/إلى واصف ملف، بينما واصف ملف آخر غير مستخدم على الرغم من أنه جاهز للإدخال/الإخراج. يتعامل select() بكفاءة مع هذا الموقف.
قانون select¶
يواجه الكثيرون ممن يحاولون استخدام select() سلوكًا يصعب فهمه وينتج نتائج غير قابلة للنقل أو نتائج حدية. على سبيل المثال، البرنامج أعلاه مكتوب بعناية لعدم الحظر في أي نقطة، على الرغم من أنه لا يضبط واصفات الملفات الخاصة به على وضع عدم الحظر. من السهل إدخال أخطاء دقيقة تزيل ميزة استخدام select()، لذا إليك قائمة بالأساسيات التي يجب مراقبتها عند استخدام select().
- 1.
- يجب أن تحاول دائمًا استخدام select() بدون مهلة زمنية. يجب ألا يكون لبرنامجك ما يفعله إذا لم تكن هناك بيانات متاحة. الكود الذي يعتمد على المهل الزمنية ليس عادةً قابلاً للنقل ويصعب تنقيحه.
- 2.
- يجب حساب قيمة nfds بشكل صحيح لتحقيق الكفاءة كما هو موضح أعلاه.
- 3.
- يجب عدم إضافة أي واصف ملف إلى أي مجموعة إذا كنت لا تنوي التحقق من نتيجته بعد استدعاء select()، والاستجابة بشكل مناسب. انظر القاعدة التالية.
- 4.
- بعد عودة select()، يجب فحص جميع واصفات الملفات في جميع المجموعات لمعرفة ما إذا كانت جاهزة.
- 5.
- الدوال read(2) وrecv(2) وwrite(2) وsend(2) لا تقرأ/تكتب بالضرورة كامل كمية البيانات التي طلبتها. إذا قرأت/كتبت كامل الكمية، فذلك لأن لديك حمولة مرور منخفضة وتدفقًا سريعًا. لن يكون هذا هو الحال دائمًا. يجب أن تتعامل مع حالة تمكن دوالك من إرسال أو استقبال بايت واحد فقط.
- 6.
- لا تقرأ/تكتب أبدًا بايتات مفردة فقط في المرة الواحدة إلا إذا كنت متأكدًا حقًا من أن لديك كمية صغيرة من البيانات لمعالجتها. من غير الفعال للغاية عدم قراءة/كتابة أكبر قدر ممكن من البيانات التي يمكنك تخزينها مؤقتًا في كل مرة. المخازن المؤقتة في المثال أدناه هي 1024 بايت على الرغم من أنه يمكن جعلها أكبر بسهولة.
- 7.
- استدعاءات read(2) وrecv(2) وwrite(2) وsend(2) وselect() يمكن أن تفشل مع الخطأ EINTR، واستدعاءات read(2) وrecv(2) وwrite(2) وsend(2) يمكن أن تفشل مع ضبط errno على EAGAIN (EWOULDBLOCK). يجب إدارة هذه النتائج بشكل صحيح (لم يُفعل ذلك بشكل صحيح أعلاه). إذا كان برنامجك لن يستقبل أي إشارات، فمن غير المرجح أن تحصل على EINTR. إذا كان برنامجك لا يضبط الإدخال/الإخراج غير المحظور، فلن تحصل على EAGAIN.
- 8.
- لا تستدعِ أبدًا read(2) أو recv(2) أو write(2) أو send(2) بطول مخزن مؤقت صفري.
- 9.
- إذا فشلت الدوال read(2) أو recv(2) أو write(2) أو send(2) مع أخطاء غير تلك المذكورة في 7.، أو أعادت إحدى دوال الإدخال 0، مما يشير إلى نهاية الملف، فعندئذٍ يجب ألا تمرر واصف الملف هذا إلى select() مجددًا. في المثال أدناه، أغلقتُ واصف الملف فورًا، ثم ضبطتُه على -1 لمنع إدراجه في مجموعة.
- 10.
- يجب تهيئة قيمة المهلة الزمنية مع كل استدعاء جديد لـ select()، لأن بعض أنظمة التشغيل تعدل البنية. ومع ذلك، فإن pselect() لا يعدل بنية المهلة الخاصة به.
- 11.
- بما أن select() يعدل مجموعات واصفات الملفات الخاصة به، فإذا كان الاستدعاء يُستخدم في حلقة، فيجب إعادة تهيئة المجموعات قبل كل استدعاء.
قيمة الإرجاع¶
انظر select(2).
ملاحظات¶
بشكل عام، تدعم جميع أنظمة التشغيل التي تدعم المقابس أيضًا select(). يمكن استخدام select() لحل العديد من المشكلات بطريقة قابلة للنقل وفعالة يحاول المبرمجون المبتدئون حلها بطريقة أكثر تعقيدًا باستخدام الخيوط، والتفريع (forking)، والتواصل بين العمليات (IPCs)، والإشارات، ومشاركة الذاكرة، وما إلى ذلك.
استدعاء النظام poll(2) له نفس وظيفة select()، وهو أكثر كفاءة إلى حد ما عند مراقبة مجموعات واصفات الملفات المتناثرة. وهو متاح الآن على نطاق واسع، ولكن تاريخيًا كان أقل قابلية للنقل من select().
توفر واجهة برمجة تطبيقات epoll(7) الخاصة بلينكس واجهة أكثر كفاءة من select(2) وpoll(2) عند مراقبة أعداد كبيرة من واصفات الملفات.
أمثلة¶
إليك مثال يوضح بشكل أفضل الفائدة الحقيقية لـ select(). القائمة أدناه هي برنامج توجيه TCP يوجه من منفذ TCP إلى آخر.
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
static int forward_port;
#undef max
#define max(x, y) ((x) > (y) ? (x) : (y))
static int
listen_socket(int listen_port)
{
int lfd;
int yes;
struct sockaddr_in addr;
lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd == -1) {
perror("socket");
return -1;
}
yes = 1;
if (setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR,
&yes, sizeof(yes)) == -1)
{
perror("setsockopt");
close(lfd);
return -1;
}
memset(&addr, 0, sizeof(addr));
addr.sin_port = htons(listen_port);
addr.sin_family = AF_INET;
if (bind(lfd, (struct sockaddr *) &addr, sizeof(addr)) == -1) {
perror("bind");
close(lfd);
return -1;
}
printf("accepting connections on port %d\n", listen_port);
listen(lfd, 10);
return lfd;
}
static int
connect_socket(int connect_port, char *address)
{
int cfd;
struct sockaddr_in addr;
cfd = socket(AF_INET, SOCK_STREAM, 0);
if (cfd == -1) {
perror("socket");
return -1;
}
memset(&addr, 0, sizeof(addr));
addr.sin_port = htons(connect_port);
addr.sin_family = AF_INET;
if (!inet_aton(address, (struct in_addr *) &addr.sin_addr.s_addr)) {
fprintf(stderr, "inet_aton(): bad IP address format\n");
close(cfd);
return -1;
}
if (connect(cfd, (struct sockaddr *) &addr, sizeof(addr)) == -1) {
perror("connect()");
shutdown(cfd, SHUT_RDWR);
close(cfd);
return -1;
}
return cfd;
}
#define SHUT_FD1 do { \
if (fd1 >= 0) { \
shutdown(fd1, SHUT_RDWR); \
close(fd1); \
fd1 = -1; \
} \
} while (0)
#define SHUT_FD2 do { \
if (fd2 >= 0) { \
shutdown(fd2, SHUT_RDWR); \
close(fd2); \
fd2 = -1; \
} \
} while (0)
#define BUF_SIZE 1024
int
main(int argc, char *argv[])
{
int h;
int ready, nfds;
int fd1 = -1, fd2 = -1;
int buf1_avail = 0, buf1_written = 0;
int buf2_avail = 0, buf2_written = 0;
char buf1[BUF_SIZE], buf2[BUF_SIZE];
fd_set readfds, writefds, exceptfds;
ssize_t nbytes;
if (argc != 4) {
fprintf(stderr, "Usage\n\tfwd <listen-port> "
"<forward-to-port> <forward-to-ip-address>\n");
exit(EXIT_FAILURE);
}
signal(SIGPIPE, SIG_IGN);
forward_port = atoi(argv[2]);
h = listen_socket(atoi(argv[1]));
if (h == -1)
exit(EXIT_FAILURE);
for (;;) {
nfds = 0;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_ZERO(&exceptfds);
FD_SET(h, &readfds);
nfds = max(nfds, h);
if (fd1 > 0 && buf1_avail < BUF_SIZE)
FD_SET(fd1, &readfds);
/* Note: nfds is updated below, when fd1 is added to
exceptfds. */
if (fd2 > 0 && buf2_avail < BUF_SIZE)
FD_SET(fd2, &readfds);
if (fd1 > 0 && buf2_avail - buf2_written > 0)
FD_SET(fd1, &writefds);
if (fd2 > 0 && buf1_avail - buf1_written > 0)
FD_SET(fd2, &writefds);
if (fd1 > 0) {
FD_SET(fd1, &exceptfds);
nfds = max(nfds, fd1);
}
if (fd2 > 0) {
FD_SET(fd2, &exceptfds);
nfds = max(nfds, fd2);
}
ready = select(nfds + 1, &readfds, &writefds, &exceptfds, NULL);
if (ready == -1 && errno == EINTR)
continue;
if (ready == -1) {
perror("select()");
exit(EXIT_FAILURE);
}
if (FD_ISSET(h, &readfds)) {
socklen_t addrlen;
struct sockaddr_in client_addr;
int fd;
addrlen = sizeof(client_addr);
memset(&client_addr, 0, addrlen);
fd = accept(h, (struct sockaddr *) &client_addr, &addrlen);
if (fd == -1) {
perror("accept()");
} else {
SHUT_FD1;
SHUT_FD2;
buf1_avail = buf1_written = 0;
buf2_avail = buf2_written = 0;
fd1 = fd;
fd2 = connect_socket(forward_port, argv[3]);
if (fd2 == -1)
SHUT_FD1;
else
printf("connect from %s\n",
inet_ntoa(client_addr.sin_addr));
/* Skip any events on the old, closed file
descriptors. */
continue;
}
}
/* NB: read OOB data before normal reads. */
if (fd1 > 0 && FD_ISSET(fd1, &exceptfds)) {
char c;
nbytes = recv(fd1, &c, 1, MSG_OOB);
if (nbytes < 1)
SHUT_FD1;
else
send(fd2, &c, 1, MSG_OOB);
}
if (fd2 > 0 && FD_ISSET(fd2, &exceptfds)) {
char c;
nbytes = recv(fd2, &c, 1, MSG_OOB);
if (nbytes < 1)
SHUT_FD2;
else
send(fd1, &c, 1, MSG_OOB);
}
if (fd1 > 0 && FD_ISSET(fd1, &readfds)) {
nbytes = read(fd1, buf1 + buf1_avail,
BUF_SIZE - buf1_avail);
if (nbytes < 1)
SHUT_FD1;
else
buf1_avail += nbytes;
}
if (fd2 > 0 && FD_ISSET(fd2, &readfds)) {
nbytes = read(fd2, buf2 + buf2_avail,
BUF_SIZE - buf2_avail);
if (nbytes < 1)
SHUT_FD2;
else
buf2_avail += nbytes;
}
if (fd1 > 0 && FD_ISSET(fd1, &writefds) && buf2_avail > 0) {
nbytes = write(fd1, buf2 + buf2_written,
buf2_avail - buf2_written);
if (nbytes < 1)
SHUT_FD1;
else
buf2_written += nbytes;
}
if (fd2 > 0 && FD_ISSET(fd2, &writefds) && buf1_avail > 0) {
nbytes = write(fd2, buf1 + buf1_written,
buf1_avail - buf1_written);
if (nbytes < 1)
SHUT_FD2;
else
buf1_written += nbytes;
}
/* Check if write data has caught read data. */
if (buf1_written == buf1_avail)
buf1_written = buf1_avail = 0;
if (buf2_written == buf2_avail)
buf2_written = buf2_avail = 0;
/* One side has closed the connection, keep
writing to the other side until empty. */
if (fd1 < 0 && buf1_avail - buf1_written == 0)
SHUT_FD2;
if (fd2 < 0 && buf2_avail - buf2_written == 0)
SHUT_FD1;
}
exit(EXIT_SUCCESS);
}
يقوم البرنامج أعلاه بتوجيه معظم أنواع اتصالات TCP بشكل صحيح بما في ذلك بيانات الإشارة خارج النطاق (OOB) التي ترسلها خوادم telnet. يعالج المشكلة الصعبة المتمثلة في تدفق البيانات في كلا الاتجاهين في وقت واحد. قد تظن أنه من الأكثر كفاءة استخدام استدعاء fork(2) وتخصيص خيط لكل تيار. يصبح هذا أكثر تعقيدًا مما قد تظن. فكرة أخرى هي ضبط الإدخال/الإخراج غير المحظور باستخدام fcntl(2). هذا أيضًا له مشاكله لأنك ينتهي بك الأمر باستخدام مهلات غير فعالة.
لا يعالج البرنامج أكثر من اتصال واحد في وقت واحد، على الرغم من أنه يمكن توسيعه بسهولة للقيام بذلك باستخدام قائمة مرتبطة من المخازن المؤقتة—واحد لكل اتصال. في الوقت الحالي، تتسبب الاتصالات الجديدة في قطع الاتصال الحالي.
انظر أيضًا¶
accept(2), connect(2), poll(2), read(2), recv(2), select(2), send(2), sigprocmask(2), write(2), epoll(7)
ترجمة¶
تُرجمت هذه الصفحة من الدليل بواسطة زايد السعيدي <zayed.alsaidi@gmail.com>
هذه الترجمة هي وثيقة مجانية؛ راجع رخصة جنو العامة الإصدار 3 أو ما بعده للاطلاع على شروط حقوق النشر. لا توجد أي ضمانات.
إذا وجدت أي أخطاء في ترجمة صفحة الدليل هذه، يرجى إرسال بريد إلكتروني إلى قائمة بريد المترجمين: kde-l10n-ar@kde.org.
| 8 فبراير 2026 | صفحات دليل لينكس 6.18 |