رفتن به نوشته‌ها

ماه: آگوست 2015

ایجاد یک فرایند با استفاده از تابع fork

قبلا با نوشتن یک فرآیند -Process- ابتدایی در لینوکس به وسیله تابع system کمی آشنا شدیم . اما همانطور که گفتم روش اصولی استفاده از تابع fork و ساخت یک فرآیند فرزند -child Process- و تغییر این فرآیند جدید طبق نیازمون هست .
در برنامه نویسی سیستمی ویندوز (win32) از توابع از پیش نوشته شده ای مثل CreateProcess برای ساخت یک process جدید استفاده میشود . در لینوکس این تابع در دسترس نیست ولی توابع fork و exec کار مشابهی را برای ما انجام میدهند .
تابع fork
با استفاده از فراخوانی تابع fork لینوکس یک فرآیند فرزند کاملا مشابه فرآیند والد -parent Process- ایجاد میکند . فرآیند فرزند با اینکه کاملا مشابه والد خود است اما در فضای حافظه متفاوتی اجرا می‌شود و بعد از فراخوانی تابع fork مسیر متفاوتی نسبت به والد خود طی می‌کند . با توجه به این موضوع که فرآیند فرزند یک فرآیند جدید است ، شناسه متفاوتی با فرآیند والد خود دارد و با توجه به همین موضوع می‌توان تشخیص داد فرآیندی که در حال اجراست فرآیند فرزند است یا فرآیند والد . مقدار بازگشتی تابع fork در فرآیند والد برابر با شناسه فرآیند فرزند است ولی این مقدار در فرآیند فرزند برابر با عدد ۰ است . با چک کردن این عدد ( مقدار بازگشتی تابع fork ) به راحتی میتوان تشخیص داد فرآیند در حال اجرا فرآیند والد است یا فرزند .

در برنامه زیر در صورتیکه فرآیند فرزند در حال اجرا باشد شرط if صحیح است و دستورات بلاک بالا اجرا می‌شوند و در‌صورتیکه فرآیند والد در حال اجرا باشد شرط if صحیح نیست و بلاک مربوط به else اجرا می‌شود . فرآیند فرزند ۱۰ مرتیه و فرآیند والد ۲۰ مرتبه اجرا می‌شوند و هر کدام بعد از هربار اجرا به مدت ۱ ثانیه تاخیر ایجاد می‌کنند :

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
 
int main ()
{
pid_t child_pid;
int count1 , count2;
 
printf ("the main process id before fork ,  is %d\n" , (int) getpid());
child_pid = fork ();
 
    if (child_pid == 0 )
    {
        printf ("this is the child Process with id %d\n" , (int) getpid());
        while(count1 < 10)
        {
            printf("child Process : %d\n" , count1 );
            sleep (1);
            count1++;
        }
    }
    else
    {
        printf ("this is parent Process with id %d\n" , (int) getpid());
        while (count2 < 20)
        {
            printf ("Parent Process : %d\n" , count2 );
            sleep (1);
            count2++;
        }
    }
return 0 ;
}

در سایت دانشگاه واترلو یک مثال ابتدایی ( شبیه به همین چیزی که من نوشتم ) از توابع fork وexec وجود داره .

خارج شدن از نظر

نوشتن یک پروسس در لینوکس

در یک ماشین لینوکسی Process به چه چیزی گفته میشود ؟ چگونه یک فرآیند – Process – ایجاد کنیم ، مدیریت کنیم و در انتها Process را خاتمه دهیم ؟

به هر نمونه از برنامه درحال اجرا در یک سیستم یونیکسی ( شبه یونیکسی ) یک Process گفته میشود . برای دیدن لیست Process ها در خط فرمان از دستور ps -e میتوانیم استفاده کنیم . هر Process یک Process والد دارد بجز init که با ID شماره ۱ در لیست Process ها قرار گرفته و وظیفه راه اندازی سیستم را برعهده دارد . فرآیند‌ها همواره در حال اجرا هستند ، حتی در هنگامی که ما هیچ کار خاصی با سیستم خود انجام نمیدهیم .

