7.2 مكتبة سي القياسية ونظام يونكس
محتويات هذا الفصل:
7.2.1 مقدمة
مكتبة سي القياسية التي تأتي مع أنظمة GNU/Linux هي من غنو GNU C Library
وللاختصار glibc اكتب info glibc لتقرأ معلومات تفصيلية (تفصيلاً مملاً)
وهي مكتبة مطابقة لمواصفات ANSI C و ISO/IEC 9899/1990
و ISO/IEC 9945-1/1996 POSIX و ANSI/IEEE Std 1003
وهي تطبق بعض متطلبات BSD 4.4
وبعض XSI أي X/open system interface
هذا التبعيض جاء لأسباب تتعلق بالأمان والموثوقية
(لأن هناك بعضٌ لم يطبق لأنه قد يسبب ثغيرة أو ماشابه)
أو لأسباب تتعلق بالأداء . تضمن لك هذه المعايير
أن يعمل برنامج على أكبر عدد من الأنظمة لأطول فترة ممكنة
بأعلى أداء ممكن. بإمكانك القول أن مكتبة سي القياسية من غنو
تعمل على أي نظام تقريباً في هذا العالم المتغير.
7.2.2 التعامل مع الأخطاء
عند استدعاء أي وظيفة من مكتبة سي القياسية فإنها في
الغالب تعيد ما يدل على أنها نجحت أو فشلت
مثلاً malloc تعيد مؤشر على الذاكرة المحجوزة
و NULL أي صفر في حال الفشل.
وبعض الوظائف تعيد رقم سالب في حالة الفشل ،
نستخدم هذا الرقم لنعرف أن الخطأ قد حدث
ولكن حتى نحصل على تفاصيل أكبر عن الخلل الذي سبب هذا الخطأ
بطريقة مفهومة أكثر نستخدم رقم يوضح سبب الفشل اسمه
errno وهو معرف في ملف errno.h
يفضل قبل أن تستدعي الوظيفة التي تسبب أخطاء أن تصفر هذا
المتغير وتفحصه بعد استدعائها ، ولترجمة هذا الرقم الأصم إلى
نص مفهوم بشرياً نستخدم strerror(errno)
التي تعيد سلسلة نصية تفسر الخطأ
هذه الوظيفة معرفة في stdio.h
يمكنك أن تطبع ناتجها على الخرج القياسي باستعمال printf
أو على جهاز الخطأ القياسي بوضع stderr على أنه الملف في
fprintf أو بمجرد استدعاء
perror أي print error
التي تأخذ معاملها مؤشر للنص المراد طباعته
على جهاز الخطأ وأسهل طريقة هي بتمرير NULL لها فتطبع
هي تلقاياً الخطأ الذي يعبر عنه strerror(errno)
مثلاً
#include<stdio.h>
int main() {
int *a;
/* try to allocate 1000 integers */
if ( !(a=malloc(sizeof(int)*1000)) ) {
/* if fail print error and exit*/
perror(NULL);
return 1;
}
return 0;
}
لاحظ أن ناتج عملية الإحلال "=" هو الطرف الأيمن أي أن
if ( !(a=malloc(sizeof(int)*1000)) )
تعني ضع malloc(sizeof(int)*1000) كقيمة ل a
الآن انفي الناتج منطقياً "!" أي إذا كانت صفراً NULL
(منطقياً تسمى FALSE)
تصبح TRUE الآن افحص هل هي متحققة TRUE
يمكن كتابة البرنامج السابق كما يلي
#include<stdio.h>
#include<stdlib.h>
int main() {
int *a;
/* try to allocate 1000 integers */
a=malloc(sizeof(int)*1000);
if ( !a ) {
/* if fail print error and exit*/
perror(NULL);
return 1;
}
return 0;
}
أو حتى
#include<stdio.h>
int main() {
int *a;
/* try to allocate 1000 integers */
a=malloc(sizeof(int)*1000);
if ( a==NULL ) {
/* if fail print error and exit*/
perror(NULL);
return 1;
}
return 0;
}
ولكن الطريقة الأكثر شيوعاً والأفضل هي الأولى .
يجب أن تلاحظ أن strerror
تعيد سلسة نصية محجوزة مسبقاً بطريقة static
بكلمات أخرى أنه صالح حتى أول استدعاء آخر لهذه
الوظيفة مرة أخرى لأنه حينها سيتم الكتابة فوقه
أي أن البرنامج التالي
#include<errno.h>
#include<stdio.h>
int main() {
char *str1,*str2;
str1=strerror(EPERM);
str2=strerror(ENOENT);
fprintf(stderr,"\n%s\n",str1);
fprintf(stderr,"\n%s\n",str2);
return 1;
}
سيطبع No such file or directory. مرتين
جرت تقاليد UNIX أن يطبع البرنامج عد حدوث خطأ اسمه
ثم نقطتين ثم الخطأ كما يتوقعه المبرمج
ثم فاصلة منقوطة ثم ما تخبرنا به مكتبة سي من تفاصيل
يمكن عمل ذلك كما في المثال (المثال مأخوذ من GNU C library manual مع تعديل بسيط)
/* errtest.c: a program to tell error */
#include<errno.h>
#include<stdio.h>
int main() {
char name[80];
FILE *file1;
printf("Enter file name: ");
fgets(name,80,stdin);
printf("trying to open %s for reading.",name);
errno = 0;
if (!file1=fopen(name,"r")) {
fprintf(stderr,"%s: Can't open file '%s'; %s\n",
program_invocation_short_name,
name,strerror(errno) );
return errno;
}
printf("The file '%s' opened successfully",name\n);
fclose(file1);
return 0;
}
نفذ لبرناماج مع إعطائه اسم ملف غير موجود
ستحصل على ما يشبه :
errtest: Can't open file 'foo.bar'; No such file or directory.
لاحظ أننا استخدمنا المتغير program_invocation_short_name
لإعادة اسم البرنامج مع حذف الأدلة
وهو تعبير غير قياسي من إضافات GNU وهناك أيضا
program_invocation_name
الذي يحتوي اسم البرنامج الكامل مع الأدلة
الطرقة القياسية لعمل ذلك هي استعمال المتغير argv[0]
وحذف الأدلة منه ب basename الموجود في string.h
/* errtest.c: a program to tell error */
#include<errno.h>
#include<stdio.h>
#include<string.h>
int main(int argc,char *argv[]) {
char name[80];
FILE *file1;
printf("Enter file name: ");
fgets(name,80,stdin);
printf("trying to open %s for reading.",name);
errno = 0;
if (!file1=fopen(name,"r")) {
fprintf(stderr,"%s: Can't open file '%s'; %s\n",
basename(argv[0]),
name,strerror(errno) );
return errno;
}
printf("The file '%s' opened successfully",name\n);
fclose(file1);
return 0;
}
وعندما يصبح البرنامج في حالة لا يمكنه المتابعة بعدها
بسبب حدوث خطأ عندها يمكن الخروج (من البرنامج وليس من الوظيفة) باستعمال
exit التي تأخذ معامل رقم يمثل سبب الخروج(أي الناتج)
ويكون غير صفري لبيان أنه ناتج عن خطأ مثلاً exit(errno);
وهو معرف في stdlib.h
ولأنه سيكثر استخدامه مع الشرط أي إذا لم تنجح في كذا اخرج
هناك اختصار هو assert التي تأخذ معاملاً هو الشرط
الذي يجب التأكد منه (شرط عدم الخروج) فإذا لم يحدث انتهى البرنامج
وإذا حدث تابع البرنامج سيره هذا الأخير معرف في assert.h
7.2.3 حجز الذاكرة
ما تعلمنا قبلاً نحجز ذاكرة في أي وقت باستعمال
malloc التي تأخذ معاملاً يدل على حجم الذاكرة بالبايت
وتعيد مؤشر من نوع void * يمكن تحويله
لأي نوع آخر صراحةً بكتابته بين قوسين أو تركه ضمني
(هذا الأخير مسموح به في المعايير الحديثة ولكن هناك معايير قديمة تشترط التحويل الصريح)
هذا في حال النجاح أما في حال فشل العملية تعيد NULL التي تكافئ صفر
وعندها طبعاً يجب أن لا تتابع البرنامج بل عليك
المحاولة برقم أقل أو الخروج مع كتابة أن السبب هو عدم وجود ذاكرة كافية..
وعمر هذه الذاكرة من عمر البرنامج أي عند إغلاق البرنامج
يتم تحريرها وإذا أردت تحريرها قبل هذا عليك استعمال free
التي تأخذ معامل وحيد هو مؤشر على الذاكرة المراد تحريرها
ويجب أن يكون هذا المؤشر محجوز ب malloc أو ما يكافئها
ويجب أن لا تحرر مؤشر محرر! وبعد تحرير ذاكرة يجب أن لا تقرأ أو تكتب فيها.
لتغير حجم ذاكرة محجوزة نستخدم realloc التي تغير الحجم مع الإحتفاظ
بالبيانات الموجودة فيها ، تأخذ realloc معاملين الأول هو المؤشر القديم الذي تريد
تحجيمه والثاني هو الحجم الجديد لذي قد يكون أكبر أو أصغر من القديم.
في حالة أن يكون الجديد أكبر يتم محاولة توسعة المؤشر الحالي نفسه
فإذا فشلت حاول حجز ذاكرة جديدة ونقل البيانات لها
وفي حالة أن يكون الجديد أقل سيحاول تقليص حجمها فإن لم ينجح
حجز مكان آخر ونقلها إليه ، بمعنى أنه في الحالتين قد تحصل على
مؤشر جديد وفي حالة الفشل سيكون هذا الجديد NULL دون المساس بالمؤشر القديم.
لاحظنا أنه في حال فشل malloc أو realloc
لا يستطيع البرنامج المتابعة إذ عليه معالجة
الوضع فإذا كان البرنمج تفاعلي يفترض أن يطبع رسالة
تفيد أنه العملية تحتاج ذاكرة أكثر مما هو متوفر
وتظهر زر لإعادة المحاولة(ربما بقيم أقل) وآخر للخرج، ولكن
في أغلب البرامج النصية التي تسير على تقاليد يونكس
جرت العادة أن يطبع رسالة تتفيد الخطأ ويخرج من البرنامج
ولأن مسألة حجز الذاكرة مسألة متكررة في البرامج
ومن غير العملي متابعة كل حالة تقترح كتيبات GNU عمل وظيفة وتسميتها
xmalloc وأخرى xrealloc
تأخذان نفس معاملات malloc و realloc
على الترتيب وتعيدان مؤشر على الذاكرة المحجوزة
ولكن في حالة الفشل لا تعيدان NULL بل تطبع رسالة خطأ وتخرج
هذه نسخة معدلة عنهما
void *xmalloc(size_t bytes) {
void *r; errno=0;
if ( !(r=malloc(bytes)) ) {
fprintf(stderr,"%s: Couldn't allocate memory; %s\n\a",
program_invocation_short_name,
strerror(errno) );
exit(errno); /* this is exit NOT return */
}
return r;
}
void *xrealloc(void *ptr,size_t bytes) {
void *r; errno=0;
if ( !(r=realloc(ptr,bytes)) ) {
fprintf(stderr,"%s: Couldn't allocate memory; %s\n\a",
program_invocation_short_name,
strerror(errno) );
exit(errno); /* this is exit NOT return */
}
return r;
}
توضع في مكان ما فوق ال main أو حتى في ملف منفصل
عندها يكون البرنامج مشابه لما يلي
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
/* ... */
/* you may put xmalloc & xrealloc here */
/* ... */
int main() {
int n; int *a;
printf("Enter number of integers: ");
scanf("%d",&n);
a=xmalloc(sizeof(int)*n);
printf("%i integer allocated successfully\n",n);
return 0;
}
دون أي شرط بعد كل حجز للذاكرة.
تكون البانات المحجوزة باستعمال * غير مستهلة بأي قيمة وهذا
لا يعني أنها تساوي صفر بل تكون قيم لا يمكن ضمان قيمتها (شبه عشوائية) ،
لحجز ذاكرة وتصفيرها نستخدم calloc
التي تأخذ معاملين الأول هو عدد العناصر والثاني
هو حجم العنصر الواحد بالبايت مثلاً ما يقابل
malloc(sizeof(int)*n); هو calloc(n,sizeof(int));
عند حجز ذاكرة باستعمال malloc
داخل وظيفة فإنها تظل صالحة حتى بعد الخروج منها (الوظيفة) إلى أن تخرج من البرنامج
بأكمله وفي الغالب فإن الكثير من الوظائف يتم تصميمها بأن
تحرر الذاكرة بعد الخروج منها عندها عليك القيام بذلك يدوياً
باستدعاء free وللتيسير عليك وإراحتك من ذلك
يمكنك أن تحجز ذاكرة من مكدس الوظيفة باستعمال alloca
التي يتم تحريرها تلقائياً عند الخروج من الوظيفة.
انظر هذا المثال:
/* ... */
int stack_alloc(int n) {
int i,*a;
printf("Allocating %d integer(s): ...",n);
if ( !(a=alloca(sizeof(int)*n)) ) return errno;
printf("%i integer allocated successfully\n",n);
for (i=0;i<n;++i) {a[i]=i; printf("a[%d]=%d\n",i,a[i]);}
printf("it will be automaticlly freed\n");
/* we need not call `free(a)' */
return 0;
}
هذا نموذج الوظائف التي تحدثنا عنها بخصوص الذاكرة وهي معرفة في stdlib.h
void *malloc(size_t bytes);
void *alloca(size_t bytes);
void *calloc(size_t items_count,size_t item_size);
void *realloc(void *ptr,size_t bytes);
void free(void *ptr);
لاحظ أن size_t هو مجرد عدد صحيح int
7.2.4 التعامل مع السلاسل النصية
لقد تحدثنا مسبقاً بإيجاز عن الوظائف الأساسية الخاصة بالسلاسل النصية في بند
7.1.14 السلاسل النصية من فصل
البرمجة بلغة C/C++ ولكننا سنتحدث هنا عن المزيد.
موضوع السلاسل النصية على علاقة وثيقة مع المنظومات(مناطق من الذاكرة)
لأن السلاسل عبارة عن منظومة عناصرها char
وتنهي ب '\0' لهذا توفر مكتبة string.h
الوظائف اللازمة للتعامل مع المنظومات والسلاسل النصية
والفرق بينها أن الوظائف التي تتعامل مع المنظومات نعطيها
الحجم بالبايت الذي يجب أن تطبق عليه بينهما الأخرى تتحسس نهاية السلسلة
ب '\0' وإذا أعطينها العدد فإنها تأخذ أيهما يأتي أولاً
،ويجب الإنتباه هنا أن طول السلسلة لا يشمل علامة النهاية '\0'
بمعنى آخر أن حجم الذاكرة المخصصة له تكون طول السلسة + 1 ويمكن معرفة طول السلسلة باستعمال الوظيفة.
strlen بتمرير السلسة لها كمعامل.
لنسخ محتويات منطقة في الذاكرة إلى منطقة أخرى
نستخدم memcpy التي تأخذ
مؤشر على المنطقة الهدف(المراد النسخ إليها) ثم مؤشر إلى المنطقة المصدر(النسخ منها)
ثم حجم المنطقة ، في المقابل لنسخ سلسلة نصية
نستخدم strcpy التي تأخذ مؤشر على السلسلة الهدف
ثم السلسلة المصدر وأما الوظيفة strncpy تأخذ معامل إضافي هو الحد الأعلى لطول
السلسلة.
/* strcpy-test.c: copying strings */
#include<stdio.h>
#include<string.h>
int main() {
char s1[]="Hello world";
char s2[]="good bye ....... world";
strcpy(s2,s1);
printf("new s2=%s",s2);
return 0;
}
في ذاك المثال يتم الكتابة فوق ما هو موجود في
s2 لتصبح قيمته
"Hello world\0..... world"
بحيث يطبع البرنامج على الشاشة عند تنفيذه
Hello world لأنه يتوقف عند علامة النهاية
'\0'
في كل الحالات السابقة يجب أن تكون المنطقتين غير متقاطعتين
لأن الخوارزمية التي تقوم بذلك تفترض ذلك فإذا كانتا متقاطعتين فإن
الناتج سيكون يحتوي على منطقة مشوهة مليئة بتكرار للتعامل مع التقاطع نستخدم
memmove انظر هذا المثال
/* memcpy-test.c: */
#include<stdio.h>
#include<string.h>
int main() {
char s1[]="good bye ........ world";
char s2[]="good bye ........ world";
char s3[]="good bye ........ world";
memcpy(s1+9,s1,8); /* non overlaping */
memcpy(s2+5,s2,8); /* overlaping */
memmove(s3+5,s3,8); /* overlaping */
printf("new s1=%s",s1);
printf("new s2=%s",s2);
printf("new s3=%s",s3);
return 0;
}
في المرة الأولى قمنا بنقل ثمانية بايت من s1
وهي good bye إلى الموقع المشار إليه ب s1+9
أي فوق النقاط وهنا لا يوجد منطقة مشتركة لذا سيظهر على الشاشة
good bye good bye world
في الثانية نقل ثمناية بايت من s2
وهي good bye إلى الموقع المشار له بs2+5
أي فوق بداية bye وفي هذه الحالة نلاحظ وجود
منطقة تقاطع من 3 بايت ، ولانستطيع أن نتوقع ما سيظهر على الشاشة
فقد يكون المصنف ذكي بما فيه الكفاية ولكن لا تعتمد على ذلك.
في الحالة الثالثة استعملنا الطريقة الصحيحة للتعامل مع مناطق التقاطع
فسينتج good good bye.... world"
لملئ منطقة من الذاكرة بقيمة محددة
نستخدم memset التي تأخذ
مؤشر على المنطقة ثم القيمة (على شكل unsigned char) ثم
حجم المنطقة .
7.2.5 الاقترانات/الوظائف الرياضية
--------------
const
--------------
M_E : e=e1=exp(1) about 2.718281828459
M_LOG2E : log2e
M_LOG10E : log10e
M_LN2 : loge2
M_LN10 : loge10
M_PI : acos(-1)=3.14159265358979323846264338327
M_PI_2 : M_PI/2
M_PI_4 : M_PI/4
M_1_PI : 1/M_PI
M_2_PI : 2/M_PI
M_2_SQRTPI : 2/sqrt(M_PI)
M_SQRT2 : sqrt(2)
M_1_SQRT2 : 1/sqrt(2)=sqrt(1/2)
and add l like M_El M_PIl to have long double
--------------
Trigonometric
--------------
sin sinf sinl
cos,tan
*sincos(x,&sinx,&cosx)
csin : complex sin=1/(2i) * ( exp(zi)-exp(-zi) )
ccos,ctan
--------------
Inverse Trigonometric
--------------
asin asinf asinl
acos,atan
atan2(y,x) : atan(y/x)
casin : complex sin inv
cacos,catan
--------------
Exp & log
--------------
exp expf expl : ex
exp2 exp2f exp2l : 2x
exp10 exp10f exp10l pow10 pow10f pow10l: 10x
expm1 expm1f expm1l: (exp(x)-1) but more accurate when x near zero
cexp cexpf cexpl : complex exp(z) = exp(creal(z))*( cos(cimg(z))+Isin(cimg(z)) )
pow powf powl : bx
cpow: compilx power
sqrt sqrtf sqrtl: square root
cbrt cbrtf cbrtl: cube root
csqrt csqrtf csqrtl: complix square root
hypot hypotf hypotl: hypot(a,b)=sqrt(a2+b2)
log logf logl : logex called ln(x) in math
log10 log10f log10l : log10x
log2 log2f log2l : log2x
logp1 logp1f logp1l: loge(x+1) more accurate when x near zero
clog clogf clogl: complex log = log(cabs(z)+Icarg(z))
*clog10
logb logbf logbl : the exponenet part ie floor (log2x)
--------------
Hyperbolic
--------------
sinh sinhf sinhl: (exp(x)-exp(-x))/2
cosh coshf coshl: (exp(x)+exp(-x))/2
tansh tanshf tanshl: sinh/cosh
csinh : (cexp(x)-cexp(-x))/2
ccosh : (cexp(x)+cexp(-x))/2
ctansh: csinh/ccosh
--------------
Inverse Hyperbolic
--------------
asinh asinhf asinhl
acosh acoshf acoshl
atansh atanshf atanshl
acsinh
accosh
actansh
--------------
Special Functions (only real part)
--------------
erf erff erfl: (2/sqrt(M_PI))*intgeral(e-t2.dt,0,x)
erfc erfcf erfcl: 1-erf but more accurate for larg x
tgamma tgammaf tgammal: gamma(x)=integral(t(x-1).e-t.dt,0,inf)
lgamma lgammaf lgammal: loge(tgamma(x)) and sign(tgamma(x)) saved in signgam
j0 j0f j0l: 1st kind Bessel function of order 0
j1 j0f j0l: 1st kind Bessel function of order 1
jn jnf jnl: 1st kind Bessel function of order n
y0 y0f y0l: 2nd kind Bessel function of order 0
y1 y0f y0l: 2nd kind Bessel function of order 1
yn ynf ynl: 2nd kind Bessel function of order n
7.2.6 التاريخ والوقت
عندما نقول "وقت" أو "زمن" فإننا نقصد هنا الوقت والتاريخ معاً
أو ما يسمى التقويم أو الوقت المطلق ويتم التعبير عنه بطريقة نصية
Wed Feb 18 16:02:05 2004
ولكن كما نعلم التعامل مع النصوص يقلل من أداء البرامج
لذا تستخدم مكتبة سي القياسية النوع time_t
لتمثيل الوقت وهو في غنو رقم يمثل الثواني منذ
تاريخ محدد (منتصف ليل 1/1/1970 حسب التوقيت الدولي) بحيث أنك إذا أردت أن تعرف كم
ثانية بين زمنين يمكنك طرحمها (ولكن هذا قد لا يكون مضموناً في المصنفات الأخرى) وعليك
استخدام الوظيفة difftime
التي تأخذ معاملين هما الزمن التالي ثم الزمن الأول وتعيد الثواني بينهما
لمعرفة الوقت الحالي استدعي time(NULL);
التي تعيد الوقت الحالي على شكل time_t
ويمكن أن تمرر لها مؤشر (تحصل عليه ب عملية "&") لمتغير من نوع time_t
ليضع الوقت الحالي فيه كما يلي
time_t t1,t2;
t1=time(NULL); /* method 1 */
time(&t2); /* method 2 */
هناك نوع آخر لمتغيرات الوقت هو tm
ويسمى هذا النوع بالوقت المحلل broken-down time وهو تمثيل للوقت
على شكل عناصر هي الثواني tm_sec
و الدقائق tm_min
والساعات tm_hour
واليوم tm_mday
والشهر tm_mon
والسنة منذ 1900 tm_year
يوم الأسبوع(صفر:الأحد و 2:الإثنين وهكذا) tm_wday
وكم يوم من بداية السنة tm_yday
وكلها تبدأ العد من صفر يعني اليوم العاشر من الشهر الأول
نخزنهما تسمعة و صفر
وتحتوي أيضا علامة تحدد Daylight Saving Time tm_isdst
وتعتبر فعالة إذا كانت موجبة وغير فعالة إذا كانت صفراً وغير معروفة إذا كانت سالبة
(أظن أنها تعني التوقيت الصيفي)
وهناك إضافات مثلاً في غنو حقل يحدد عدد الثواني إلى الشرق من
UTC أي التوقيت الدولي Universal Time Coordenaite
أو كما يسميه العامة توقيت غرينتش GMT
مثلاً الأردن +2*60*60
يسمى هذا الحقل tm_gmtoff
وآخر يعطي اسم المنطقة الزمنية tm_zone
لتحويل الوقت من time_t إلى tm
نستخدم gmtime أو localtime
نمرر لهما مؤشر ل time_t الذي نريد نحويله
فتعيد مؤشر (لذاكرة داخل المكتبة) من نوع tm
الأولى حسب توقيت غرينتش والثانية حسب التوقيت المحلي للجهاز
(في ويندوز تتحكم بها من لوحة التحكم وملفات ini وأيضا عبارة COUNTRY في ملف CONFIG.SYS وفي لينكس
تفعل ذلك متغيرات البيئة ونصوص الإقلاع وطبعاً مركز التحكم )
انظر هذا المثال
/* time-test.c: tells GMT/UTC and local time */
#include<stdio.h>
#include<time.h>
int main() {
tm *tm1;
time_t t1=time(NULL);
tm1=gmtime(&t1);
printf("%02d:%02d:%02d@UTC\n",
tm1->tm_hour,tm1->tm_min,tm1->tm_sec);
tm1=localtime(&t1);
printf("%02d:%02d:%02d@localtime\n",
tm1->tm_hour,tm1->tm_min,tm1->tm_sec);
return 0;
}
لتحويل الوقت من tm إلى نص نستخدم
asctime التي تأخذ مؤشر إلى الوقت المحلل tm
وتعيد مؤشر لسلسة نصية (تنتهي بسطر جديد) وهي ضمن ذاكرة مكتبة سي
ولتحويل الوقت من time_t إلى نص نستخدم
ctime التي تأخذ مؤشر إلى time_t
وتعيد مؤشر لسلسلة نصية وكأن هذه الوظيفة
عبارة عن استدعاء ل localtime ثم asctime
مثلاً لطباعة الوقت الحالي printf("%s\n",ctime(time(NULL)));
الوظيفتان asctime و ctime
تنتجان سلسلة نصية بالطريقة القياسية التي تشبه
Wed Feb 18 16:02:05 2004
إذا أردت طريقة أخرى لتنسيق النص استعمل
strftime
التي تأخذ سلسلة نصية لكتابة النص ورقم يمثل الحد الأقصى لطولها
ثم سلسلة تمثل الهيئة التي تريد تنسيق النص بها
بطريقة تشبه طريقة sprintf
ثم أخيراً متغير الوقت المحلل من نوع tm
السلسلة التي تنسق النص تكون الأشياء فيها كما هي عدا علامة
'%' التي تفسر وفقاً لما بعدها
| %a | اسم اليوم من الأسبوع مختصراً (باللغة المحلية) |
| %A | اسم اليوم من الأسبوع كاملاً (باللغة المحلية) |
| %b | اسم الشهر مختصراً (باللغة المحلية) |
| %B | اسم الشهر كاملاً (باللغة المحلية) |
| %c | التاريخ والوقت بالتنسيق الذي تحدده الإعدادات المحلية |
| %C | القرن أي رقم السنة مع حدف الآحاد والعشرات |
| %d | اليوم من الشهر(العد من واحد) |
| %D | الطريقة الأمريكية مثل 12/31/04 بكلمات أخرى%m/%d/%y |
| %e | اليوم من الشهر(العد من واحد) كما في %d مع وضع مسافة بدل الصفر على اليسار |
| %F | طريقة ISO مثل 2004-12-31 بكلمات أخرى%Y-%m-%d |
| %H | الساعة بنظام ال24 ساعة |
| %I | الساعة بنظام ال12 ساعة |
| %m | رقم الشهر (العد من واحد) |
| %M | الدقائق |
| %p | صباحاً/مساءً مختصرة مثلاً am/pm |
| %P | صباحاً/مساءً مختصرة مثلاً AM/PM |
| %r | الوقت والتاريخ بنظام 12 ساعة وفقاً للإعدادات المحلية |
| %R | الساعة والدقيقية %H:%M |
| %S | الثواني |
| %T | الوقت دون التاريخ بطريقة %H:%M:%S |
| %x | التاريخ وفقاً للإعدادات المحلية |
| %X | الوقت من اليوم(دون التاريخ) وفقاً للإعدادات المحلية |
| %z | المنطقة الزمنية كرقم |
| %Z | المنطقة الزمنية كنص مختصر |
| %% | % |
لعكس العملية أي التحويل من نص إلى وقت tm
نستعمل strptime
التي تأخذ سلسلة نصية ليتم تحليلها ثم أخرى للتنسيق ثم مؤشر على
وقت محلل من نوع tm
لتغيير الوقت نستخدم stime التي تأخذ
مؤشر إلى time_t وتعيد صفر في حال النجاح في ذلك
7.2.7 الإشارات signals
كما في الأداة kill من سطر الأوامر
بحيث نستطيع إرسال إشارة معينة لبرنامج معين (في الغالب لإنهاء البرنامج) ،
توفر مكتبة سي الوظيفة kill التي تأخذ معاملين أولمها
هو معرف العملية pid والآخر رقم الإشارة وهي واحدة من
SIGABRT SIGALRM SIGBUS SIGCHLD
SIGCLD SIGCONT SIGEMT SIGFPE
SIGHUP SIGILL SIGINFO SIGINT
SIGIO SIGIOT SIGKILL SIGLOST
SIGPIPE SIGPOLL SIGPROF SIGQUIT
SIGSEGV SIGSTKSZ SIGSTOP SIGSYS
SIGTERM SIGTRAP SIGTSTP SIGTTIN
SIGTTOU SIGURG SIGUSR1 SIGUSR2
SIGVTALRM SIGWINCH SIGXCPU SIGXFSZ
تعيد الوظيفة صفر في حال النجاح.
هذه الوظيفة معرفة في ملف signal.h
يمكن أن يرسل البرنامج إشارة لنفسه ب raise
التي تأخذ معامل واحد هو الإشارة.
بعض هذه الإشارات مثل SIGKILL تنهي البرنامج بحث لا يملك البرنامج
منعها أو إهمالها أو القيام بأي شيء ولكن بعض الإشارات مثل SIGTERM
يمكن تغيير السلوك التلقائي لها (مثلاً شطب ملف مؤقت قبل لخروج) ،
بعض هذه الإشارات يمكن ضبطها على موعد معين
بالوظيفة alarm التي تأخذ معامل واحد هو عدد الثواني
فهذه الوظيفة ترسل إشارة SIGALRM بعد إنقضاء
تلك الثواني.
بعض هذه الإشارات تحديداً SIGUSR1 و SIGUSR2
وضعت لتستخدمها أنت كيفما شئت للتخاطب بين برامجك.
ليتمكن البرنامج من التعامل مع الإشارات التي يستلمها
بطريقة مختلفة عن السلوك التلقائي
بحيث تعرف وظيفة تأخذ معامل هو رقم الإشارة ولا تعيد شيء مثلاً
void my_handler(int signum) {
printf ("Someone send me signal %d\n",signum);
}
لوضع هذه الوظيفة لتكون هي التي تستدع عند وصول إشارة
نستعمل الوظيفة signal المعامل الأول هو الإشارة
والثاني هو الوظيفة. تعيد هذه الوظيفة في حال النجاح مؤشر على
الوظيفة التي كانت سابقاً. يمكنك استعمال الوظيفتين التاليتين
SIG_DFL و SIG_IGN للسلوك التلقائي و لإهمال الإشارة على الترتيب.
أشهر الوظائف التي قد تفكر في تغيير سلوكها
- SIGALRM تنبيه ناتج عن توقيت
- SIGTERM الإشارة الشهيرة لإنهاء برنامج
- SIGINT المقاطعة الناتجة من ضغط CTRL+C
- SIGTSTP التوقيف الناتجة من ضغط CTRL+Z
- SIGHUP فصل الإتصال
مثلاً لعدم الاستجابة ل CTRL+C يمكنك عمل
7.2.8 الأرقام العشوائية
نقصد بالعشوائية تلك التي تظهر كذلك ولكنها في الحقيقة سلسلة رياضية؛
لهذا تسمى في غنو pseudo random numbers ، توفر مكتبة سي القياسية
وظائف خاصة بذلك وهي معرفة في stdlib.h
كلما تريد رقم عشوائي استدع rand
لكي يعطيك رقم غير سالب بين الصفر و RAND_MAX
الذي هو 2147483647 أي الجزء غير السالب من العدد الصحيح int ولكنه في
الأنظمة من طراز 16 بت يكون 32767 ، بتنفيذ هذه الوظيفة
تحصل على سلسلة من الأرقام تبدو عشوائية
وعند تنفيذ البرنامج مرة أخرى تحصل على نفس السلسلة
دائماً ولكن هذا في الغالب ليس هو المطلوب بل المطلوب
هو الحصول على سلسلة مختلفة في كل مرة
لهذا نستخدم ما يسمى بذور seeds وهي رقم يحدد
الرقم العشوائي التالي ونحدده باستدعاء srand في بداية البرنامج
وتمرير رقم لها وبتمرير رقم مختلف في كل مرة
نحص على سلسلة مختلفة من الأرقام ولكن من أين نحصل على رقم مختلففي كل مرة؟
الجواب هو باستدعاء time(NULL)
التي تعيد "الوقت" الحالي (عدد صحيح يمثل الثواني منذ 1970) وطبعا
يكون استدعاء srand(time(NULL)) مرة واحدة في بداية
البرنامج ، ثم استدعائات متتالية ل rand
ولكي تحصل على رقم يتراوح بين a و b
يمكنك استعمال عملية باقي القسمة التي تعيد رقم
أكبر أو يساوي الصفر وأقل من الرقم المقسوم عليه ثم جمع رقم البداية أي
int r=(rand()%(b-a+1))+a
مثلاً للحصول على رقم 1-6
استدع (rand()%6)+1
هناك طريقة أخرى للحصول على الأرقام العشوائية
وهي باستدعاء drand48() التي تعيد عدد نسبي بين الصفر
و الواحد (الفترة مفتوحة) وللحصول على فترة أخرى اضرب
في طول الفترة واجمع بدايتها ونستعمل srand48 وتمرير عدد
صحيح له لعمل لسلة مختلفة في كل مرة كما في srand
7.2.9 التعامل مع الملفات بطريقة السيال المنسقة
هناك طريقتان للتعامل مع الملفات هما سيال البيانات Streams وواصف الملف
File Descriptor الأولى هي طريقة أسهل وأكثر مرونة
والأخرى طريقة ذات مستوى منخفض ومباشرة وفي الغالب
تكون مفضلة على الأولى عند التعامل مع ملفات غير عادية (مثل الأنابيب والأجهزة)
ولكن الأولى يمكنها التعامل مع الملفات
بطريقة منسقة أكثر. للتعامل مع الملف في الطريقتين
نقوم أولاً بإخبار المكتبة وبالتالي نظام التشغيل بأننا نريد
التعامل مع الملف الفلاني وبالطريقة الفلانية(قراءة فقط أو قراءة وكتابة .. إلخ)
فيرد بالرفض لأن الملف غير موجود أو لأننا لا نملك الحق في الملف الفلاني
،فنقوم بمعاجة هذه الحالة (بالخروج أو بإعادة المحاولة ) ،
أو يرد بأن ذلك ممكن ويعيد لنا متغير من نوع معين
ليمثل الملف وحالته بدل التعامل من خلال اسمه على شكل سلسلة فيما بعد،
تسمى هذه العملية فتح الملف .
وثم نستخدم هذا المتغير للكتابة في الملف أو القراءة منه
وعند الإنتهاء نقوم بإغلاق الملف ومعنى ذلك
هو انهاء العمليات المطلوبة التي كانت مؤجلة وتحرير كل المصادر التي حجزت
للقيام بها فهناك حد معين من الملفات التي يمكن
فتحها في وقت واحد ونعمل ذلك لتجنب الوصول لهذا الحد.
في الطريقة الأولى وهي سيال البيانات Streams
حيث جاء المصطلح من تخيل الملف وكأنه
تيار من البيانات تستطيع أن تأخذ منه أي تقرأ Read
أو تطرح فيه أي تكتب Write
ويمكن لهذه الكتابة أن تكون منسقة أي اكتب رقم بعرض كذا وبه كذا منزلة عشرية
ويمكن أن تكون مباشرة مثل انقل كذا بايت مما هو موجود في العنوان
التالي من الذاكرة وكذلك القراءة
لفتح ملف بهذا الأسلوب نستخدم الوظائف المعرفة في stdio.h
وأولها fopen
التي تأخذ معاملين أولهما سلسلة نصية تمثل اسم الملف
وثانيهما سلسلة نصية تمثل طور فتح الملف(للقراءة/للكتابة)
كما يلي
| السلسلة التي تمثل الطور | معناها |
| r | فتح ملف موجود أصلاً للقراءة |
| w | إنشاء ملف جديد أو مسح القديم والكتابة فوقه |
| a | فتح ملف للإضافة append إلى آخره أو انشاؤه إن لم يكن موجود |
| r+ | فتح ملف موجود أصلاً للقراءة والكتابة |
| w+ | فتح للقراءة والكتابة وإنشائه إن لم يكن موجود |
| a+ | فتح للقراءة والإضافة وإنشائه إن لم يكن موجود القراءة من أي مكان والإضافة إلى النهاية |
تحذير
هناك علامة أخرى للطور هي إما t أو b
الأولى هي للملفات النصية text والثانية للثنائية binary
وفي الحقيقة في أنظمة يونكس لا معنى لها وتهمل
فكلها ملفات، ولكنها مهمة بالنسبة لنظام دوس ووريث أخطاؤه ويندوز
فهناك خطأ تاريخي في طريقة تعاملهما مع السطر الجديد
حيث يكون السطر الجديد في الملف النصي "\n\r"
ولكنه يحمل في الذاكرة "\n"
تخيل الآلة الكاتبة(وليس الطابعة) عندما تضغط إدخال
فإن عربة الكتابة تنزل سطر جديد line feed(LF) أي "\n"
ثم تعود العربة لبداية السطر courage return(CR) اي "\r"
ففيهما لا معنى لسطر جديد دون عودة
لهذا إذا فتحت ملف ثنائي(ملف صورة أو تنفيذي) في الطور النصي
فإن كل رموز "\n\r" تصبح "\n"
ولكن هذه الرموز في هكذا ملفات لا تمثل سطر جديد ولا عودة
بل ربما تعليمة للمعالج أو نقطة أو لون مما يسبب مشاكل.
في يونكس نحن نتجنب هذا الصداع فالملف ليس آلة كاتبة
السطر الجديد "\n" هو سطر جديد
وعند عرضه على طابعة أو شاشة ينزل سطر ويعود للبداية
أما "\r" فهي تعيد المؤشر
لبداية السطر الحالي من أجل غايات المسح والكتابة فوقه.
بكلمات أخرى أنه في الحالتين لا يقوم بإضاعة وقته في عمليات تحويل
على أي حال يمكنك أن تضع العلامة t أو b في أي مكان
من نص الطور مثلاً wt+ أو r+b
وتعيد الوظيفة مؤشر تحجزه هي من نوع FILE بحروف كبيرة
هذا مثال
FILE *myfile;
myfile=fopen("readme.txt","r");
ولإغلاق الملف نقوم باستدعاء fclose
وتمرير متغير الملف مثلاً fclose(myfile)
لقراءة بايت من الملف fgetc ولكتابة بايت
fputc في الملف ولكتابة سلسلة نصية fputs
ولقراءة سلسلة نصية(سطر كامل) fgets
وهذه الأخيرة تأخذ مؤشر على السلسلة والحد الأقصى لطولها والملف
ولكن هذه تعمل من الموقع الحالي وهي غير منسقة
لاستعمال التنسيق نستعمل fprintf وربما تكون قد خمنت
أنها تأخذ معاملاً أضافيا(يكون المعامل الأول هو الملف) والباقي نفس
printf التقليدية ولكن بدل من الظهور على الشاشة
تكتب في الملف
مثلاً fprintf(myfile,"You have $%.2f in your credit.\n",c);
وللقراءة نستعمل fscanf التي طبعا ولا داعي للذكر
لا تختلف عن scanf إلا أن الأولى تقرأ من الموقع الحالي
من الملف بدلاً من جهاز الدخل القياسي
وهي تأخذ كما هو متوقع الملف والتنسيق ومؤشرات على المتغيرات
مثلاً fscanf(myfile,"%d",&i);
من الشائع استخدام fflush مع تمرير متغير الملف
إليها لتقوم باهمال بقية الدخل مثلاً لنفرض أنك
استعملت scanf("%d",&i); لقراءة قيمة i (من لوحة المفاتيح)
فقام المستخدم بإدخال رقمين عندها سيمرر الرقم الآخر
إلى الاستدعاء التالي لوظائف القراءة مثل scanf
وهذا جيد حيث يمكنك سؤال المستخدم ليدخل رقمين
ب scanf("%d %d",&i,&j); أو
scanf("%d",&i); scanf("%d",&j);
ولكن أحياناً تريد اهمال كل باقي الدخل مثلاً في برنامج حساس يسأل "هل أنت متأكد؟" فضغط المستخدم y مرتين
بالخطأ هنا لأن المسألة حساسة لا نريد من "y" الثانية أن تحسب لجواب السؤال التالي
فنستعمل fflush في حالتنا الملف هو جهاز الدخل القياسي
نستدع fflush(stdin);
تلميح
هناك ثلاث ملفات مفتوحة دائماً هي stdin و stdout و stderr
أي الدخل القياسي والخرج القياسي والخطأ القياسي
وهي متغيرات من نوع مؤشرات إلى FILE
مثلاً يمكنك كتابة fgetc(stdin) و fprintf(stderr,"Error")
هذه نماذج الوظائف المذكورة
FILE* fopen (const char* FileName, const char* Mode);
int fflush (FILE* file);
int fclose (FILE* file);
FILE* tmpfile ();
int fprintf (FILE* file, const char* Format, ...);
int printf (const char* Format, ...);
int sprintf (char* str, const char* Format, ...);
int snprintf (char* caBuffer, size_t n, const char* Format, ...);
int vprintf (const char* Format, va_list varg);
int vsprintf (char* str, const char* Format, va_list varg);
int vsnprintf (char* str, size_t n, const char* Format, va_list varg);
int fscanf (FILE* file, const char* Format, ...);
int scanf (const char* Format, ...);
int sscanf (const char* str, const char* Format, ...);
int fgetc (FILE* file);
char* fgets (char* str, int Size, FILE* file);
int fputc (int c, FILE* file);
int fputs (const char* str, FILE* file);
int ungetc (int c, FILE* file);
7.2.10 التعامل مع الملفات بطريقة مباشرة
أحياناً كما قلنا تريد حمل كذا بايت ووضعها في العنوان الفلاني عندها نستعمل
fread التي تأخذ مؤشر على الذاكرة
حجم الوحدة بالبايت وعدد الوحدات ثم متغير الملف
وتعيد الوظيفة عدد الوحدات التي تم قرأتها فعلاً
فإذا طلبت منه أن يقرأ عشر وحدات وأعاد خمسة هذا قد يعني
أنه وصل إلى نهاية الملف ولم يعد هناك أكثر من 5 وحدات أو
حدوث خطأ (عطب) في وسيط التخزين
يمكنك أن تفحص الخطأ باستعمال ferror وتمرير متغير الملف له
و تعلمه أنك عملت الخطأ ب clearerr.
عند قراءة جزء من الملف بأي طريقة فإن مؤشر الموقع يتحرك للأمام
بمقدار القراءة فتكون القراءة التالية للوحدة التي تليه
ولمعرفة أنك وصلت إلى آخر الملف وقرأة
كل ما به استدع feof
مع تمرير متغير الملف لها وتعيد قيمة تكون صحيحة عند الوصول إلى نهاية الملف
يمكن استعمالها لقراءة ومعالجة الملف بأكمله
#define DO_AT_ONCE 0xFFFFul
FILE *f1=fopen("my_file","rb");
char *block=(char *)malloc(0xFFFFul);
size_t n;
if (!f1||!block) exit(errno);
printf("reading 1st block ...\n");
while( !feof(f1) ) {
n=fread(block,1,DO_AT_ONCE,f1)
printf("\tprocessing it (%d bytes)\n",n);
my_process(n,block);
printf("reading next block ...\n");
}
fclose(f1);
printf("done\n");
أو يمكن استعمال الرقم الذي تعيده fread
#define DO_AT_ONCE 0xFFFFul
FILE *f1=fopen("my_file","rb");
char *block=(char *)malloc(0xFFFFul);
size_t n;
if (!f1||!block) exit(errno);
printf("reading 1st block ...\n");
while( (n=fread(block,1,DO_AT_ONCE,f1))!=0 ) {
printf("\tprocessing it (%d bytes)\n",n);
my_process(n,block);
printf("reading next block ...\n");
}
fclose(f1);
printf("done\n");
وبطريقة مشابهة يمكنك الكتابة باستعمال الوظيفة fwrite
التي لا تختلف صيغتها عن fread
ويمكن القراءة أو الكتابة من أي موقع فليس بالضرورة أن يكون الموقع
التالية لأنه من الممكن القفز إلى أي موقع من الملف والعمل فيه
باستدعاء fseek أو fseeko
الأخيرة ليست جزء من معايير ANSI ولكنها من معايير POSIX
والفرق أن الأخيرة تستعمل متغير من نوع off_t وليس int
لتمثيل الإزاحة لمزيد من الموثوقية لأن الأنظمة تختلف فيما بينها
في قدرتها. نمرر لهما متغير الملف ثم الإزاحة ثم نحدد من
أين تكون هذه الإزاحة: بداية الملف SEEK_SET أو
الموقع الحالي SEEK_CUR أو
الإزاحة عن ما قبل نهاية الملف SEEK_END
ولمعرفة الموقع الحالي أي الإزاحة عن بداية الملف
نستدع ftell أو ftello
مع تمرير الملف لها. ويجب أن أذكر بأنه يفترض عدم
استدعاء هذه الوظائف في الملفات التي تفتح على أنها نصية
في بعض أنظمة التشغيل (تعرفون ..) انظر التحذير السابق
ولكنها ستنجح في أنظمة غنو. إذا كنت تريد من برنامج العمل
في أنظمة غير غنو وتصر على فتح الملف نصياً هناك
وظائف تصلح وهي fsetpos و fgetpos
ولكن الأسهل هو فتح الملف في الطور الثنائي.
نماذج الوظائف المذكورة هي
size_t fread (void* Array, size_t ObjectSize, size_t Count,FILE* file);
size_t fwrite (const void* Array, size_t ObjectSize, size_t Count,FILE* file);
int fseek (FILE* file, long Offset, int from);
long ftell (FILE* file);
void rewind (FILE* file);
int feof (FILE* file);
int ferror (FILE* file);
void clearerr (FILE* file);
تلميح
إن الوظائف التي شرحناها سابقاً تصلح لملفات لا يزيد حجمها عن 2 أو 4 غيغا بايت
وللعمل بالملفات الكبيرة LFS أي Larg File Systems
هناك طريقتين الأولى هي استعمال الوظائف الخاصة بالملفات الكبيرة
وهي نفس أسماء الوظائف السابقة عدا أنها تنتهي ب 64
مثلاً بديل fopen هو fopen64
وهكذا . أما الطريقة الثانية وهي المفضلة
استعمال الوظائف العادية مع تحديد
_FILE_OFFSET_BITS
لتصبح قيمتها 64
عند تصنيف البرنامج بمصنفات غنو كما يلي
gcc -d_FILE_OFFSET_BITS=64 myfile.c -o myfile
قد يصل الحد الأقصى لحجم الملف 2^63=9x1018
خصوصاً عند استعمال الطريقة الأولية اللاحقة
7.2.11 الطريقة الأولية في التعامل مع الملفات
تحت الطرق السابقة المرنة ذات المستوى الراقي
توجد طريقة أولية تسمى File Descriptor
وهو عبارة عن رقم فريد تخصصه النواة ليصف الملف معين
الوظائف الخاصة به معرفة في ملف unistd.h
و fnctl.h وإذا لم يتوفرا ربما تجد io.h
لفتح ملف نستدع open ونمرر لها اسم
الملف وبعض العلامات لتحديد طور فتح الملف ويمكن إضافة
معامل ثالث اختياري هو التراخيص في حالة إنشاء ملف جديد
مثل 0755 الصفر للثماني
وتعيد الوظيفة رقم صحيح غير سالب في حال النجاح يسمى واصف الملف
File Descriptor وفي حال الفشل سالب
هو في الغالب سالب واحد ويتم تحديد errno
أما علامات الطور فهي واحدة أو أكثر من التالية ( ضع | بينها)
| O_RDONLY | السماح بالقراءة |
| O_WRONLY | السماح بالكتابة |
| O_RDWR | السماح بالقراءة والكتابة |
| O_CREATE | إنشاء الملف إن لم يكن موجوداً |
| O_EXCL | فتح ملف موجود مسبقاً |
| O_TRUNC | حذف كل بيانات الملف ليصبح حجمه صفر يفضل استعمال ftruncate |
| O_APPEND | مؤشر الكتابة يشير إلى نهاية الملف |
| O_TEMPORARY | يتم حذف الملف عند إغلاقه |
وهذه مجموعة منها غير موجودة في ويندوز وبعضها موجودة فقط في غنو
| O_NONBLOCK | عدم الإنتظار حتى تتم العملية (مثلاً عند فتح ملف جهاز) |
| O_FSYNC | علامة التزامن؛ عن وظائف الكتابة لا تعود حتى تكتب البيانات على القرص |
| O_NOLINK | فتح الوصلة وليس ما تشير إليه |
| O_NOTRANS | فتح الملف مع تخطي مترجم الملف أي كما يراه المترجم |
في حالة استعمال O_NONBLOCK عمليات الكتابة والقراءة
تعتبر فاشلة إذا لم تتم مباشرة ،
يمكن تغيير بعض هذه العلامات باستدعاء fcntl
(وهي تشبه ioctl الخاصة بنواة النظام ولكن fcntl جزء من مكتبة سي القياسية)
مثلاً fcntl(fd,F_SETFL,my_new_flag);
ويمكن معرفة العلامات الحالية ب
fcntl(fd,F_GETFL,0); وتعيد الوظيفة قيمة غير سالبة
في حال النجاح حيث fd هو الرقم الذي يصف الملف،
هاتان الوظيفتان واحدة تضع العلامة O_NONBLOCK وأخرى تزيلها
int set_nonblock_flag (int desc) {
int oldflags = fcntl (desc, F_GETFL, 0);
/* If reading the flags failed, return error indication now. */
if (oldflags == -1) return -1;
oldflags |= O_NONBLOCK;
return fcntl (desc, F_SETFL, oldflags);
}
int unset_nonblock_flag (int desc) {
int oldflags = fcntl (desc, F_GETFL, 0);
/* If reading the flags failed, return error indication now. */
if (oldflags == -1) return -1;
oldflags &= ~O_NONBLOCK;
/* Store modified flag word in the descriptor. */
return fcntl (desc, F_SETFL, oldflags);
}
ويمكن استعمال fcntl
لوضع أقفال على قراءة أو كتابة الملف
مثلاً قفل الكتابة تحدد فيه أنك تريد الكتابة
ويجب أن لا تقرأ البرامج من الملف لأنه سيتغير
أما قفل القراءة فهو يحدد بأن البرامج الأخرى يفترض
أن لا تكتب في الملف حتى تتمكن الأولى من قراءته ولكن
هذه أقفال غير إلزامية والبرامج ليست مجبرة على
احترمها (مثل قوانين ال UN) لهذا لن أتحدث عنها أكثر.
بعد فتح الملف نستعمل read وتمرير رقم وصف الملف
ومؤشر على منطقة من الذاكرة والحجم
وتعيد read رقم يمثل المقدار الفعلي الذي
تم قراءته . ونستعمل write وتمرير رقم وصف الملف ومؤشر
على منطقة من الذاكرة والحجم وتعيد المقدار الفعلي الذي تم كتابته
ونستعمل lseek التي لها نفس صيغة
fseek غير أن معاملها الأول هو رقم وصف الملف
لتحويل ملف مفتوح بطريقة stream إلى File Descriptor نستدع
fileno ونمرر لها مؤشر لمتغير الملف
فتعيد رقم وصف الملف
وللقيام بالعملية العكسية نستعمل fdopen
التي تأخذ معاملين هما رقم وصف الملف و سلسلة نصية تمثل الطور بنفس طريقة fopen
وتعيد مؤشر لمتغير الملف من نوع FILE
من الوظائف الشائعة الإستخدام هي نسخ واصف ملف بواصف جديد
في عملية تسمى مضاعفة/نسخ duplicate حيث
يحل يمكن استعمال أي منها للقيام بعمليات القراءة والكتابة
باستدعاء dub(oldfd) حيث تعيد رقم واصف ملف جديد
وإذا كنت تريد تحديد الواصف الجديد أيضاً
dub2(oldfd,newfd) حيث تغلق الجديد ويحل مكانه القديم
وقد تبدو هذه العملية بلا فائدة
ولكن فائدتها تكم في استبدال الدخل القياسي (الرقم الواصف له
STDIN_FILENO ) بأي ملف آخر
/* redirect.c: 'echo "hello" > readme.txt' */
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main() {
int fd;
fd=open("readme.txt",O_WRONLY|O_CREATE);
if (fd==-1) exit(1);
dup2(fd,STDOUT_FILENO);
printf("hello\n");
close(fd);
return 0;
}
7.2.12 انتظار مدخلات ومخرجات من الملفات
في الحالات العادية عند القراءة من ملفات مفتوحة دون العلامة
O_NONBLOCK
فإن عملية القراءة تنتظر توفر المدخلات من الأجهزة
(أو توفر علامة بالفشل)
مثلاً عند القراءة من الدخل القياسي فإن البرنامج يتوقف
حتى يدخل المستخدم عبر لوحة المفاتيح الكمية المطلوبة
ولكن في حالة O_NONBLOCK فإن عمليات
القراءة يجب أن تكون بعد توفر المدخلات وإلا فإنها تفشل.
حالة أخرى؛ لنفرض أنك فتحت أكثر من ملف دون O_NONBLOCK وتريد
القراءة من أي واحد منها تتوفر منه مدخلات فإن محاولة
القراءة من ملف لم تتوفر مدخلات منه قبل آخر متوفرة
فإن المحاولة ستظل معلقة حتى يتوفر مدخلات من الأول
وتظل المدخلات القادمة من الثاني تنتظر.
لحل هاتان المشكلتان نستعمل الوظيفة select
(وأيضاً لها فوائد أخرى في الشبكات كما سنرى)
هذه الوظيفة تأخذ عدد من الواصفات fd على شكل مجموعة التي تريد إنتظار جاهزيتها
خلال فترة من الزمن.
هذه المجموعة من نوع fd_set
مرر مؤشر على المجموعة إلى FD_ZERO لجعلها مجموعة خالية استهلالاً.
ثم أضف الواصفات fd التي تريد إليها باستدعاء
FD_SET المعامل الأول هو الواصف fd والثاني هو مؤشر على المجموعة.
نموذج الوظيفة select هو كما يلي:
int select (int N, fd_set *r-fds, fd_set *w-fds, fd_set *except-fds, struct timeval *timeout);
حيث N هي الحد الأعلى للعدد الواصفات في المجموعة يمكك أن تجعله
FD_SETSIZE وهو أكبر عدد مسموح به.
أما r-fds و w-fds هو مجموعة الواصفات
التي تريد إنتظار جاهزيتها للقراءة والكتابة (على الترتيب) ،
أما except-fds فهي مجموعة الواصفات التي تريد فحص حالات استثائية.
يمكنك وضع NULL مكان أي منها لإهماله.
و timeout هو الفترة الزمنية التي يفترض أن ينتظرها كحد أقصى لحدوث الجاهزية
يمكنك وضع NULL لزمن غير محدود.
تعيد الوظيفة عدد الواصفات الجاهزة. تقوم الوظيفة بتعديل
المجموعة لتحتوي الواصفات الجاهزة فإذا كنت تريد الأصلية
اعمل منها نسخة ومرر النسخة.
يمكنك أن تعرف إن كان الواصف الفلاني في المجموعة
أم لا نستعمل FD_ISSET أول معامل لها هو الواصف والثاني
مؤشر على مجموعة.
هذا مثال يوضح استخدام تقليدي لهذه الوظيفة.
حيث fd1 و fd2
واصفات الملفات التي نريد انتظارها للقراءة
fd_set orig_set, temp_set;
FD_ZERO (orig_set);
FD_SET (fd1, orig_set);
FD_SET (fd2, orig_set);
temp_set=orig_set;
if (select (FD_SETSIZE, temp_set, NULL, NULL, NULL)<0) exit(1);
for (i = 0; i < FD_SETSIZE; ++i) {
if (FD_ISSET (i, &temp_set)) {
printf("we have some thing on fd=%d\n",i);
read(i,&c,1);
}
}
7.2.13 التعابير العادية Regular Expretions
التعابير العادية هي من أقوى الطرق المستخدمة للتعامل مع النصوص
بحثاً وإبدالاً ومبدأ عمل المصنفات تقوم عليها
لهذا ليس غريباً أن تسمى عملية توليدها من النصوص تصنيفاً compiling،
نعم فالتعابير العادية لا تخزن كنصوص يتم إعرابها وتحليلها في كل مرة
بل تخزن على شكل تركيب من نوع regex_t
الوظيفة التي تحول النص إلى هذا النوع هي regcomp
ونمرر لها مؤشر على تركيب التعبير regex_t
ثم سلسلة نصية ثم علامات تحدد بعض الخيارات وتعيد الوظيفة صفر في حال النجاح
وهي موجودة في regex.h
العلامات هي (استعمل "|" للجمع بين أكثر من واحدة)
| REG_EXTENDED | قبول التعابير الإضافية |
| REG_ICASE | إهمال حالة الحروف (الإنجليزية) إن كبيرة أو صغيرة |
| REG_NOSUB | عدم توليد السلاسل الفرعية(تلك المحصورة بين أقواس هلالية) |
| REG_NEWLINE | معاملة السطر الجديد"\n" كحالة خاصة |
مثلاً يمكن عمل ذلك كما يلي
regex_t rg1;
regcomp(&rg1,"^hd[a-d][0-9]\\{0,3\\}",REG_EXTENDED);
بعد تصنيفه يمكننا مطابقته في عملية تسمى تنفيذ التعبير
باستدعاء الوظيفة regexec
أبسط المعاملات هي بأن تأخذ مؤشر على التعبير المصنف أي regex_t *
ومؤشر على النص المراد مطابقته ثم 0 ثم NULL ثم بعض
العلامات هي REG_NOTBOL أي لا تفترض أن بداية النص هي بداية سطر
أي أن هذا النص قد يكون مجتزأ أو العلامة REG_NOTEOL
أي لا تفترض أن نهاية النص هي نهاية سطر لأنه قد يكون مجتزأ
أو ضع صفر
regex_t rg1;
char *pat1="^hd[a-d][0-9]\\{0,3\\}",*str1="hdc12";
regcomp(&rg1,pat1,REG_EXTENDED);
if (regexec(&rg1,str1,0,NULL,0)==0) {
printf("%s Matches %s",str1,pat1);
} else {
printf("No Match");
}
regfree(&rg1);
ولكن يمكن للتعابير العادية أن توفر لنا وسيلة
أكثر مرونة باستعمال التركيب regmatch_t
حيث يمكننا أن نعرف أي حصلت المطابقة
الذي عنصره الأول وهو rm_so يمثل إزاحة بداية
المطابفة و rm_eo الذي هو إزاحة نهاية المطابقة
ولمعرفة تلك المعلومات احجز منظومة من نوع regmatch_t
وعدد عناصرها يزيد بواحد عن الحد الأعلى للتعابير الفرعية التي تريد
وتمريرها مكان NULL وتمرير ذلك العدد مكان الصفر في regexec كما يلي
عندما تعود يكون العنصر الأول من المنظومة (رقم صفر) يمثل
المطابقة الكلية أما التاليات فتكون للفرعايات
regex_t rg1;
char *pat1="c(o\\{2,\\})l",*str1="This is a coooler!";
regmatch_t rm[2];
regcomp(&rg1,pat1,REG_EXTENDED|REG_ICASE);
if (regexec(&rg1,str1,2,rm1,0)==0) {
printf("'%s' Matches %s",str1,pat1);
printf("match happen from %d to %d",rm[0].rm_so,rm[0].rm_eo);
printf("the submatch from %d to %d",rm[1].rm_so,rm[1].rm_eo);
} else {
printf("No Match");
}
regfree(&rg1);
كما لاحظت بعد الإنتهاء من العمليات نستدع regfree
لتحرير الذاكرة التي تحجزها regcomp
تحذير
الكثير من الوظائف أدناه موجودة حصراً على أنظمة يونكس ، وربما لن تجدها في المصنفات
غير القياسية مثل (...) تعرفون من أقصد. هذا على الرغم من أنها ووظائف قياسية
ضمن معايير POSIX.
7.2.14 اللغات العالمية والاعدادات المحلية
تتم ترجمة البرامج المفتوحة المصدر
باستعمال gettext وهي أداة ووظيفة ومكتبة من غنو
حتى تستفيد منها يجب أن تستعمل #include<libintl.h>
عليك تحويل كل النصوص في برنامج باستدعاءات ل gettext
وتمرير النص المراد ترجمته سيكون هذا النص هو مفتاح الترجمة
وتستخدم غنو طرق تحسين تجعل من هذا المفتاح النصي
بسرعة المفتاح الرقمي إذ أن ملف الترجمة يكون مع بطريقة
توفر البحث فيه . هذا مثال لبرنامج بسيط
/* hello-i18n.c: International "Hello, world!" */
#include<stdio.h>
#include<libintl.h>
int main() {
setlocale (LC_ALL,""); /* change from LC_ALL=C to default locale*/
textdomain("hello-i18n"); /* load file hello-i18n.mo */
/* you may also use /usr/local/share/locale */
bindtextdomain ("test-package","/usr/share/locale");
printf("%s\n",gettext("Hello, world!"));
return 0;
}
هذا المثال يتوقع وجود ملف
/usr/share/locale/XY/LC_MESSAGES/hello-i18n.mo
حيث XY هي اللغة التي سيترجم لها
وهذا الملف يتم توليده من ملف hello-i18n.po
وهذا الأخير ملف نصي يحتوي الأصل والترجمة كما يلي
# hello-i18n.po: translation for hello-i18n.c
# this is a comment
msgid "Hello, world!"
msgstr "مرحباً، يا عالم!"
ولتحويل هذا الملف إلى .mo نستعمل الأداة
msgfmt كما يلي
msgfmt hello-i18n.po -o hello-i18n.mo
ثم انقل هذا الملف إلى المكان المناسب
ويمكن كتابة ملف po يحتوي النص المراد ترجمته
بأي محرر نصوص يدوياً أو توليده تلقائياً من ملف السي
بواسطة الأداة xgettext
7.2.15 الأدلة والملفات
لمعرفة الدليل الحالي نستدع getcwd مع تلرير مؤشر لموقع من الذاكرة
والحد الأقصى لطوله إذا لم تكن تريد التحديد استدع getcwd(NULL,0)
التي تحجز ذاكرة ب malloc وتخزن فيها الدليل الحالي
وعليك تحرير الذاكرة بنفسك.
تعيد هذه الوظيفة مؤشر على اسم الدليل الحالي أو NULL .
يقابل هذه الوظيفة chdir التي تقوم بتغير الدليل الحالي
إلى الدليل الذي تمرره له تعيد هذه الوظيفة صفر في حال
النجاح وسالب في حال الفشل.
لمعرفة محتويات دليل ربما تفضل استعمال glib لأنها توفر طيف أكبر من الأنظمة.
على أي حال، توفر مكتبة سي القياسية هذه الوظائف
باستعمال opendir مع تمرير اسم الدليل الذي تريد معرفة
محتوياته تعيد هذه الوظيفة مؤشر الدليل DIR *
أو NULL من الطرق الشائعة لاستدعاء هذه الوظيفة
opendir("./") لمعرفة محتويات الدليل الحالي.
لمعرفة الملفات واحداً تلو واحد نستدع
readdir مرة بعد أخرة،نمرر مؤشر الدليل له
تعيد هذه الوظيفة مؤشر من نوع عنصر من الدليل dirent
أو NULL إذا تم قراءة كل شيء ، المؤشر الذي تعيده
تحجزه الوظيفة وهي مسؤولة عن تحريره استدعاؤها مرة
أخرى لنفس مؤشر الدليل قد يكتب فوق المؤشر السابق.
يتحوي التركيب dirent الذي تعيده هذه
الوظيفة المعلومات الأساسية عن الملفات مثل
d_name التي هي اسم الملف ومنها أيضاً
d_type التي تحدد نوع الملف (عادي أو دليل أو جهاز)
لإعادة قراءة الملفات من البداية نستعمل rewinddir
مع تمرير مؤشر الدليل. هذه الوظيفة لا تعيد شيء.
عند الإنتهاء تقوم بإغلاق مؤشر الدليل ب closedir مع
تمرير المؤشر له. هذا مثال يقوم بسرد محتويات الدليل الحالي
#include<stdio.h>
#include<dirent.h>
#incltde<unistd.h>
int main() {
DIR *dp;
dirent *entry;
if (!dp=opendir("./")) {
perror("Could not open director.");
return 1;
}
while(entry=readdir(dp)) {
printf("./%s%s\n",entry->d_name,
(entry->d_type==D_DIR)?"/":NULL);
}
closedir(dp);
return 0;
}
7.2.16 تفريع برامج
يمكن للبرامج استدعاء برامج أخرى
في هذه الحالة نسمي البرنامج الذي تم استدعاؤه بالبرنامج الإبن
والذي استدعاه الأب مكونين شجرة كبيرة مثلاً
عند تشغيل الجهاز تعمل النواة على استدعاء برنامج واحد هو init
وباقي البرامج تكون أبناء وأحفاد له
يمكن للبرنامج الأب أن ينتظر انتهاء البرنامج
الإبن أو متابعة العمل ويمكنه كذلك التواصل معه.
قبل استدعاء برنامج عليك أولاً التفريع fork
وذلك باستدعاء وظيفة بهذا الاسم دون معاملات
تعيد قيمة سالبة (سالب واحد) في حالة الفشل،
وفي حالة النجاح تقوم هذه الوظيفة بعمل نسخة من البرنامج
الحالي على أنها إبنة البرنامج الذي طلب التفريع
وتعيد الرقم المعرف للإبن PID في النسخة الأب من البرنامج
وتعيد صفر في النسخة البنت من البرنامج
وتتابعا (أي النسختين) العمل من المنطقة التي تلي fork .
/* self-fork.c: a dummy fork */
#include<stdio.h>
#include<unistd.h>
int main() {
pid_t p=fork();
switch(p) {
case 0:
printf("I'm the child process\n");
printf("My PID is %d, My parent PID is %d\n",getpid(),getppid());
break;
case -1:
perror("Can't fork"); exit(0);
default:
printf("I'm the parent process\n");
printf("My child PID is %d \n",p);
printf("wait it to end\n",p);
waitpid(p,NULL,0);
printf("the child ended\n",p);
}
}
وربما تتسائل ما فائدة تشعيب البرنامج واستدعائه لنسخة منه.
في الحقيقة لا يوجد فائدة! ولكن بعد التفريع تقوم العملية
البنت باستدعاء برنامج ليحل مكانها باستعمال أحد وظائف exec وأخواتها
تختلف في ما بينها في طريقة تمرير المعاملات
أو البحث عن البرنامج المراد تنفيذه
| execv | تمرر المعاملات على شكل منظومة |
| execl | تكون معاملات الوظيفة الإضافية هي معاملات البرنامج |
| execve | تمرير متغيرات البيئة إضافة إلى execv |
| execle | تمرير متغيرات البيئة قبل المعاملات كما في execl |
| execvp | كما فيexecv ولكن يتم البحث عن البرنامج في المسارات PATH |
| execlp | كما فيexecl ولكن يتم البحث عن البرنامج في المسارات PATH |
هذه الوظائف لا تعود في حالة النجاح لأنها تستبدل البرنامج
الحالي ببرنامج آخر ولكنها تعود في حال الفشل
وظيفة execl وأخواتها التي تأخذ المعاملات من معاملات الوظيفة
يجب لأن تنتهي ب NULL
/* run-ls.c: a program calling 'ls' */
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<unistd.h>
int main() {
pid_t p=fork();
int status;
switch(p) {
case 0:
printf("I'm the child process\nI'm going to call 'ls'\n");
execlp("ls","ls","-F",NULL); /* running 'ls -F' */
perror("Couldn't run 'ls -F'");
exit(errno);
case -1:
perror("Can't fork"); exit(0);
default:
printf("I'm the parent process\n");
printf("My child PID is %d \n",p);
printf("wait it to end\n");
waitpid(p,&status,0);
printf("the child ended with value %d\n",status);
}
}
يمكن اختصار العملية بالوظيفة system المعرفة في stdlib.h
والتي يمكن استعمالها في معظم أنظمة التشغيل
تأخذ هذه الوظيفة سلسلة نصية(البرنامج ومعاملاته) وتنفذها وكأنها طبعت في محث الأوامر sh
(في ويندوز و دوس وكأنها كتبت في command.com محث دوس)
وتقوم هذه الوظيفة بالتشعيب والبحث عن البرامج المطلوب وتنفيذه وانتظار انتهاؤه
ولكن الوظائف السابقة تعطيك تحكم أكبر فليس بالضرورة أن تنتظر انتهاؤه
ولكتابة برنامج للإستعملات الحساسة والآمنة
يجب تجنب استعمال system
وتجنب execvp و execlp لأنها تبحث عن البرنامج
في متغير البيئة PATH الذي يمكن تغيره بحيث ينفذ برنامج
آخر وعند الحاجة استعمال execv أو execl
مع تحديد موقع البرنامج المطلق مثلاً /bin/ls
أو نسبة لمكان وجود البرنامج مثلاً برنامج /usr/bin/myprog
يريد تنفيذ البرنامج /usr/lib/myprog/mysub
يمكن معرفة المسار باستدعاء
sprintf(filename,"%s/../lib/myprog/mysub",dirname(arg[0]));
التي تنتج /usr/bin/../lib/myprog/mysub
التي تكافئ المطلوب فإذا كانت توزيعة قد غيرت سابقة البرنامج
من /usr إلى /usr/X11R6
فأصبح البرنامج الأب /usr/X11R6/bin/myprog
وفرعه /usr/X11R6/lib/myprog/mysub
فإن الحيلة السابقة تنجح أيضا في معرفة ذلك.
هذا مثال يوضح استعمال system
/* run-sh-ls.c: a program calling a shell to run'ls' */
#include<stdio.h>
#include<stdlib.h>
int main() {
int s;
printf("I'm the parent process\n");
s=system("ls -F");
printf("the child ended with value %d\n",s);
}
7.2.17 التخاطب بين العمليات/البرامج
هناك عدة طرق للتخاطب بين العمليات IPC أي Inter-Process Communication
منها
- fork/pipe - التفريع فالأنبوب
- ملفات الأنابيب FIFO الخاصة وتسمى named pipe
- IPC المعيارية الخاصة بالنظام الخامس SysV
- message queues
- semaphores
- shared memory objects
- socket سواء المحلية أم عبر شبكات
نستخدمها باستثناء الأولى في عمل علاقة خادم/مخدوم
فالأولى لا تحتاج تصميم برنامج بطريقة معينة
فأي برنامج دون إعادة كتابته يمكنه أن يرسل بيانات
أو يستقبلها عبر الأنابيب. مثلاً بكتابة في باش ls | tr 'A-Z' 'a-z'
الطريقة الثانية FIFO هي أسهل طريقة لعمل علاقة server/client
محلية. الثالثة تستفيد من نواة النظام المتوافق مع sysV.
أما الأخيرة فهي الطريقة المفضلة لعمل برامج الشبكات.
7.2.18 التخاطب بين البرامج عبر الأنابيب
أسهل طريقة لتنفيذ برنامج وارسال بيانات عبر أنبوب في أحد الإتجاهات
هي باستعمال popen وتمرير الأمر الذي تريد تنفيذه
عبر محث الأوامر sh ثم معاملاً آخر هو نص يحدد اتجاه
الأنبوب (أي هل تريد أن تقرأ منه"r" أم تكتب "w")
وتعيد الوظيفة مؤشر لمتغير ملف FILE * وكأنه فتح ب
fopen
/* run-sort.c: a program calling a shell to sort */
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main() {
FILE *f1;
printf("using '/bin/sort' to sort {camel,apple,zoo,foobar}\n");
f1=popen("/bin/sort","w");
if (!f1) exit(1);
printf("sending ... ");
fprintf(f1,"camel\napple\nzoo\nfoobar\n");
printf("OK.\nthe sorted list:\n"); pclose(f1);
return 0;
}
هذه الطريقة تستدعي محث الأوامر وتخفي الكثير من التعقيدات تحتها
فالطريقة الأولية تقوم على
- استدعاء pipe لعمل أنبوب بطرفين قراءة/كتابة
- استدعاء fork لتفريع البرنامج الإبن
- يغلق كل من الأب والإبن الطرف الذي لا يريده من الأنبوب ويستعمل الآخر
- يستبدل الإبن الدخل/الخرج القياسي بالأنبوب
- يستدع برنامج آخر بأحدى أخوات exec
وظيفة pipe تأخذ مؤشر على منظومة من عددين صحيحين
هما رقما وصف الملف الخاص بالقراءة والكتابة العائد على الأنبوب
int fd[2]; /* fd[0] for input and fd[1] for output */
if (pipe(fd)) perror("Can't create pipe");
يمكن أن نستعمل fdopen للتحويل من واصف الملف إلى متغير الملف
بحيث نستعمل وظائف مثل fprintf و fgets.
هذا برنامج يخاطب نفسه عبر أنبوب
/* self-pipe.c: a program talking to it self */
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main() {
FILE *f1,*f2;
char str[80];
int fd[2];
if (pipe(fd)) { perror("Can't create pipe"); exit(errno);}
pid_t p=fork();
switch(p) {
case 0:
printf("sun: I'm the child process\n");
close(fd[0]); /* close the reading so I can write*/
f1=fdopen(fd[1],"w");
printf("sun: Hello dad. can you hear me!!\n");
fprintf(f1,"Hello dad. can you hear me!!\n");
break;
case -1:
perror("Can't fork"); exit(0);
default:
printf("parent: I'm the parent process\n");
close(fd[1]); /* close the writting so I can read */
f2=fdopen(fd[0],"r");
printf("parent: I'm listening sun\n");
fgets(str,80,f2);
printf("parent: did you say '%s'?\n",str);
printf("wait it to end\n");
waitpid(p,&status,0);
printf("the child ended with value %d\n",status);
}
}
7.2.19 التخاطب عبر FIFO
شرحنا طريقة الأنبوب ولكن هذا الأنبوب كان بإتجاه واحد.
يمكننا غنو/لينكس (وأنظمة يونكس) من عمل ملف خاص نميزه
من خلال حرف p على يسار الأذونات وعلامة ‘|‘ على يمين الاسم
عند استعمال ls -lF
نعمل هكذا ملف بواسطة mkfifo أو mknod
bash# mknod /tmp/myfifo p
الآن عندما يقوم برنامج بالكتابة في هذا الملف لا ترسل
البيانات إلى الملف(على القرص) بل إلى البرنامج الذي يقراً
من هذا الملف.
يتم توقيف البرنامج الذي يكتب حتى يبدأ برنامج آخر بالقراءة
ويتم توقيف برنامج يحاول القراءة حتى يبدأ برنامج بالكتابة.
هذان مثالان. الأول خادم واللآخر مخدوم.
/* myfifo-serv.c - sample fifo testing server *
* run it in the background to have a server */
#include <stddef.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#define MYFIFO "/tmp/myfifo"
#define MY_MAX 180
int main() {
FILE *f;
char str[MY_MAX];
remove(MYFIFO);
if (mknod(MYFIFO,S_IFIFO|0666,0)) {
perror("Could not create file");
exit(1);
}
if (!f=fopen(MYFIFO,"r")) {
perror("Could not open file");
exit(1);
}
printf("myfifo-serv: waiting to recive data from clients\n");
printf("myfifo-serv: use myfifo-client to send data\n");
while(1) {
fgets(str,MY_MAX,f);
printf("myfifo-serv: string received [%s]\n",str);
}
}
/* myfifo-client.c - sample fifo testing client *
* run it as 'myfifo-client string' */
#include <stddef.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#define MYFIFO "/tmp/myfifo"
#define MY_MAX 180
int main(int argc,char argv[]) {
FILE *f;
stat mystat;
if (argc!=2) {
printf("myfifo-client: sample fifo testing client
syntax: myfifo-client STRGING
where STRING is the string to send to server.
example:
myfifo-client 'Hello every body'
");
exit(0);
}
if (stat(MYFIFO,&mystat)||!S_ISFIFO(mystat.st_mode)) {
perror("Server is not running");
exit(1);
}
printf("myfifo-client: sending [%s] to server\n",argv[1]);
if (!f=fopen(MYFIFO,"w")) {
perror("Could not open file");
exit(1);
}
fputs(argv[1],f);
fclose(f);
}
بعد التصنيف، جرب كتابة الأوامر
bash# myfifo-serv &
myfifo-serv: waiting to recive data from clients
myfifo-serv: use myfifo-client to send data
bash# myfifo-client "Hello, world!"
myfifo-client: sending [Hello, world!] to server
myfifo-serv: string received [Hello, world!]
يمكن التحكم في من يرسل ومن يستقبل بواسطة
الأذونات فالإذن 0666 يعني أن الكل، يمكن أن تجعلها 0624
أي من في المجموعة يسمح له بالكتابة وخارجها قراءة
أو 0642 للعكس. وعمل sgid لملف البرنامج الخادم
chmod 2755 myfifo-serv
وتعمل مجموعة myfifo وتغيّر مجموعة المالك للخادم.
يمكن عمل ملفين واحد بإتجاه (من الخادم) وآخر بإتجاه آخر(إلى الخادم).
يمكن تغيير السلوك التلقائي بأن ينتظر
البرنامج في غفوة حتى يتوفر قراءة/كتابة على الطرف الآخر بواسطة
الخيار O_NONBLOCK عند فتح الملف ب open
أو بتغيرها فيما بعد ب fcntl.
ونستعمل الوظيفة select من أجل الانتظار.
7.2.20 التخاطب عبر socket
قنواة socket نوعان: النوع الأول محلية تسمى local/file/unix socket
والثانية عبر الشبة وفق بروتوكولات الإنترنت IPv4 أو IPv6
هذا التصنيف وفق عائلة البرتوكول PF ففي
الأول يتم التخاطب بين برنامجين ويعتمد ملف على أنه العنوان
(كما في FIFO ولكن كل برنامج له ملف)
أما الثانية تعتمد عنوان IP مثل 172.12.0.1
الأمر الآخر هو طريقة الإرسال فهي إما سيال stream (طلب-موافقة-فتح قناة )
فالأول يتصل الثاني يستمع ثم يقرر فتح اتصال وتخصيص fd بعدها يتم الإتصال
بين الإثنين عبره وكأنه ملف تصل البيانات بنفس ترتيب الإرسال.
أما الثانية فهي إبراق البيانات Datagram حيث في كل
عملية من إرسال/استقبال يجب أن يحدد العنوان في هذا الأخير
قد تصل البرقية datagram عدة مرات أو قد تصل بترتيب غير الذي أرسلت به.
كما وعليك أن تحدد البرتوكول حسب طريقة الإتصال و عائلة البرتوكولات والأفضل أن تضع
0 لاختيار المناسب
الصيغة العامة لعمل socket هي
socket (PF, STYLE, PROTOCOL)
حيث PF هي
PF_LOCAL أو PF_INET أو PF_INET6
و STYLE هي
SOCK_STREAM أو SOCK_DGRAM
و PROTOCOL هي صفر.
تعيد هذه الوظيفة رقم صحيح موجب هو fd.
تلميح
PF_LOCAL له أسماء أخرى هي PF_UNIX و PF_FILE
هي فقط للتواصل عبر جهاز واحد. هذا لا يعني
أنه لا يمكن عمل اتصال بين برنامجين على جهاز واحد لا يحتوي جهاز اتصال
باستعمال PF_INET إذ يمكن استعمال العنوان المحلي127.0.0.1
فيما بعد نربط القنواة socket بعنوان
ثم نتصرف بطريقة مختلفة كخادم أو مخدوم .
لعمل برنامج وكيل/مخدوم client فإننا نقم بالخطوات بالشكل التالي:
- أولاً نستدع socket بالمعاملات المناسبة
وفق نوع الإتصال للحصول على رقم موجب هو واصف الملف fd.
- إذا كان الإتصال من نوع PF_LOCAL نحدد ملف ليمثل
عنواناً لبرنامجنا المخدوم client ونربطه مع الواصف باستدعاء bind
أما النوع الآخر (نوع IP) يتم تحديد عنوانك تلقائياً من عنوان الجهاز.
- حضر عنوان (والمنفذ في حالة IP) الذي تريد الإتصال معه .
- يتم الإرسال والاستقبال بحسب أسلوب الإتصال style كما يلي
- SOCK_DGRAM
-
نستعمل الوظائف sendto و recvfrom
التي تأخذ عنوان المرسل إليه والمستقبل منه في كل رسالة
- SOCK_STREAM
-
هنا نطلب إتصال باستدعاء connect على العنوان
الذي نريد أن تتصل به فإن وافق ونجحت يمكنك القراءة والكتابة
عبر الواصف بالوظائف read و write
و recv و send
أما عمل خادم فهو أكثر تعقيداً، كما يلي
- أولاً نستدع socket بالمعاملات المناسبة
وفق نوع الإتصال للحصول على رقم موجب هو واصف الملف fd.
- إذا كان الإتصال من نوع PF_LOCAL نحدد ملف ليمثل
عنواناً لبرنامجنا الخادم ونربطه مع الواصف باستدعاء bind.
أما النوع الآخر (نوع IP) نربطه ليستقبل من كل العناوين
INADDR_ANY أو من المحلية INADDR_LOOPBACK
وحدد منفذ port واربطها باستدعاء bind
- يتم الإرسال والاستقبال بحسب أسلوب الإتصال style كما يلي
- SOCK_DGRAM
-
نستعمل الوظائف sendto و recvfrom
التي تأخذ عنوان المرسل إليه والمستقبل منه في كل رسالة
- SOCK_STREAM
-
- انتظر طلب إتصال من المخدمات وذلك بواسطة listen
(تستطيع تحديد حجم الطابور لكن 1 تصلح)
- تصل الطلبات عبر fd ولكن الإتصال نفسه يتم عبر واصف آخر نحصل عليه من
استدعاء accept ويمكن استدعاء getpeername
لمعرفة من أين هذا الإتصال.
- لتحقيق هذه المعادلة الصعبة في الاستماع fd الأصلي من
أجل طلبات إتصال جديد أو متابعة الواصفات التي قبل خادمنا
أن يتواصل معها، نقوم بما يلي
- انتظر طلبات listen
- اعمل مجموعة من نوع fd_set وأضف إليها fd الأصلي
- انتظر بالوظيفة select قدوم بيانات
إلى أي عنصر في المجموعة السابقة
- لكل عنصر انظر هل هو fd الأصلي فإن كان فانظر في قبول الإتصال الجديد
accept ثم أضف الواصف الناتج للمجموعة fd_set
لكي نتواصل ونتظر منه.
- إذا لم يكن الواصف الأصلي فهي رسالة جديدة ممن
قبلنا الإتصال معه ويمكنك القراءة والكتابة
عبره بالوظائف read و write
و recv و send
- كرر الخطوة 3
تحذير
يجب تحويل الأرقام (رقم المنفذ port وعنوان IP) من هيئة الجهاز
الحالي لهيئة الشبكة (حيث تختلف الأجهزة في ترتيب البايتات
بحيث يكون الأكثر/الأقل أهمية أولاً big-endian/little-endian على الترتيب)
بواسطة مثلاً htons (port)
htonl (INADDR_LOOPBACK).
لا يجوز وضع accept داخل thread في برامج multi-threads.
ملفات headers التي يجب استعمالها
هي بالأساس sys/socket.h
ومن أجل العناوين المحلية sys/un.h
وعناوين IP الملف netinet/in.h
الملف arpa/inet.h
يحتوي الوظيفة inet_aton
لتحويل سلسلة نصية تحتوي العنوان على صورة "a.b.c.d"
ومن أجل حل العناوين بالاسم مثل gnu.org بواسطة خادم DNS نستعمل
ملف netdb.h
لتسهيل العمل يقترح كتيب مكتبة glibc الوظائف التالية
المستعملة بكثرة. لهذا أجمعها في ملف واحد.
/* socket-common.c - add it to your project for common function */
#include <stddef.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <netdb.h>
int make_named_socket (const char *filename) {
struct sockaddr_un name;
int sock;
size_t size;
/* Create the socket. */
sock = socket (PF_LOCAL, SOCK_DGRAM, 0);
if (sock < 0) {
perror ("socket"); exit (1);
}
/* Bind a name to the socket. */
name.sun_family = AF_LOCAL;
strncpy (name.sun_path, filename, sizeof (name.sun_path));
size = SUN_LEN (&name);
if (bind (sock, (struct sockaddr *) &name, size) < 0) {
perror ("bind"); exit (1);
}
return sock;
}
int make_socket (uint16_t port) {
int sock;
struct sockaddr_in name;
/* Create the socket. */
sock = socket (PF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror ("socket"); exit (1);
}
/* Give the socket a name(any_address:port). */
name.sin_family = AF_INET;
name.sin_port = htons (port);
name.sin_addr.s_addr = htonl (INADDR_ANY);
if (bind (sock, (struct sockaddr *) &name, sizeof (name)) < 0) {
perror ("bind"); exit (1);
}
return sock;
}
void init_sockaddr (struct sockaddr_in *name,const char *hostname,uint16_t port){
struct hostent *hostinfo;
name->sin_family = AF_INET;
name->sin_port = htons (port);
hostinfo = gethostbyname (hostname);
if (hostinfo == NULL) {
fprintf (stderr, "Unknown host %s.\n", hostname);
exit (1);
}
name->sin_addr = *(struct in_addr *) hostinfo->h_addr;
}
لاحظ أن العناوين في PF_LOCAL تكون من نوع sockaddr_un
وتحدد بوضع العنصر sun_family ليكون AF_LOCAL
ونسخ مسار الملف إلى العنوان المشار له ب sun_path
بواسطة strcpy ونعرف الحجم الحقيقي(لأن sun_path محجوزة ل108 محرف مسبقاً) لها ب SUN_LEN (نحتاجه ل bind).
أما PF_INET فهي من نوع sockaddr_in
العنصر sin_family يجب أن يساوي AF_INET
و sin_port رقم المنفذ و s_addr إلى عنوان IP
محول إلى رقم (بواسطة gethostbyname مثلاً) أو من الجاهزة
مثل htonl (INADDR_LOOPBACK) و
htonl (INADDR_ANY)
ونعرف حجمها ب sideof.
هذه الأمثلة معدل من كتيب مكتبة سي من غنو أولها مخدوم وخادم توضيحي
محلي بطريقة الإبراق datagram يشبه الزوج السابق FIFO:
/* mysock-lo-dgram-client: local data gram socket sample client */
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SERVER "/tmp/serversocket"
#define CLIENT "/tmp/mysocket"
#define MAXMSG 512
/* This function is defiend in our 'socket-common.c' */
extern int make_named_socket (const char *name);
int main (int argc,char *argv[]) {
int sock;
struct sockaddr_un name;
size_t size;
int nbytes;
/* Process arguments and show usage */
if (argc!=2) {
printf("mysock-lo-dgram-client: sample socket testing client
syntax: mysock-lo-dgram-client STRGING
where STRING is the string to send to server.
example:
mysock-lo-dgram-client 'Hello every body'
");
exit(0);
}
/* Make the socket. */
sock = make_named_socket (CLIENT);
/* Initialize the server socket address. */
name.sun_family = AF_LOCAL;
strcpy (name.sun_path, SERVER);
size=SUN_LEN (&name); /* it was strlen(name.sun_path) + sizeof(name.sun_family)*/
/* Send the datagram. */
nbytes = sendto (sock, argv[1], strlen (argv[1]) + 1, 0, (struct sockaddr *) & name, size);
if (nbytes < 0) {
perror ("sendto (client)"); exit (-1);
}
/* Clean up. */
remove (CLIENT);
close (sock);
return 0;
}
/* mysock-lo-dgram-server: local data gram socket sample server*/
/* run it in the background using '&' */
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SERVER "/tmp/serversocket"
#define MAXMSG 512
/* This function is defiend in our 'socket-common.c' */
extern int make_named_socket (const char *name);
int main (void) {
int sock;
char message[MAXMSG];
struct sockaddr_un name;
size_t size;
int nbytes;
/* Remove the filename first, it's ok if the call fails */
unlink (SERVER);
/* Make the socket, then loop endlessly. */
sock = make_named_socket (SERVER);
while (1) {
/* Wait for a datagram. */
size = sizeof (name);
nbytes = recvfrom (sock, message, MAXMSG, 0,
(struct sockaddr *) & name, &size);
if (nbytes < 0) {
perror ("recfrom (server)"); exit (1);
}
/* Give a diagnostic message. */
fprintf (stderr, "Server: got message: [%s]\n", message);
}
}
الزوج التالي مخدوم وخادم توضيحي
عبر IP بطريقة السيال STREAM :
/* mysock-ip-client: internet sample client */
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#define PORT 5555
/* This function is defiend in our 'socket-common.c' */
extern void init_sockaddr (struct sockaddr_in *name, const char *hostname,uint16_t port);
char *SERVERHOST,*MESSAGE;
void write_to_server (int filedes) {
int nbytes;
nbytes = write (filedes, MESSAGE, strlen (MESSAGE) + 1);
if (nbytes < 0) {
perror ("write"); exit (1);
}
}
int main (int argc,char *argv[]) {
int sock;
struct sockaddr_in servername;
/* Process arguments and show usage */
if (argc!=3) {
printf("mysock-ip-client: sample inet socket testing client
syntax: mysock-ip-client HOSTNAME STRGING
where: STRING is the string to send to server.
HOSTNAME is the host of the server like 'mynet.com'
example:
mysoc-ip-client 'localhost' 'Hello every body'
");
exit(0);
}
SERVERHOST=argv[1];
MESSAGE=argv[2];
/* Create the socket. */
sock = socket (PF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror ("socket (client)"); exit (1);
}
/* Connect to the server. */
init_sockaddr (&servername, SERVERHOST, PORT);
if (0 > connect (sock,
(struct sockaddr *) &servername,
sizeof (servername))) {
perror ("connect (client)"); exit (1);
}
/* Send data to the server. */
write_to_server (sock);
close (sock);
return 0;
}
/* mysock-ip-serv: internet sample server */
/* run it in the background */
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#define PORT 5555
#define MAXMSG 512
int read_from_client (int filedes) {
char buffer[MAXMSG];
int nbytes;
nbytes = read (filedes, buffer, MAXMSG);
if (nbytes < 0) { /* Read error. */
perror ("read"); exit (1);
} else if (nbytes == 0) { /* End-of-file. */
return -1;
} else { /* Data read. */
fprintf (stderr, "Server: got message: `%s'\n", buffer);
return 0;
}
}
int main (void) {
extern int make_socket (uint16_t port);
int sock;
fd_set active_fd_set, read_fd_set;
int i;
struct sockaddr_in clientname;
size_t size;
/* Create the socket and set it up to accept connections. */
sock = make_socket (PORT);
if (listen (sock, 1) < 0) {
perror ("listen"); exit (1);
}
/* Initialize the set of active sockets. */
FD_ZERO (&active_fd_set);
FD_SET (sock, &active_fd_set);
while (1) {
/* Block until input arrives on one or more active sockets. */
read_fd_set = active_fd_set;
if (select (FD_SETSIZE, &read_fd_set, NULL, NULL, NULL) < 0) {
perror ("select"); exit (1);
}
/* Service all the sockets with input pending. */
for (i = 0; i < FD_SETSIZE; ++i)
if (FD_ISSET (i, &read_fd_set)) {
if (i == sock) {
/* Connection request on original socket. */
int new;
size = sizeof (clientname);
new = accept (sock,
(struct sockaddr *) &clientname,
&size);
if (new < 0) {
perror ("accept"); exit (1);
}
fprintf (stderr,
"Server: connect from host %s, port %hd.\n",
inet_ntoa (clientname.sin_addr),
ntohs (clientname.sin_port));
FD_SET (new, &active_fd_set);
} else {
/* Data arriving on an already-connected socket. */
if (read_from_client (i) < 0) {
close (i);
FD_CLR (i, &active_fd_set);
}
} /* enf of if-else */
} /* end of for */
} /* end of while */
}
|