ברוב המאמרים בבלוג נגעתי בעיקר בפיתוח ב User Space והגיע הזמן להכיר את ה Kernel שמניע את כל המערכת, בסדרת המאמרים הקרובה אני אתמקד בפיתוח מול ה Kernel של Linux ואנסה לכסות כמה שאפשר מהתחום ההענק הזה, אז כמו תמיד המאמר הראשון הוא לצורך הכרות לפני שנצלול פנימה.
Kernel
אם חשבתם שאתם בעל הבית על המחשב שלכם אז טעיתם בגדול אתם סה"כ משתמשים פשוטים שנשמעים להוראות של בעל הבית האמיתי שנקרא Kernel, זו תוכנית שרצה בעצמה וממש לא צריכה את העזרה שלנו, המטרה שלה לגרום לכל העסק לתקתק, היא אחראית על הכל ואין דבר שנסתר ממנה (כמעט...).
שימו לב!
- עבודה מול ה Kernel עלולה להזיק למחשב שלך, מומלץ להשתמש במכונה וירטואלית.
- המאמר נכתב על Fedora 14.
תוכניות שרצות בעולם ה User Space צריכות לגשת לחומרה והן יכולות לעשות זאת רק בעזרת ה Kernel, לצורך העניין תוכנית שרוצה לקרוא מקובץ צריכה להפעיל פונקציה משכבת ה Api של ה Kernel שנקראת System Call שתפעיל את המנגנונים המתאימים על מנת לגשת לחומרה.
Example Module
הדוגמה הראשונה תעשה הכרות עם תהליך כתיבת ה Module וטעינה למערכת, התוכנית עצמה מאוד פשוטה מדובר על HelloWorld שרץ ב Kernel, אז חבל על הדיבורים וקדימה לעבודה, ניצור קובץ helloworld.c בעזרת gedit או כל עורך אחר ונעתיק את הקוד הבא:
#include <linux/module.h> //all kernel modules
#include <linux/init.h> //init and exit macros
int init_driver(void)
{
//write to kernel log
printk("Hello Kernel \n");
return 0;
}
void cleanup_driver(void)
{
//write to kernel log
printk("Goodbye Kernel \n");
}
//pointing to custom init function when the module loaded
module_init(init_driver);
//pointing to custom cleanup function when the module unloaded
module_exit(cleanup_driver);
על מנת שנוכל להפעיל את ה Module יש לבנות פונקציה Init ופונקציה Cleanup ולשלוח את ה Pointer שלהם ל Macros מיוחדים - module_init ו module_exit , ברגע שנפעיל את ה Module הפונקציה Init תרוץ וכאשר נוריד את ה Module פונקצית ה Cleanup תרוץ ונוצרה התחלה וסוף עבור ה Module ,בעזרת הפקודה dmesg נוכל לראות את הפלט של printk.
Makefile
קובץ הגדרות עבור ה Compiler שאוסף Headers מתיקיית המקור של ה Linux ומשלב אותם בתוכנית, הקובץ עדין במרחקים בין השורות ויש להשים לב שהוא במבנה המתאים, לרוב נכתוב את הקובץ פעם אחת ונשתמש בו לאורך התוכנית.
obj-m := helloworld.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
יש לשמור את הקובץ בשם Makefile בתיקייה שמכילה את הקובץ helloworld.c ולאחר מכן נריץ את הפקודה make ב Terminal חשוב לוודא ש GCC מותקן על המכונה:
[root@localhost helloKernel]# make
התוצר הסופי הוא קובץ בסיומת (KO (Kernel Object שהוא ה Driver, על מנת שנטען אותו נשתמש בפקודה insmod והשם של ה Module.
[root@localhost helloKernel]# insmod helloworld.ko
על מנת להסיר את ה Driver יש להשתמש בפקודה rmmod והשם של ה Module.
[root@localhost helloKernel]# insmod helloworld.ko
העבודה עם insmod ו rmmod מאפשרת לטעון באופן דינמי Drivers ללא צורך של Compile חדש לכל ה Kernel והופכת את העבודה לנוחה יותר.
Character Device Driver
ב Linux קיימים 3 סוגים של (Device Driver (Character,Block,Network, החלטתי בהתחלה להתמקד ב Character Device שמאפשר קריאה וכתיבה של Char בודד בכל פעם, הוא נפוץ מאוד ויש לו שימושים רבים כמו במקלדות,עכברים, מודמים וכו, אבל לפני שנכנס לקוד צריך להכיר מספר דברים במערכת ההפעלה, יש משפט עתיק על Linux שאומר Everything Is A File , לכל רכיב חומרתי יש קובץ מייצג שדרכו ניתן לגשת לחומרה, הדגמתי באחד המאמרים בבלוג כיצד ניתן לשלוט בהתקנים בעזרת כתיבה או קריאה מקובץ באמצעות BeagleBone , נכנס לתיקייה dev/ ונרשום את הפקודה ls-l על מנת לראות את כל ההתקנים במערכת.
האות הראשונה בהרשאות ב Linux מייצגת את השיוך, כפי שניתן לראות ההתקן Loop7 מתחיל עם האות b כלומר מדובר ב Block Device ו lp0 מתחיל ב c כלומר Character Device, קיימים 2 מספרים חשובים עבור כל התקן Major ו Minor שיחד הם מזהה יחודי עבור ה Device.
השלב הבא הוא יצירת ה Device File שמאפשר ל User Space לדבר עם ה Kernel Space כפי שנראה בהמשך, לא ניתן לייצר קובץ בספריית ה dev/ ככה סתם ויש להשתמש בפקודה מיוחדת שנקראת mknod כפי שניתן לראות בדוגמה:
[root@localhost dev]# mknod /dev/cdev c 89 1
בהרצת הפקודה נוצר קובץ בתיקיית dev/ מסוג Character Device עם מספרי ה Major וה Minor, יש הרשאה בלעדית ל Root לכתוב או לקרוא מהקובץ, ניתן לשנות את ההרשאות בעזרת הפקודה chmod, וניתן להסיר את הקובץ בעזרת פקודת rm פשוטה.
לאחר יצירת קובץ של Device File , נבנה את ה Driver שהולך ל"התלבש" על הקובץ, כל עוד שה Driver לא נטען למערכת לא ניתן לכתוב או לקרוא מהקובץ, יש לממש מספר פונקציות שדרכן נוכל להתממשק ל Device, הרעיון שלוקחים את ה Structure של File Operations שמייצג קבוצה של פונקציות לעבודה עם קבצים ודורסים את הפונקציות עם פונקציות שלנו, כך שבכל פעם שמתבצעת קריאה לקובץ הפונקציות של ה Driver יקפצו ויעשו את העבודה.
#include <linux/module.h>
#include <linux/string.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/init.h>
//must write license
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("simple character device");
#define MAX 100
#define MIN 0
static char buffer[MAX] = {MIN};
//prototype of the overridden functions
static int c_open(struct inode * , struct file *);
static int c_close(struct inode * , struct file *);
static ssize_t c_read(struct file *, char *,size_t,loff_t *);
static ssize_t c_write(struct file *, const char *,size_t,loff_t *);
//overridden file operations structure
static struct file_operations fops =
{
.read = c_read,
.open = c_open,
.write = c_write,
.release = c_close,
};
//load module and override file operation structure for the file with new methods
int init_driver(void)
{
int check = register_chrdev(89,"cdev",&fops);
if(check<0){
printk("Error register device \n");}
else
{
printk("Device register complete \n");
}
return check;
}
//remove module
void cleanup_driver(void)
{
unregister_chrdev(89,"cdev");
}
static ssize_t c_read(struct file *dfile, char *buff,size_t len,loff_t *off)
{
printk("File device Read \n");
unsigned short ret;
//get the minimum number between 2 numbers
int bytes = min(MAX - (int)(*off),(int)len);
//no more bytes to read
if(bytes == MIN){
return MIN;}
//copy from kernel space address to user space address
ret = copy_to_user(buff,*off+buffer,bytes);
if(ret){
//error: bad address
return -EFAULT;
}
else{
//change pointer offset position by bytes to reads
*off = *off + bytes;
return bytes;
}
}
static ssize_t c_write(struct file *dfile,const char *buff,size_t len,loff_t *off)
{
printk("File device Write \n");
unsigned short ret;
//any write reset buffer
memset(buffer,MIN,MAX);
//copy from user space address to kernel space address
ret = copy_from_user(*off+buffer,buff,len);
if(ret){
//error: bad address
return -EFAULT;}
else {
//change pointer offset position by length
*off = *off + len;
return len;
}
}
static int c_open(struct inode *inod, struct file *flip)
{
printk("Device file open \n");
return MIN;
}
static int c_close(struct inode *inod, struct file *flip)
{
printk("Device file closed \n");
return MIN;
}
//pointing to custom init function when the module loaded
module_init(init_driver);
//pointing to custom cleanup function when the module unloaded
module_exit(cleanup_driver);
ההתנהגות לחומרה כקובץ זה אחד היתרונות החזקים ב Kernel של Linux וזה יוצר סביבה גנרית להתממשקות כפי שניתן לראות בקוד נדרסו 4 פונקציות (Open, Close, Read, Write) ובכל רגע שתוכנית מה User Space תעבוד מול ה Device File הפונקציות יופעלו, חשוב מאוד שבשלב טעינת ה Module להתחבר ל Device File בעזרת הפונקציה register_chrdev שולחים את מספר ה Major ,שם ה Device File ואת ה File Operations Structure על מנת שנדרוס את הפונקציות,סגירת ה Device File מתבצעת בעזרת הפונקציה unregister_chrdev.
User Space
ניתן לכתוב או לקרוא מה Device File בעזרת הכלים הבסיסיים ב Console כמו cat ו echo אבל על מנת להשלים את התמונה ניצור תוכנית ב User Space שתכתוב ותקרא מהחומרה כאילו זה קובץ ב File System:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
#define MAX 100
int main(int args, char *argv[]) {
//the name of the application will be first
if(args == 1)
{
printf("no args...\n");
return EXIT_SUCCESS;
}
char buffer[MAX];
memset(buffer,0,MAX);
printf("input from user space:%s \n",argv[1]);
//open function in the driver execute
int fd = open("/dev/cdev", O_RDWR);
if(fd !=-1)
{
//write function in the driver execute
write(fd,argv[1],strlen(argv[1]));
//set position of the offset pointer back to beginning of file,
//function not overridden in the file operations structure
lseek(fd,0,SEEK_SET);
//read function in the driver execute
read(fd,&buffer,strlen(argv[1]));
//close function in the driver execute
close(fd);
printf("output from kernel space:%s \n",&buffer);
}
return EXIT_SUCCESS;
}
סיכום
המאמר הזה הוא טיפה בים הענק והמסובך הזה שנקרא Kernel, במהלך המאמרים הבאים אני יעבור על מספר מנגנונים שנמצאים ב Kernel ועל סוגי התקנים נוספים מה שבטוח זה לא יהיה קל אבל מאוד מעניין.
מקורות מידע
אין תגובות:
הוסף רשומת תגובה