در لینوکس هر Process با یک ID اختصاصی شناسایی می‌شود ، که به این شماره اختصاصی معمولا pid (مخفف Process ID ) گفته می‌شود . این شناسه اختصاصی یک عدد ۱۶ بیتی است و در زمان ایجاد Process به آن اختصاص داده می‌شود . به Process والد ( Process ای که باعث به وجود آمدن Process فعلی می‌شود والد Process فعلی محسوب می‌گردد.) نیز ppid (مخفف Parend Process ID ) گفته می‌شود . در یک سیستم لینوکسی فرآیند ها – Processes – بصورت یک درخت در نظر گرفته می‌شوند که init در ریشه قرار گرفته است و هر Process بالاتر به عنوان والد Process پایین‌تر محسوب می‌شود .برای مشاهده فرآیندها به همراه فرآیند والد آنها از دستور

$  ps -e -o pid,ppid,command

استفاده می‌کنیم . سوئیچ e- باعث نمایش همه Process ها میشود و با استفاده از سوئیچ o- میتوان موارد مورد نیاز برای نمایش را ( مانند ppid و توضیحات درباره Process در حال اجرا )به مفسر shell اعلام کرد . اگر در ترمینال فعلی دستور بالا را دوباره اجرا کنیم یک شماره PID جدید به Process ما ( یعنی ps -e -o pid,ppid,command ) اختصاص می‌یابد ، اما شماره ppid همچنان بدون تغییر باقی می‌ماند . زیرا والد Process ما همین پنجره ترمینال است و باعث به وجود آمدن Process ما این پنجره ترمینال می‌باشد . پس تا وقتی از این پنجره ترمینال هر دستوری اجرا شود ppid همیشه ثابت خواهد بود .

ایجاد یک فرآیند در لینوکس
برای ایجاد یک Process در لینوکس از ۲ روش میتوانیم استفاده کنیم . در این پست روش اول توضیح داده می‌شود و ان‌شاءالله در پست بعدی روش دوم معرفی می‌گردد :

درکتابخانه استاندارد C یک تابع به اسم system وجود دارد که میتواند برنامه های موردنظر ما را در داخل برنامه در حال اجرا call کرده و خروجی را به برنامه اصلی برگرداند . روش کار بسیار ساده است ، برنامه فعلی (Process والد ) در حال اجرا یک فرآیند فرزند ایجاد میکند . فرآیند فرزند بعد از اتمام کار نتیجه را به فرایند والد برگردانده و خودش به اتمام میرسد . برای روشن شدن ماجرا برنامه ساده زیر را در نظر بگیرید :

#include<stdlib.h>
 
int main (){
 
    int return_value;
    return_value = system("ls -l ");
return return_value;
}

در این برنامه تابع system برنامه ls با سوئیچ -l را در مسیر جاری فراخوانی کرده نتیجه را در کنسول چاپ می‌کند . تابع system وضعیت اجرای فرمان در پوسته سیستم عامل را بازمیگرداند . درصورتیکه پوسته نتواند اجرا شود مقدار ۱۲۷ و اگر خطای دیگری رخ دهد مقدار ۱- بازگردانده خواهد شد .

نقص های این روش بیشتر مربوط به یکسان نبودن نوع پوسته ها و ورژن پوسته ها در سیستم های مختلف شبه یونیکسی می‌باشد. به عنوان مثال خروجی پوسته های مختلف کاملا مشابه نبوده و ممکن است در برنامه‌های پیچیده‌تر باعث اشکال در تفسیر child-process یا فرآیند فرزند شود . همچنین نقایص امنیتی نیز ایراد دیگری است که به این روش وارد است .

برای آشنایی با system میتونید از این منبع استفاده کنید .

خارج شدن از نظر

نوشتن یک Kernel Module در لینوکس۲

بعد از نوشتن اولین Kernel Module ادامه مبحث را با توسعه برنامه نوشته شده قبلی پیگیری میکنیم

