חיי דרייבר וממשק למרחב משתמש¶
פרסום ממשק יוצר אחריות חדשה¶
נניח שדרייבר מפרסם קובץ התקן. מאותו רגע תהליך יכול לפתוח אותו, לשמור מתאר, לבצע פעולה במקביל לתהליך אחר ולהמשיך להחזיק במתאר גם כשההתקן הפיזי נעלם. הסרת השם ממערכת הקבצים אינה מבטלת אוטומטית הפניות שכבר נפתחו. לכן נפריד בין חיי הרישום, חיי קוד המודול, חיי ההתקן וחיי כל פתיחה.
נבנה התקן תוכנה קטן שמחזיק תא הודעה אחד לכל פתיחה. הממשק יאפשר כתיבה, קריאה, בדיקת מוכנות והפסקה לוגית. הוא אינו דרייבר חומרה ואינו טוען שכל ניתוק פיזי כבר טופל. הוא מספק חוזה חיים מדויק שעליו נוכל לבנות בפרויקט תור אירועים משותף.
החוזה של תא ההודעה¶
כל כתיבה מכילה בין בית אחד לשישים וארבעה בתים. כתיבה לתא מלא ממתינה או מחזירה שגיאת מוכנות בהתאם למצב המתאר. קריאה לתא ריק פועלת באותה צורה. באפר קריאה קטן מדי אינו צורך את ההודעה. הפסקה לוגית מונעת כתיבות חדשות, מאפשרת לקרוא הודעה שכבר קיימת ואז מחזירה סוף זרם.
הקוד הבא הוא מודול מלא בשם slot_probe.c לגרסת הייחוס. נבנה אותו עם obj-m += slot_probe.o ונבדוק באורח המעבדה. קובץ ההתקן נגיש למנהל בתוך האורח כדי שלא להוסיף כרגע ניהול הרשאות משתמשים למערכת המינימלית.
#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/poll.h>
#include <linux/mutex.h>
#include <linux/ioctl.h>
#include <linux/string.h>
#define AMIT_STOP _IO('A', 1)
#define AMIT_MAX 64
struct slot {
struct mutex lock;
wait_queue_head_t wait;
bool full;
bool dead;
size_t length;
unsigned char bytes[AMIT_MAX];
};
static int slot_open(struct inode *inode, struct file *file)
{
struct slot *slot = kzalloc(sizeof(*slot), GFP_KERNEL_ACCOUNT);
if (!slot)
return -ENOMEM;
mutex_init(&slot->lock);
init_waitqueue_head(&slot->wait);
file->private_data = slot;
return nonseekable_open(inode, file);
}
static int slot_release(struct inode *inode, struct file *file)
{
kfree(file->private_data);
return 0;
}
static ssize_t slot_read(struct file *file, char __user *buffer,
size_t count, loff_t *offset)
{
struct slot *slot = file->private_data;
ssize_t result;
if (!count)
return 0;
for (;;) {
if (mutex_lock_interruptible(&slot->lock))
return -ERESTARTSYS;
if (slot->full)
break;
if (slot->dead) {
mutex_unlock(&slot->lock);
return 0;
}
mutex_unlock(&slot->lock);
if (file->f_flags & O_NONBLOCK)
return -EAGAIN;
result = wait_event_interruptible(slot->wait,
READ_ONCE(slot->full) || READ_ONCE(slot->dead));
if (result)
return result;
}
if (count < slot->length)
result = -EMSGSIZE;
else if (copy_to_user(buffer, slot->bytes, slot->length))
result = -EFAULT;
else {
result = slot->length;
WRITE_ONCE(slot->full, false);
}
mutex_unlock(&slot->lock);
wake_up_interruptible_poll(&slot->wait, EPOLLOUT | EPOLLWRNORM);
return result;
}
static ssize_t slot_write(struct file *file, const char __user *buffer,
size_t count, loff_t *offset)
{
struct slot *slot = file->private_data;
unsigned char temporary[AMIT_MAX];
int error;
if (!count)
return 0;
if (count > AMIT_MAX)
return -EMSGSIZE;
if (copy_from_user(temporary, buffer, count))
return -EFAULT;
for (;;) {
if (mutex_lock_interruptible(&slot->lock))
return -ERESTARTSYS;
if (slot->dead) {
mutex_unlock(&slot->lock);
return -ENODEV;
}
if (!slot->full)
break;
mutex_unlock(&slot->lock);
if (file->f_flags & O_NONBLOCK)
return -EAGAIN;
error = wait_event_interruptible(slot->wait,
!READ_ONCE(slot->full) || READ_ONCE(slot->dead));
if (error)
return error;
}
memcpy(slot->bytes, temporary, count);
slot->length = count;
WRITE_ONCE(slot->full, true);
mutex_unlock(&slot->lock);
wake_up_interruptible_poll(&slot->wait, EPOLLIN | EPOLLRDNORM);
return count;
}
static __poll_t slot_poll(struct file *file, poll_table *table)
{
struct slot *slot = file->private_data;
__poll_t mask = 0;
poll_wait(file, &slot->wait, table);
mutex_lock(&slot->lock);
if (slot->full)
mask |= EPOLLIN | EPOLLRDNORM;
if (!slot->dead && !slot->full)
mask |= EPOLLOUT | EPOLLWRNORM;
if (slot->dead)
mask |= EPOLLHUP;
mutex_unlock(&slot->lock);
return mask;
}
static long slot_ioctl(struct file *file, unsigned int command, unsigned long argument)
{
struct slot *slot = file->private_data;
if (command != AMIT_STOP)
return -ENOTTY;
mutex_lock(&slot->lock);
WRITE_ONCE(slot->dead, true);
mutex_unlock(&slot->lock);
wake_up_interruptible_poll(&slot->wait, EPOLLIN | EPOLLOUT | EPOLLHUP);
return 0;
}
static const struct file_operations operations = {
.owner = THIS_MODULE,
.open = slot_open,
.release = slot_release,
.read = slot_read,
.write = slot_write,
.poll = slot_poll,
.unlocked_ioctl = slot_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = slot_ioctl,
#endif
.llseek = no_llseek,
};
static struct miscdevice device = {
.minor = MISC_DYNAMIC_MINOR,
.name = "amit_slot",
.fops = &operations,
.mode = 0600,
};
static int __init slot_init(void) { return misc_register(&device); }
static void __exit slot_exit(void) { misc_deregister(&device); }
module_init(slot_init);
module_exit(slot_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Amit per-open message slot");
בעלות של פתיחה אחת¶
כל פתיחה מקבלת אובייקט משלה, ולכן שתי פתיחות בלתי תלויות אינן מתקשרות דרך התא. שכפול מתאר של אותה פתיחה כן משתף את מצב הקובץ הפרטי. הקריאה האחרונה לשחרור הקובץ מחזירה את הזיכרון, והפניות של פעולות פעילות מנוהלות על ידי שכבת הקבצים. אין לשחרר את המצב מתוך פקודת ההפסקה הלוגית, משום שעדיין קיימים מתארים שיכולים לקרוא אותו.
השדה .owner קושר שימוש בפעולות הקובץ לחיי קוד המודול. ניסיון לפרוק את המודול בזמן שמתאר מחזיק אותו אינו תרגול של ניתוק פיזי. ניתוק חומרה יכול להתרחש גם כאשר הקוד נשאר טעון, ואז צריך לסמן מצב ניתוק, להעיר ממתינים ולשמור אובייקט תוכנה עד שההפניות האחרונות מסתיימות.
מוכנות אינה צריכה¶
פונקציית הבדיקה רושמת קודם את מקור היקיצה ואז קוראת מצב תחת הנעילה. הסדר נועד למנוע חלון שבו מצב משתנה אחרי בדיקה אבל לפני רישום ההמתנה. גם לאחר חזרה עם מוכנות, תהליכון אחר יכול לצרוך את ההודעה לפני הקריאה שלנו. לכן הקריאה עצמה עדיין בודקת מחדש את המצב ומטפלת בחוסר מוכנות.
הפסקה עם הודעה בתא מחזירה גם אפשרות קריאה וגם סיום. הלקוח צריך לנקז את ההודעה ואז לקבל סוף זרם. הסימן לסיום אינו פקודה למחוק מידע שכבר הובטח במסגרת החוזה. זהו אותו עיקרון שראינו בסגירת חיבורי רשת, אבל כאן אנו מגדירים בעצמנו את התנהגות הדרייבר.
העתקה תחת נעילה והמחיר שלה¶
הכתיבה מעתיקה לבאפר זמני לפני רכישת הנעילה, ואז מפרסמת הודעה שלמה. הקריאה שומרת את ההודעה אם ההעתקה למשתמש נכשלת, ולכן היא מחזיקה נעילה ממתינה לאורך ההעתקה. ההקשר מאפשר שינה, אבל משתמש שמצליח לעכב טיפול בדף שלו יכול להאריך את החזקת הנעילה של אותה פתיחה.
זאת מגבלה מכוונת של הממשק הפשוט, ולא טענה שזהו תכנון מתאים לכל דרייבר. תכנון מתקדם יכול להשתמש ברשומה בבעלות זמנית או להגדיר שהודעה נצרכת לפני ההעתקה, אך עליו להכריע מה קורה בכשל ומה רואים קוראים אחרים. הוצאת ההעתקה מהנעילה בלי מודל בעלות על ההודעה תיצור מרוץ חדש.
לקוח שמוכיח את חוזה ההפסקה¶
הלקוח הבא משתמש בתלות libc = "0.2". ערך הפקודה תואם את ההגדרה ללא ארגומנט בממשק המעבדה של לינוקס על x86_64. במוצר עם כמה יעדים נייצר או נשתף הגדרות ממשק במקום להניח שמספרים שנכתבו ידנית ניידים לכל ארכיטקטורה.
use std::fs::OpenOptions;
use std::io::{self, Read, Write};
use std::os::fd::AsRawFd;
fn main() -> io::Result<()> {
let mut file = OpenOptions::new().read(true).write(true).open("/dev/amit_slot")?;
file.write_all(b"hello")?;
let mut bytes = [0_u8; 5];
file.read_exact(&mut bytes)?;
assert_eq!(&bytes, b"hello");
if unsafe { libc::ioctl(file.as_raw_fd(), 0x4101 as libc::c_ulong) } == -1 {
return Err(io::Error::last_os_error());
}
assert_eq!(file.read(&mut bytes)?, 0);
assert_eq!(file.write(b"x").unwrap_err().raw_os_error(), Some(libc::ENODEV));
Ok(())
}
ממשק בינארי שנשאר לאורך זמן¶
ממשק פקודות מורכב צריך להגדיר גדלי שדות, יישור, בתים שמורים וטיפול במבנים מגרסה אחרת. נימנע מחשיפת מצביעי קרנל ומטיפוסים שגודלם משתנה בלי הסבר. נאתחל כל בית שמוחזר למשתמש כדי שלא להדליף תוכן דרך ריפוד שלא נכתב.
שינוי משמעות של מספר פקודה קיים יכול לשבור לקוח ישן גם אם קוד הקרנל עדיין מתקמפל. עדיף לתכנן פקודה חדשה כאשר החוזה משתנה, ולשמור התנהגות מוגדרת לפקודה הישנה. הדוגמה משתמשת בפקודה פשוטה ללא מבנה כדי לבודד את חיי ההפסקה, ואת הממשק המורכב נרחיב בפרויקט.
פרסום ההתקן מתועד בממשק ההתקנים הפשוטים, וכללי פקודות הבקרה מפורטים בתכנון הממשק הבינארי. ההצלחה החשובה אינה רק הופעת קובץ תחת /dev, אלא היכולת להסביר מי מחזיק בכל מצב ואיך כל ממתין יוצא בזמן סיום.