في المقالة السابقة ، قدمنا تعدد المهام. اليوم حان الوقت للنظر في موضوع برامج تشغيل الأجهزة الشخصية.
على وجه التحديد ، سنقوم اليوم بكتابة برنامج تشغيل طرفي ، وهو آلية للمعالجة المؤجلة للمقاطعات ، وننظر في موضوع معالجات نصفي المقاطعات العليا والسفلى من المقاطعات.
نبدأ بإنشاء هيكل الجهاز ، ثم تقديم دعم I / O للملف الأساسي ، والنظر في هيكل ووظائف io_buf للعمل مع الملفات من stdio.h.
جدول المحتويات
بناء نظام (جعل ، مجلس التعاون الخليجي ، الغاز). التمهيد الأولي (متعدد التمهيد). إطلاق (qemu). مكتبة C (strcpy ، memcpy ، strext). مكتبة C (sprintf ، strcpy ، strcmp ، strtok ، va_list ...). بناء المكتبة في وضع kernel ووضع تطبيق المستخدم. سجل نظام النواة. ذاكرة الفيديو الإخراج إلى المحطة (kprintf ، kpanic ، kassert). الذاكرة الديناميكية ، الكومة (kmalloc ، kfree). تنظيم الذاكرة والتعامل مع المقاطعة (GDT ، IDT ، PIC ، syscall). الاستثناءات. الذاكرة الظاهرية (دليل الصفحة وجدول الصفحة). العملية. المجدول. تعدد المهام. مكالمات النظام (القتل ، الخروج ، ملاحظة).
برامج تشغيل الأجهزة الشخصية. مكالمات النظام (ioctl ، fopen ، fread ، fwrite). مكتبة C (fopen ، fclose ، fprintf ، fscanf).نظام ملفات kernel (initrd) ، قزم ، وداخله. مكالمات النظام (exec). شل كبرنامج كامل للنواة. وضع حماية المستخدم (ring3). قسم حالة المهمة (tss).
شخصية السائقين
كل شيء يبدأ مع ظهور جهاز رمزي. كما تتذكر ، في Linux Device Drivers ، بدا تعريف الجهاز كما يلي:
struct cdev *my_cdev = cdev_alloc( ); my_cdev->ops = &my_fops;
النقطة الأساسية هي تعيين الجهاز لتنفيذ وظائف إدخال / إخراج الملف.
سنتقدم بهيكل واحد ، لكن المعنى سيكون مماثل:
extern struct dev_t { struct clist_head_t list_head; char name[8]; void* base_r; void* base_w; dev_read_cb_t read_cb; dev_write_cb_t write_cb; dev_ioctl_cb_t ioctl_cb; struct clist_definition_t ih_list; };
يتوافق كل جهاز مع نصف قائمة المقاطعات التي يتم استدعاؤها عند إنشاء المقاطعات.
في نظام Linux ، تسمى هذه النصفات بالعلوية ، على العكس ، أقل (المستوى الأدنى).
شخصيا ، بدا لي أكثر منطقية وتذكرت بالصدفة المصطلحات في الاتجاه المعاكس. وصفنا كل عنصر من عناصر قائمة النصفين السفلى من الانقطاعات كما يلي:
extern struct ih_low_t { struct clist_head_t list_head; int number; ih_low_cb_t handler; };
عند التهيئة ، سيقوم برنامج التشغيل بتسجيل جهازه من خلال وظيفة dev_register ، بمعنى آخر إضافة جهاز جديد إلى قائمة الرنين:
extern void dev_register(struct dev_t* dev) { struct clist_head_t* entry; struct dev_t* device; entry = clist_insert_entry_after(&dev_list, dev_list.head); device = (struct dev_t*)entry->data; strncpy(device->name, dev->name, sizeof(dev->name)); device->base_r = dev->base_r; device->base_w = dev->base_w; device->read_cb = dev->read_cb; device->write_cb = dev->write_cb; device->ioctl_cb = dev->ioctl_cb; device->ih_list.head = dev->ih_list.head; device->ih_list.slot_size = dev->ih_list.slot_size; }
لكي يعمل كل هذا بطريقة أو بأخرى ، نحتاج إلى نظام rudiment لنظام الملفات. في البداية ، سيكون لدينا فقط ملفات لأجهزة الشخصيات.
أي سيكون فتح الملف مساوياً لإنشاء بنية FILE من stdio لملف برنامج التشغيل المقابل.
في هذه الحالة ، سوف تطابق أسماء الملفات اسم الجهاز. نحدد مفهوم واصف الملف في مكتبة C الخاصة بنا (stdio.h).
struct io_buf_t { int fd; char* base; char* ptr; bool is_eof; void* file; }; #define FILE struct io_buf_t
للبساطة ، اسمح لجميع الملفات المفتوحة بتخزينها في قائمة حلقات في الوقت الحالي. يتم وصف عنصر القائمة كما يلي:
extern struct file_t { struct clist_head_t list_head; struct io_buf_t io_buf; char name[8]; int mod_rw; struct dev_t* dev; };
لكل ملف مفتوح ، سنقوم بتخزين رابط للجهاز. ننفذ قائمة حلقات من الملفات المفتوحة وننفذ مكالمات نظام القراءة / الكتابة / ioctl.
عند فتح أحد الملفات ، نحتاج فقط إلى تعيين المواضع الأولية لقواعد التخزين المؤقت للقراءة والكتابة من برنامج التشغيل إلى بنية io_buf_t ، وبالتالي ، ربط عمليات الملفات مع برنامج تشغيل الجهاز.
extern struct io_buf_t* file_open(char* path, int mod_rw) { struct clist_head_t* entry; struct file_t* file; struct dev_t* dev; entry = clist_find(&file_list, file_list_by_name_detector, path, mod_rw); file = (struct file_t*)entry->data; if (entry != null) { return &file->io_buf; } entry = clist_insert_entry_after(&file_list, file_list.head); file = (struct file_t*)entry->data; dev = dev_find_by_name(path); if (dev != null) { file->dev = dev; if (mod_rw == MOD_R) { file->io_buf.base = dev->base_r; } else if (mod_rw == MOD_W) { file->io_buf.base = dev->base_w; } } else { file->dev = null; unreachable(); } file->mod_rw = mod_rw; file->io_buf.fd = next_fd++; file->io_buf.ptr = file->io_buf.base; file->io_buf.is_eof = false; file->io_buf.file = file; strncpy(file->name, path, sizeof(file->name)); return &file->io_buf; }
يتم تعريف عمليات القراءة / الكتابة / ioctl في الملف بنمط واحد باستخدام استدعاء نظام القراءة كمثال.
سوف يستدعي النظام نفسه الذي تعلمناه الكتابة في الدرس الأخير ببساطة هذه الوظائف.
extern size_t file_read(struct io_buf_t* io_buf, char* buff, u_int size) { struct file_t* file; file = (struct file_t*)io_buf->file; if (file->dev != null) { return file->dev->read_cb(&file->io_buf, buff, size); } else { unreachable(); } return 0; }
باختصار ، سيقومون بسحب عمليات الاسترجاعات من تعريف الجهاز. الآن سوف نكتب سائق المحطة.
سائق المحطة
نحتاج إلى مخزن مؤقت لإخراج الشاشة ومخزن مؤقت لإدخال لوحة المفاتيح ، بالإضافة إلى عدة إشارات لأوضاع الإدخال والإخراج.
static const char* tty_dev_name = TTY_DEV_NAME; static char tty_output_buff[VIDEO_SCREEN_SIZE]; static char tty_input_buff[VIDEO_SCREEN_WIDTH]; char* tty_output_buff_ptr = tty_output_buff; char* tty_input_buff_ptr = tty_input_buff; bool read_line_mode = false; bool is_echo = false;
نكتب وظيفة إنشاء جهاز. إنه ببساطة يعيد عمليات الاستدعاء لعمليات الملفات ومعالجات النصفين الأدنى من الانقطاعات ، وبعد ذلك يسجل الجهاز في قائمة النغمات.
extern void tty_init() { struct clist_head_t* entry; struct dev_t dev; struct ih_low_t* ih_low; memset(tty_output_buff, 0, sizeof(VIDEO_SCREEN_SIZE)); memset(tty_input_buff, 0, sizeof(VIDEO_SCREEN_WIDTH)); strcpy(dev.name, tty_dev_name); dev.base_r = tty_input_buff; dev.base_w = tty_output_buff; dev.read_cb = tty_read; dev.write_cb = tty_write; dev.ioctl_cb = tty_ioctl; dev.ih_list.head = null; dev.ih_list.slot_size = sizeof(struct ih_low_t); entry = clist_insert_entry_after(&dev.ih_list, dev.ih_list.head); ih_low = (struct ih_low_t*)entry->data; ih_low->number = INT_KEYBOARD; ih_low->handler = tty_keyboard_ih_low; dev_register(&dev); }
يتم تعريف معالج المقاطعة السفلي للوحة المفاتيح كما يلي:
static void tty_keyboard_ih_low(int number, struct ih_low_data_t* data) { char* keycode = data->data; int index = *keycode; assert(index < 128); char ch = keyboard_map[index]; *tty_input_buff_ptr++ = ch; if (is_echo && ch != '\n') { *tty_output_buff_ptr++ = ch; } struct message_t msg; msg.type = IPC_MSG_TYPE_DQ_SCHED; msg.len = 4; *((size_t *)msg.data) = (size_t)tty_keyboard_ih_high; ksend(TID_DQ, &msg); }
نحن هنا ببساطة وضع الحرف الذي تم إدخاله في المخزن المؤقت لوحة المفاتيح. في النهاية ، نسجل الدعوة المؤجلة لمعالج النصفين العلويين من مقاطعة لوحة المفاتيح. يتم ذلك عن طريق إرسال رسالة (IPC) إلى مؤشر ترابط kernel.
خيط النواة نفسه بسيط للغاية:
void dq_task() { struct message_t msg; for (;;) { kreceive(TID_DQ, &msg); switch (msg.type) { case IPC_MSG_TYPE_DQ_SCHED: assert(msg.len == 4); dq_handler_t handler = (dq_handler_t)*((size_t*)msg.data); assert((size_t)handler < KERNEL_CODE_END_ADDR); printf(MSG_DQ_SCHED, handler); handler(msg); break; } } exit(0); }
عند استخدامه ، سيتم استدعاء معالج النصفين العلويين من مقاطعة لوحة المفاتيح. والغرض منه هو تكرار شخصية على الشاشة عن طريق نسخ المخزن المؤقت الإخراج إلى ذاكرة الفيديو.
static void tty_keyboard_ih_high(struct message_t *msg) { video_flush(tty_output_buff); }
الآن يبقى لكتابة وظائف I / O أنفسهم ، ودعا من عمليات الملفات.
static u_int tty_read(struct io_buf_t* io_buf, void* buffer, u_int size) { char* ptr = buffer; assert((size_t)io_buf->ptr <= (size_t)tty_input_buff_ptr); assert((size_t)tty_input_buff_ptr >= (size_t)tty_input_buff); assert(size > 0); io_buf->is_eof = (size_t)io_buf->ptr == (size_t)tty_input_buff_ptr; if (read_line_mode) { io_buf->is_eof = !strchr(io_buf->ptr, '\n'); } for (int i = 0; i < size - 1 && !io_buf->is_eof; ++i) { char ch = tty_read_ch(io_buf); *ptr++ = ch; if (read_line_mode && ch == '\n') { break; } } return (size_t)ptr - (size_t)buffer; } static void tty_write(struct io_buf_t* io_buf, void* data, u_int size) { char* ptr = data; for (int i = 0; i < size && !io_buf->is_eof; ++i) { tty_write_ch(io_buf, *ptr++); } }
عمليات الحرف - الحرف ليست أكثر تعقيدًا ولا أعتقد أنها تحتاج إلى تعليق.
static void tty_write_ch(struct io_buf_t* io_buf, char ch) { if ((size_t)tty_output_buff_ptr - (size_t)tty_output_buff + 1 < VIDEO_SCREEN_SIZE) { if (ch != '\n') { *tty_output_buff_ptr++ = ch; } else { int line_pos = ((size_t)tty_output_buff_ptr - (size_t)tty_output_buff) % VIDEO_SCREEN_WIDTH; for (int j = 0; j < VIDEO_SCREEN_WIDTH - line_pos; ++j) { *tty_output_buff_ptr++ = ' '; } } } else { tty_output_buff_ptr = video_scroll(tty_output_buff, tty_output_buff_ptr); tty_write_ch(io_buf, ch); } io_buf->ptr = tty_output_buff_ptr; } static char tty_read_ch(struct io_buf_t* io_buf) { if ((size_t)io_buf->ptr < (size_t)tty_input_buff_ptr) { return *io_buf->ptr++; } else { io_buf->is_eof = true; return '\0'; } }
يبقى فقط للسيطرة على أوضاع المدخلات والمخرجات لتنفيذ ioctl.
static void tty_ioctl(struct io_buf_t* io_buf, int command) { char* hello_msg = MSG_KERNEL_NAME; switch (command) { case IOCTL_INIT: if (io_buf->base == tty_output_buff) { kmode(false); tty_output_buff_ptr = video_clear(io_buf->base); io_buf->ptr = tty_output_buff_ptr; tty_write(io_buf, hello_msg, strlen(hello_msg)); video_flush(io_buf->base); io_buf->ptr = tty_output_buff_ptr; } else if (io_buf->base == tty_input_buff) { unreachable(); } break; case IOCTL_CLEAR: if (io_buf->base == tty_output_buff) { tty_output_buff_ptr = video_clear(io_buf->base); video_flush(io_buf->base); io_buf->ptr = tty_output_buff_ptr; } else if (io_buf->base == tty_input_buff) { tty_input_buff_ptr = tty_input_buff; io_buf->ptr = io_buf->base; io_buf->is_eof = true; } break; case IOCTL_FLUSH: if (io_buf->base == tty_output_buff) { video_flush(io_buf->base); } else if (io_buf->base == tty_input_buff) { unreachable(); } break; case IOCTL_READ_MODE_LINE: if (io_buf->base == tty_input_buff) { read_line_mode = true; } else if (io_buf->base == tty_output_buff) { unreachable(); } break; case IOCTL_READ_MODE_ECHO: if (io_buf->base == tty_input_buff) { is_echo = true; } else if (io_buf->base == tty_output_buff) { unreachable(); } break; default: unreachable(); } }
ننفذ الآن إخراج إدخال الملف على مستوى مكتبتنا C.
extern FILE* fopen(const char* file, int mod_rw) { FILE* result = null; asm_syscall(SYSCALL_OPEN, file, mod_rw, &result); return result; } extern void fclose(FILE* file) { asm_syscall(SYSCALL_CLOSE, file); } extern u_int fread(FILE* file, char* buff, u_int size) { return asm_syscall(SYSCALL_READ, file, buff, size); } extern void fwrite(FILE* file, const char* data, u_int size) { asm_syscall(SYSCALL_WRITE, file, data, size); }
حسنًا ، إليك بعض الوظائف الرفيعة المستوى:
extern void uvnprintf(const char* format, u_int n, va_list list) { char buff[VIDEO_SCREEN_WIDTH]; vsnprintf(buff, n, format, list); uputs(buff); } extern void uscanf(char* buff, ...) { u_int readed = 0; do { readed = fread(stdin, buff, 255); } while (readed == 0); buff[readed - 1] = '\0'; uprintf("\n"); uflush(); }
حتى لا نخدع بقراءة التنسيق حتى الآن ، سنقرأ دائمًا فقط في السطر كما لو أن علامة٪ s قد أعطيت. كنت كسولًا جدًا في تقديم حالة مهمة جديدة لانتظار واصفات الملفات ، لذلك نحاول فقط قراءة شيء ما في حلقة لا نهائية حتى ننجح.
هذا كل شيء. الآن يمكنك ربط السائقين بأمان إلى نواةكم!
مراجع
شاهد
الفيديو التعليمي لمزيد من المعلومات.
→ شفرة المصدر
في مستودع بوابة (تحتاج فرع الدرس 8)
مراجع
- جيمس مولوي. لفة نظام التشغيل الخاص بك UNIX استنساخ.
- زوبكوف. مجمع ل DOS ، ويندوز ، يونيكس
- كلاشينكوف. المجمع سهل!
- تانينباوم. أنظمة التشغيل. التنفيذ والتطوير.
- روبرت لوف. نواة لينكس وصف عملية التطوير.