ابتدا به برنامه نوشته شده قبلی نگاهی می اندازیم :

 *  hello-1.c - The simplest kernel module.
 */
#include <linux/module.h> /* Needed by all modules */
#include <linux/kernel.h> /* Needed for KERN_INFO */
 
int init_module(void)
{
    printk(KERN_INFO "Hello world 1.\n");
 
    /*
     * A non 0 return means init_module failed; module can't be loaded.
     */
    return 0;
}
 
void cleanup_module(void)
{
    printk(KERN_INFO "Goodbye world 1.\n");
}

در برنامه بالا تابع های ما معرفی شده و تعریف میشوند . نام توابع باید حتما همانند مثال فوق باشد تا قابلیت الحاق به کرنل را داشته باشه . این شیوه برنامه نویسی سخت بوده و توسعه آن کند پیش خواهد رفت . در اینجا روش جدیدتری برای توسعه Kernel Module معرفی میشود که توسعه دهنده از نام های دلخواه استفاده کند و در انتها نام تابع نوشته شده را به ماکروهای Kernel Module معرفی میکند :

/*  
 *  hello-2.c - Demonstrating the module_init() and module_exit() macros.
 *  This is preferred over using init_module() and cleanup_module().
 */
#include <linux/module.h> /* Needed by all modules */
#include <linux/kernel.h> /* Needed for KERN_INFO */
#include <linux/init.h>       /* Needed for the macros */
 
static int __init hello_2_init(void)
{
    printk(KERN_INFO "Hello, world 2\n");
    return 0;
}
 
static void __exit hello_2_exit(void)
{
    printk(KERN_INFO "Goodbye, world 2\n");
}
 
module_init(hello_2_init);
module_exit(hello_2_exit);

در برنامه بالا توابع init و exit با نام دلخواه نوشته شده که کار پرینت کردن عبارت دلخواه در log سیستم را انجام میدهند . و در انتها توابع فوق به ماکرو های module_init و module_exit معرفی شده اند . در این حالت برنامه نویسی منظم تر و توسعه سریعتر پیش میرود .
ماکروهای module_init و module_exit در هدر فایل linux/init.h قرار دارد که در ابتدا به برنامه ما include شد .
تنها نکته مهم این است که همانند تمامی برنامه ها به زبان c هر تابع تنها بعد از معرفی قابل دستیابی خواهد بود ، بنابر این معرفی توابع باید حتما قبل از ماکروها باشد .
برای کامپایل برنامه ابتدا Makefile بصورت زیر نوشته و با دستور make میتواین Kernel Module را کامپایل کنیم :

obj-m += hello-2.o
 
all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
 
clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

برای اضافه کردن Module نوشته شده به کرنل همانند قبل از دستور

#insmod hello-2.ko

استفاده میکنیم .
همچنین برای پایین آوردن Module نوشته شده از کرنل از دستور

# rmmod hello_2

استفاده میکنیم.

اضافه کردن License به Kernel Module
از کرنل ورژن ۲٫۲ به بعد برای الحاق Module های اختصاصی به کرنل احتیاج به License برای مشخص شدن حقوق مالکیت نرم افزار می باشد . این کار برای جلوگیری از نگرانی کاربران از نوع نرم Module مورد استفاده در کرنل میباشد .
برای اضافه کردن License و حق نشر از ماکروی MODULE_LICENSE استفاده میکنیم ، انواع حق نشر قابل دسترس مطابق می باشد :

 "GPL"				[GNU Public License v2 or later]
 "GPL v2"			[GNU Public License v2]
 "GPL and additional rights"	[GNU Public License v2 rights and more]
 "Dual BSD/GPL"			[GNU Public License v2 or BSD license choice]
 "Dual MIT/GPL"			[GNU Public License v2 or MIT license choice]
 "Dual MPL/GPL"			[GNU Public License v2 or Mozilla license choice]

