7.2 مكتبة سي القياسية ونظام يونكس كتاب لينكس الشامل >>

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 التعامل مع السلاسل النصية

موضوع السلاسل النصية على علاقة وثيقة مع المنظومات(مناطق من الذاكرة) لأن السلاسل عبارة عن منظومة عناصرها 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 للسلوك التلقائي و لإهمال الإشارة على الترتيب. أشهر الوظائف التي قد تفكر في تغيير سلوكها مثلاً لعدم الاستجابة ل CTRL+C يمكنك عمل
signal(SIGINT,SIG_IGN);

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+فتح للقراءة والإضافة وإنشائه إن لم يكن موجود القراءة من أي مكان والإضافة إلى النهاية

warningتحذير

هناك علامة أخرى للطور هي إما 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);

tipتلميح

هناك ثلاث ملفات مفتوحة دائماً هي 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);

tipتلميح

إن الوظائف التي شرحناها سابقاً تصلح لملفات لا يزيد حجمها عن 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

warningتحذير

الكثير من الوظائف أدناه موجودة حصراً على أنظمة يونكس ، وربما لن تجدها في المصنفات غير القياسية مثل (...) تعرفون من أقصد. هذا على الرغم من أنها ووظائف قياسية ضمن معايير 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 منها

نستخدمها باستثناء الأولى في عمل علاقة خادم/مخدوم فالأولى لا تحتاج تصميم برنامج بطريقة معينة فأي برنامج دون إعادة كتابته يمكنه أن يرسل بيانات أو يستقبلها عبر الأنابيب. مثلاً بكتابة في باش 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 تأخذ مؤشر على منظومة من عددين صحيحين هما رقما وصف الملف الخاص بالقراءة والكتابة العائد على الأنبوب
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.

tipتلميح

PF_LOCAL له أسماء أخرى هي PF_UNIX و PF_FILE هي فقط للتواصل عبر جهاز واحد. هذا لا يعني أنه لا يمكن عمل اتصال بين برنامجين على جهاز واحد لا يحتوي جهاز اتصال باستعمال PF_INET إذ يمكن استعمال العنوان المحلي127.0.0.1

فيما بعد نربط القنواة socket بعنوان ثم نتصرف بطريقة مختلفة كخادم أو مخدوم .

لعمل برنامج وكيل/مخدوم client فإننا نقم بالخطوات بالشكل التالي:

أما عمل خادم فهو أكثر تعقيداً، كما يلي
warningتحذير

يجب تحويل الأرقام (رقم المنفذ 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 */
}


<< السابق كتاب لينكس الشامل التالي >>