סנכרון בקרנל - פתרון¶
תרגיל 1 - בחירת מנגנון סנכרון¶
א. מונה חבילות רשת (מתעדכן מ-interrupt handler)
פעולות אטומיות (atomic_t) - זה מונה פשוט שצריך רק increment. פעולה אטומית מספיקה ועובדת גם מהקשר פסיקה. אלטרנטיבה טובה: per-CPU counter - כל CPU סופר בנפרד, ורק כשרוצים את הסכום הכולל קוראים מכולם. זה אפילו יעיל יותר כי אין cache line bouncing בין מעבדים.
// אפשרות 1 - atomic:
atomic_t packet_count = ATOMIC_INIT(0);
// ב-interrupt handler:
atomic_inc(&packet_count);
// אפשרות 2 - per-CPU (עדיף):
DEFINE_PER_CPU(unsigned long, packet_count);
// ב-interrupt handler:
this_cpu_inc(packet_count);
ב. עץ עם הקצאת זיכרון בעדכון
mutex - כי העדכון כולל הקצאת זיכרון (kmalloc עם GFP_KERNEL), שיכולה לישון. ב-spinlock אסור לישון, אז mutex הוא הבחירה הנכונה. זה גם מתרחש רק ב-process context (יצירת קובץ ע"י משתמש).
static DEFINE_MUTEX(tree_mutex);
void create_file_entry(...)
{
mutex_lock(&tree_mutex);
struct node *n = kmalloc(sizeof(*n), GFP_KERNEL);
// ... insert to tree ...
mutex_unlock(&tree_mutex);
}
ג. טבלת ניתוב - קריאות תכופות, כתיבות נדירות
RCU - זה בדיוק ה-use case הקלאסי של RCU. הקריאות (lookup לכל חבילה) קורות אלפי פעמים בשנייה ולא צריכות מנעול בכלל. העדכונים הנדירים משלמים מחיר גבוה יותר (העתקה ו-grace period), אבל זה קורה רק לעיתים רחוקות. בפועל, זה בדיוק מה שלינוקס עושה עם טבלת הניתוב.
ד. מונה context switches לכל CPU
משתנים per-CPU - כל CPU סופר רק את ה-context switches שלו, אז אין צורך בסנכרון כלל. זה בדיוק מה שהקרנל עושה בפועל.
ה. רשימת חיבורים - גישה מ-interrupt handler ומ-process context
spinlock עם irqsave - כי interrupt handler צריך לגשת לרשימה (אסור mutex), וגם process context ניגש (צריך להגן מפני פסיקות). חייבים להשתמש ב-spin_lock_irqsave כדי למנוע דדלוק בין process context ל-interrupt handler.
DEFINE_SPINLOCK(conn_lock);
// ב-interrupt handler:
spin_lock(&conn_lock);
// ... search list ...
spin_unlock(&conn_lock);
// ב-process context:
spin_lock_irqsave(&conn_lock, flags);
// ... add/remove connection ...
spin_unlock_irqrestore(&conn_lock, flags);
תרגיל 2 - זיהוי דדלוקים¶
קטע א - דדלוק! (ABBA)¶
כן, יש סכנת דדלוק.
הסיטואציה:
1. Thread 1 נועל lock_a, מנסה לנעול lock_b
2. Thread 2 נועל lock_b, מנסה לנעול lock_a
3. שניהם מסתובבים לנצח - דדלוק!
תיקון: תמיד לנעול באותו סדר:
// Thread 2 - תוקן:
spin_lock(&lock_a); // אותו סדר כמו Thread 1
spin_lock(&lock_b);
// ... work ...
spin_unlock(&lock_b);
spin_unlock(&lock_a);
קטע ב - דדלוק! (פסיקה)¶
כן, יש סכנת דדלוק.
הסיטואציה:
1. my_process_func נועלת data_lock
2. בדיוק באותו רגע, מגיעה פסיקה על אותו CPU
3. my_irq_handler מנסה לנעול data_lock
4. ה-handler מסתובב, אבל הפונקציה שמחזיקה את המנעול לא יכולה לרוץ (כי ה-handler רץ על אותו CPU) - דדלוק!
תיקון: ב-process context, להשתמש ב-spin_lock_irqsave:
void my_process_func(void)
{
unsigned long flags;
spin_lock_irqsave(&data_lock, flags); // משבית פסיקות!
// ... read shared data ...
spin_unlock_irqrestore(&data_lock, flags);
}
כעת פסיקות מושבתות בזמן שהמנעול מוחזק, אז ה-handler לא יכול לרוץ באמצע.
קטע ג - תקין¶
אין סכנת דדלוק.
הקוד תקין - mutex יכול לישון, ו-kmalloc עם GFP_KERNEL יכול לישון, ושניהם ב-process context. מותר לישון כשמחזיקים mutex (בניגוד ל-spinlock). הקוד גם מטפל נכון בכשלון (משחרר את ה-mutex לפני return).
קטע ד - שגוי! (לא דדלוק, אבל באג)¶
זו שגיאה, לא בהכרח דדלוק.
mutex_lock יכול לישון, ו-interrupt handler לא יכול לישון (הוא לא שייך לתהליך, אז אין לו process context שניתן להרדים). זה יגרום ל-kernel oops/panic.
תיקון: להשתמש ב-spinlock (עם irqsave אם צריך):
void my_interrupt_handler(int irq, void *dev)
{
spin_lock(&my_spinlock);
// ... update data ...
spin_unlock(&my_spinlock);
}
תרגיל 3 - השלמת קוד עם spinlock¶
#include <linux/list.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
struct message {
struct list_head list;
char data[256];
int priority;
};
static LIST_HEAD(message_queue);
static DEFINE_SPINLOCK(queue_lock); /* spinlock כי get_message נקראת מ-interrupt */
int add_message(const char *data, int priority)
{
struct message *msg;
unsigned long flags;
/* GFP_ATOMIC כי ייתכן שנקראים מהקשר שלא יכול לישון,
או כי ננעול spinlock מיד אחרי. אם add_message נקראת
רק מ-process context, אפשר GFP_KERNEL ולהקצות לפני הנעילה. */
msg = kmalloc(sizeof(*msg), GFP_ATOMIC);
if (!msg)
return -ENOMEM;
strscpy(msg->data, data, sizeof(msg->data));
msg->priority = priority;
spin_lock_irqsave(&queue_lock, flags);
list_add_tail(&msg->list, &message_queue);
spin_unlock_irqrestore(&queue_lock, flags);
return 0;
}
struct message *get_message(void)
{
struct message *msg = NULL;
unsigned long flags;
spin_lock_irqsave(&queue_lock, flags);
if (!list_empty(&message_queue)) {
msg = list_first_entry(&message_queue, struct message, list);
list_del(&msg->list);
}
spin_unlock_irqrestore(&queue_lock, flags);
return msg;
}
הסבר:
- השתמשנו ב-spinlock כי get_message נקראת מ-interrupt handler (שלא יכול לישון)
- השתמשנו ב-irqsave כי add_message עלולה להיקרא מ-process context ואז פסיקה שקוראת ל-get_message תיצור דדלוק
- השתמשנו ב-GFP_ATOMIC כי ההקצאה קורית בהקשר שצריך spinlock (אם ההקצאה תמיד מ-process context, אפשר להקצות לפני הנעילה עם GFP_KERNEL)
תרגיל 4 - RCU¶
struct config {
int max_connections;
int timeout_ms;
char server_name[64];
struct rcu_head rcu;
};
static struct config __rcu *current_config;
// callback לשחרור אחרי grace period:
static void config_free_rcu(struct rcu_head *head)
{
struct config *cfg = container_of(head, struct config, rcu);
kfree(cfg);
}
// קריאת הגדרות:
int get_timeout(void)
{
int timeout;
rcu_read_lock();
struct config *cfg = rcu_dereference(current_config);
if (cfg)
timeout = cfg->timeout_ms;
else
timeout = -1; // ברירת מחדל אם אין הגדרות
rcu_read_unlock();
return timeout;
}
// עדכון הגדרות:
int update_config(int max_conn, int timeout, const char *name)
{
struct config *new_cfg, *old_cfg;
new_cfg = kmalloc(sizeof(*new_cfg), GFP_KERNEL);
if (!new_cfg)
return -ENOMEM;
new_cfg->max_connections = max_conn;
new_cfg->timeout_ms = timeout;
strscpy(new_cfg->server_name, name, sizeof(new_cfg->server_name));
// מחליפים את המצביע (אטומית):
old_cfg = rcu_dereference_protected(current_config,
lockdep_is_held(&config_update_mutex));
rcu_assign_pointer(current_config, new_cfg);
// משחררים את ההגדרות הישנות אחרי grace period:
if (old_cfg)
call_rcu(&old_cfg->rcu, config_free_rcu);
return 0;
}
הסבר:
- rcu_read_lock() / rcu_read_unlock() - רק משביתים/מפעילים preemption. אפס overhead.
- rcu_dereference() - קוראת את המצביע בצורה בטוחה (מבטיחה שהקומפיילר וה-CPU לא יסדרו מחדש את הקריאה)
- rcu_assign_pointer() - מעדכנת את המצביע באופן אטומי עם memory barrier
- call_rcu() - רושמת callback שייקרא אחרי שכל הקוראים הקיימים סיימו (grace period). רק אז בטוח לשחרר את הזיכרון הישן.
הערה: בפועל, צריך גם mutex על פעולות הכתיבה כדי למנוע מצב ששני writers מתחרים. הוספתי התייחסות ל-config_update_mutex ב-rcu_dereference_protected.
תרגיל 5 - ניתוח lockdep¶
א. אילו שני מנעולים מעורבים?
table_lock- מנעול על טבלת הניתובsocket_lock- מנעול על ה-socket
ב. סדר הנעילה שגורם לבעיה:
נתיב 1 (process_packet):
1. נועל socket_lock (ב-process_packet+0x15)
2. מנסה לנעול table_lock (ב-update_routing_table+0x28)
נתיב 2 (update_routing_table):
1. נועל table_lock (ב-update_routing_table+0x28)
2. קורא ל-send_notification שנועל socket_lock (ב-send_notification+0x35)
כלומר: נתיב 1 נועל socket -> table, ונתיב 2 נועל table -> socket.
ג. למה זה עלול ליצור דדלוק?
זה דדלוק ABBA קלאסי:
1. Thread A (process_packet) נועל socket_lock, מנסה לנעול table_lock
2. Thread B (update_routing_table) נועל table_lock, מנסה לנעול socket_lock
3. שניהם ממתינים אחד לשני לנצח
ד. איך לתקן?
יש כמה אפשרויות:
-
לקבוע סדר נעילה אחיד - למשל, תמיד
table_lockלפניsocket_lock. אז ב-process_packetצריך לנעולtable_lockלפניsocket_lock. -
ב-
update_routing_table, לא לקרוא ל-send_notificationבתוך הנעילה - לשחרר אתtable_lockלפני שליחת ההודעה, או לדחות את ההודעה לאחרי שחרור המנעול. -
לאחד את שני המנעולים למנעול אחד - אם הם תמיד משמשים יחד, אולי אפשר מנעול אחד (אבל זה מקטין את ה-concurrency).
אפשרות 2 היא בדרך כלל הטובה ביותר - לצמצם את העבודה שנעשית בתוך הנעילה.