ماکروی MODULE_LICENSE در هدرفایل linux/module.h قرار دارد . بطور مشابه ماکروی MODULE_DESCRIPTION برای ارائه توضیحات مختری راجع به عملکرد Module مورد استفاده قرار میگیرد . MODULE_AUTHOR نویسنده Module را مشخص میکند و MODULE_SUPPORTED_DEVICE مشخص میکند که Module با چه نوعی از devise ها سازگاری دارد . این ماکرو ها در هدر فایل linux/module.h قرار دارند . این ماکروها برای مستند سازی Kernel Module نوشته شده بکار میروند و توسط ابزارهایی مثل ObjDump قابل دیده شدن هستند . (objdump ابزاری برای برگرداندن سورس کد از فایل باینری میباشد.)

خارج شدن از نظر

کامپایل کردن Kernel Module در لینوکس ۱

کامپایل کردن Kernel Modules ، چرا ؟ و چگونه ؟

یکی از گیکی ترین کارهای ممکن در لینوکس کامپایل کردن کرنل و به تبع این کار نوشتن ماژول هایی برای کاربردی خاص در کرنل لینوکس است . مثلا فرض کنید بعد از آپدیت کرنل لینوکس ، یکی از درایور ها از کار افتاده و بعد از درخواست از سازنده ، کد C درایور را دریافت میکنید . در این حالت کامپایل کد C و الحاق ماژول ایجاد شده بر عهده خود شما خواهد بود . این کار نه تنها برای اضافه کردن درایور های سخت افزاری بلکه برای انجام کاری خاص در فضای کرنل که شما تعریف میکنید کاربرد دارد .

من فرض میکنم شما قبلا برنامه هایی معمولی با زبان C نوشته اید و با قابلیت های اساسی این زبان آشنا هستید و قصد دارید قدرت این زبان برنامه نویسی را در کرنل لینوکس ،جایی که با یک اشتباه مهلک میتوانید فایل های سیستمی لینوکس خود را از دست بدید به کار بگیرید .

اما واقعا Kernel Module چیست ؟ ماژول های کرنل یک قطعه کد هستند که با درخواست سرویس های دیگر میتوانند در کرنل بارگزاری (Load) شوند یا از کرنل پایین آورده شوند (UnLoad) . این ماژولها به توسعه قابلیت های کرنل کمک میکنند . بارگزاری این ماژول‌ها در کرنل معمولا احتیاجی به ریبوت سیستم ندارد . یکی از مثالهای خوب در این مورد اضافه کردن سخت افزارها به کرنل لینوکس است ، با اضافه شدن هر سخت افزار ماژول درایور آن در فضای کرنل راه اندازی میشود . در صورت وجود نداشتن این قابلیت در لینوکس ما با یک کرنل یکپارچه روبرو میشدیم که درایور هر قطعه باید مستقیما در ایمیج کرنل قرار میگرفت . در این صورت برای توسعه هر قابلیت در کرنل یا اضافه کردن هر سخت افزار به سیستم باید این کرنل بزرگ دوباره ساخته شده و سیستم از نو راه اندازی می‌شد .

پیش نیازهای کامپایل Kernel Module

برای کامپایل Kernel Module در لینوکس باید kernel headers packages در سیستم نصب باشد ، در حالت عادی این پکیج ها در سیستم نصب نیست ،برای نصب در خانواده دبیان بصورت زیر عمل می‌کنیم :

$ sudo apt-get update
$ apt-cache search linux-headers-$(uname -r)
$ sudo apt-get install linux-headers-$(uname -r)

در آرچ لینوکس از دستور زیر استفاده میکنیم :

sudo pacman -Sy
sudo pacman -S linux-headers

برای تست به نوشتن یک برنامه ساده اکتفا میکنیم:

/*
 *  hello-1.c - The simplest kernel module.
 */
#include <linux/module.h> /* Needed by all modules */
#include <linux/kernel.h> /* Needed for KERN_INFO */
 
int init_module(void)
{
    printk(KERN_INFO "Hello world 1.\n");
 
    /*
     * A non 0 return means init_module failed; module can't be loaded.
     */
    return 0;
}
 
void cleanup_module(void)
{
    printk(KERN_INFO "Goodbye world 1.\n");
}

برنامه بالا را بنام hello-1.c سیو میکنیم و یک فایل تکست بنام Makefile بصورت زیر در محل سورس برنامه فوق ایجاد میکنیم:

obj-m += hello-1.o
 
all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
 
clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

از نظر تکنیکی تنها وجود خط اول Makefile کاملا ضروری است . بعد از نوشتن Makefile با استفاده از ترمینال Kernel Module ای که نوشتیم را با نوشتن دستور make در ترمینال کامپایل میکنیم ، دقت کنید که با نوشتن دستور کامپایل از کد توسط gcc نمیتوانیم مستقیما kernel Module را کامپایل کنیم .

اضافه کردن Kernel Module به هسته لینوکس
برای اضافه کردن ماژولی که نوشتیم از دستور insmod استفاده میکنیم :

$ su -
# insmod hello.ko

تحلیل کارکرد Module نوشته شده
در Kernel Modules ما حداقل ۲ تابع داریم . تابع init_module برای مقدار دهی اولیه -initialization- برنامه ما در آغاز برنامه و در هنگام استارت Module استفاده میشود . و تابع cleanup_module در انتهای برنامه و هنگامی که برنامه از کرنل پایین آورده میشود -UnLoad- اجرا میشود .

معمولا تابع init_module یک کارکرد جدید برای کرنل ایجاد میکند و یا یکی از قابلیت فعلی کرنل را تغییر داده و تعریف جدیدی از آن ارائه میدهد . و تابع cleanup_module قابلیتهایی که تابع init_module تغییر داده بود به حالت پیش فرض خود باز میگرداند .

هر Kernel Module حتما باید شامل هدر فایل linux/module.h باشد . در اینجا ما برای استفاده از تابع printk از هدر فایل linux/kernel.h استفاده کردیم .

تابع ()printk یک تابع برای نوشتن اطلاعاتی در log سیستم میباشد . سیستم لینوکس دارای ۸ اولویت میباشد که با نوشتن ماکرو در تابع printk میتوان اولویت نوشتن در log سیستم را تغییر داد . اولویت ها به همراه ماکروها بصورت زیر است :

Name		String	Meaning											alias function
KERN_EMERG	"0"	Emergency messages, system is about to crash or is unstable				pr_emerg
KERN_ALERT	"1"	Something bad happened and action must be taken immediately				pr_alert
KERN_CRIT	"2"	A critical condition occurred like a serious hardware/software failure			pr_crit
KERN_ERR	"3"	An error condition, often used by drivers to indicate difficulties with the hardware	pr_err
KERN_WARNING	"4"	A warning, meaning nothing serious by itself but might indicate problems		pr_warning
KERN_NOTICE	"5"	Nothing serious, but notably nevertheless. Often used to report security events.	pr_notice
KERN_INFO	"6"	Informational message e.g. startup information at driver initialization			pr_info
KERN_DEBUG	"7"	Debug messages	pr_debug, pr_devel if DEBUG is defined
KERN_DEFAULT	"d"	The default kernel loglevel	

برای دیدن پیغامی که تابع printk در log سیستم چاپ میکند باید برنامه syslogd در حال سرویس باشد :

sudo pacman -S syslog-ng
systemctl enable syslog-ng
systemctl start syslog-ng
cat /var/log/messages.log

با توجه به اینکه هر لحظه message های زیادی وارد فایل messages.log میشود میتوانیم از دستور grep برای پیدا کردن message مورد نظر خودمان استفاده کنیم :

sudo cat /var/log/messages.log | grep Hello

پایین آوردن -UnLoad- کرنل ماژول
برای unload کردن یک Kernel Module از دستور rmmod استفاده می‌کنیم :

rmmod hello-1

خارج شدن از نظر