מה באמת קורה כשלוחצים על מקש?

מדריך מעשי לקלט, פלט והתקני חומרה: מקלדות, drivers, interrupts, buffers, backpressure, DMA, latency, flush, ADC וקריאות חלקיות.

פורסם 2026-07-13. תגיות: Hardware, I/O, Systems.

כשאנחנו לוחצים על מקש, מצלמים תמונה או שומרים קובץ, הפעולה נראית כמעט מיידית. בפועל, המידע עובר דרך שרשרת של רכיבי חומרה, בקרים, דרייברים, תורים ומנגנונים של מערכת ההפעלה.

הבנת השרשרת הזאת חשובה לא רק למי שכותב דרייברים. היא מסבירה מדוע קריאת קובץ יכולה להחזיר רק חלק מהמידע, למה תוכנת וידאו צוברת פיגור, מדוע `flush()` אינו מבטיח שמידע הגיע פיזית לדיסק, ואיך תוכנה יכולה להתמודד עם התקן שמייצר מידע מהר מכפי שהיא מסוגלת לעבד.

מה הם קלט ופלט?

קלט הוא מידע שנכנס אל המערכת שבחרנו לבחון. פלט הוא מידע שיוצא ממנה.

מנקודת המבט של אפליקציה:

הסיווג תלוי בגבולות המערכת. כונן SSD הוא התקן פלט כאשר כותבים אליו, והתקן קלט כאשר קוראים ממנו. מסך מגע הוא התקן פלט כאשר הוא מציג תמונה והתקן קלט כאשר הוא מזהה אצבע.

המסלול מלחיצה פיזית ועד האפליקציה

לחיצה על מקש אינה מגיעה ישירות לקוד שלנו.

המסלול המפושט הוא:

לחיצה פיזית
↓
הבקר בתוך המקלדת מזהה שינוי במטריצת המקשים
↓
המקלדת יוצרת דוח USB
↓
בקר ה-USB במחשב מקבל את הדוח
↓
הבקר עשוי להפעיל interrupt למעבד
↓
מערכת ההפעלה וה-driver מפרשים את הדוח
↓
האירוע נשמר בתור קלט
↓
מערכת ההפעלה מנתבת אותו לאפליקציה
↓
האפליקציה מקבלת אירוע Key Press

המקלדת אינה שולחת בדרך כלל את האות `A` כפי שהיא מופיעה במסמך. היא שולחת דוח שמכיל קוד מקש ומידע נוסף, למשל האם Shift לחוץ. מערכת ההפעלה, פריסת המקלדת והאפליקציה משתתפות בפירוש האירוע.

מה תפקידו של ה-driver?

התקן פיזי מדבר בשפה של registers, פקודות ופרוטוקולים. האפליקציה מעוניינת בממשק פשוט יותר: לקרוא, לכתוב, לצלם או לקבל אירוע.

ה-driver הוא שכבת התוכנה שמכירה את ההתקן או את תקן התקשורת שלו. הוא מתרגם בין הממשק שמציגה מערכת ההפעלה לבין הפעולות שהבקר הפיזי מבין.

האפליקציה אינה אמורה לדעת באיזו כתובת נמצא register מסוים או כיצד נראה דוח USB. היא עובדת מול הפשטה שמערכת ההפעלה מספקת.

Polling לעומת Interrupts

אחת הדרכים לגלות שהתקן מוכן היא polling: המעבד או הבקר בודקים שוב ושוב האם הגיע מידע.

יש מידע?
לא.
יש מידע?
לא.
יש מידע?
כן.

היתרון הוא פשטות ושליטה. החיסרון הוא שהמערכת עלולה לבזבז זמן מעבד על בדיקות חוזרות.

Interrupt מאפשר לבקר להודיע למעבד שאירע משהו. במקום לבדוק ללא הפסקה, המעבד ממשיך לעבוד ומקבל התראה בעת הצורך.

במערכות אמיתיות משתמשים לעיתים בשילוב. לדוגמה, מארח USB מתשאל את התקני ה-USB לפי לוח זמנים, ולאחר שבקר ה-USB קיבל מידע הוא יכול להפעיל interrupt למעבד.

גם interrupt אינו חינם. אם התקן יפעיל התראה על כל בית בודד, מספר ההתראות עלול להיות עצום. לכן מערכות משתמשות ב-buffers, באצוות ולעיתים במעבר זמני ל-polling תחת עומס גבוה.

למה צריך Buffer?

Buffer הוא אזור אחסון זמני בין יצרן מידע לבין צרכן מידע.

מצלמה → buffer → אפליקציה

המצלמה יכולה לייצר frame בזמן שהאפליקציה עדיין מעבדת את הקודם. ה-buffer מאפשר לשמור זמנית את המידע ולהחליק הבדלים קצרים בקצב.

הוא מועיל כאשר העומס זמני:

במשך רגע קצר היצרן מהיר יותר
↓
המידע מצטבר ב-buffer
↓
לאחר מכן הצרכן משלים את הפער

אבל buffer אינו פותר פער קבוע.

נניח שמצלמה מפיקה 60 frames בשנייה, והאפליקציה מסוגלת לעבד רק 20. בכל שנייה מצטברים 40 frames:

60 - 20 = 40

אם התור בלתי מוגבל, הזיכרון ילך ויתמלא. במקביל כל frame ימתין זמן רב יותר לעיבוד, ולכן ה-latency יגדל. אחרי כמה דקות התצוגה עשויה להציג את העבר במקום את מה שקורה עכשיו.

Backpressure: כשהצרכן אינו עומד בקצב

Backpressure הוא מנגנון שמגביל את היצרן או את כמות המידע שבתור כאשר הצרכן אינו מסוגל לעמוד בקצב.

המדיניות יכולה להיות:

הבחירה תלויה במשמעות המידע.

בתצוגת וידאו חיה לרוב עדיף למחוק frames ישנים ולהציג את החדש ביותר. אין ערך רב בהצגת frame מלפני עשר שניות.

בהקלטת מצלמת אבטחה ייתכן שאסור לאבד מידע. במקרה כזה צריך מערכת אחסון, קידוד ועיבוד שמסוגלת לעמוד בקצב. שמירה לדיסק אינה backpressure בפני עצמה, ואם הדיסק איטי מדי הפיגור פשוט יעבור לתור אחר.

DMA: העברת מידע בלי שהמעבד יעתיק כל בית

ללא DMA, המעבד היה עלול להשתתף ישירות בהעתקה של כמויות מידע גדולות מהתקן לזיכרון.

ב-DMA התהליך נראה כך:

ה-driver מכין buffer ב-RAM
↓
המעבד מגדיר לבקר כתובת ואורך
↓
ההתקן מעביר את המידע ישירות ל-RAM
↓
בסיום הבקר מפעיל interrupt
↓
מערכת ההפעלה משלימה את הבקשה

המעבד עדיין מעורב בהכנה, בהרשאות, בניהול ובטיפול בסיום. הוא פשוט אינו מעתיק בעצמו כל בית.

DMA חשוב במיוחד בכוננים, בכרטיסי רשת, בכרטיסי קול ובמצלמות שמעבירים כמויות גדולות של מידע.

מחבר, Bus ופרוטוקול אינם אותו דבר

קל להתבלבל בין שלושה מושגים:

מחבר פיזי → הצורה שאליה מחברים את הכבל
Bus או קישור → נתיב התקשורת
פרוטוקול → כללי השיחה

USB-C, למשל, מתאר בעיקר מחבר פיזי. שני כבלים שנראים זהים אינם בהכרח תומכים באותן מהירויות או באותם פרוטוקולים.

פרוטוקול מגדיר כיצד נראית הודעה, מי רשאי לשלוח, כיצד מזהים שגיאה, כיצד ההתקן מזוהה ומה משמעות הנתונים.

כאשר מחברים התקן USB, מערכת ההפעלה מבצעת enumeration: היא מזהה שהגיע התקן, מקצה לו כתובת על קישור ה-USB, קוראת את ה-descriptors שלו ובוחרת driver מתאים.

הכתובת הזאת אינה כתובת RAM. היא מזהה שמאפשר למארח לפנות להתקן הנכון על הקישור.

Latency, Throughput ו-IOPS

שלושה מדדים מתארים היבטים שונים של ביצועי I/O.

Latency הוא הזמן שלוקח לפעולה בודדת לקבל מענה.

Throughput הוא כמות המידע שמעובדת ביחידת זמן.

IOPS הוא מספר פעולות הקלט והפלט בשנייה.

מערכת יכולה להעביר 5GB בשנייה ועדיין לגרום לכל בקשה להמתין שתי שניות. במקרה כזה ה-throughput גבוה, אבל ה-latency גרוע.

אצוות, או batching, מאחדות פעולות קטנות לפעולה גדולה יותר. הדבר מפחית תקורה ומשפר בדרך כלל throughput, אבל עלול להגדיל latency משום שהפריט הראשון מחכה עד שהאצווה תהיה מוכנה.

מה באמת עושה `flush()`?

כאשר כותבים באמצעות writer או stream ממוסגר, המידע עשוי להישאר זמנית ב-buffer.

writer.write("hello");
writer.flush();

`flush()` מבקש להעביר מידע שנצבר לשכבה שמתחת, אך אינו סוגר את המשאב ואינו מבטיח בהכרח שהמידע הגיע פיזית למדיית האחסון.

המידע עשוי לעבור דרך כמה שכבות:

Java buffer
↓
מערכת ההפעלה
↓
page cache
↓
תור ההתקן
↓
בקר הכונן
↓
מדיית האחסון

אחרי `flush()` אפשר להמשיך לכתוב. אחרי `close()` המשאב נסגר ואסור להמשיך להשתמש בו.

העולם הפיזי הוא אנלוגי

מיקרופון, חיישן טמפרטורה וחיישן אור אינם מתחילים ממספרים דיגיטליים. הם מפיקים אות חשמלי שמשתנה באופן רציף.

ADC, או Analog-to-Digital Converter, דוגם את האות וממיר אותו למספרים.

תופעה פיזית
↓
אות חשמלי אנלוגי
↓
ADC
↓
דגימות דיגיטליות
↓
buffer
↓
תוכנה

קצב הדגימה קובע כמה פעמים בשנייה מודדים. הרזולוציה קובעת כמה ערכים שונים ניתן לייצג בכל מדידה.

קצב דגימה גבוה יותר מייצר יותר מידע ודורש יותר אחסון, רוחב פס ועיבוד.

DAC מבצע את הפעולה ההפוכה. הוא ממיר דגימות דיגיטליות לאות אנלוגי שיכול להפעיל רמקול.

רעש, ממוצע והמחיר של יציבות

חיישן אמיתי אינו מחזיר תמיד את אותו ערך:

25.01
24.98
25.03
25.00

אפשר להפחית רעש באמצעות ממוצע נע. ככל שהחלון גדול יותר, התוצאה יציבה יותר, אך היא מגיבה לאט יותר לשינוי אמיתי.

אם החיישן נדגם 100 פעמים בשנייה ומחשבים ממוצע של 20 דגימות, החלון מייצג:

20 / 100 = 0.2 שנייה

כאשר הטמפרטורה קופצת מ-25 ל-30 מעלות, הממוצע לא קופץ מיד. הוא מתקרב בהדרגה לערך החדש, משום שבחלון עדיין נשארות דגימות ישנות.

זהו trade-off קלאסי:

חלון גדול → פחות רעש, יותר השהיה
חלון קטן → תגובה מהירה, פחות יציבות

קריאה אחת אינה מבטיחה לקבל הכול

הקוד הבא אינו מבטיח שכל המערך יתמלא:

byte[] buffer = new byte[1024];
int bytesRead = input.read(buffer);

`bytesRead` יכול להיות `240`. במקרה כזה רק:

buffer[0] עד buffer[239]

שייכים לקריאה הנוכחית.

שאר המערך עשוי להכיל אפסים או מידע מקריאה קודמת. לכן לולאת העתקה נכונה נראית כך:

byte[] buffer = new byte[8192];
int bytesRead;

while ((bytesRead = input.read(buffer)) != -1) {
    output.write(buffer, 0, bytesRead);
}

הערך `-1` פירושו EOF: הזרם הסתיים ולא יגיע ממנו עוד מידע.

חשוב להבדיל:

EOF          → הזרם הסתיים
Timeout      → לא התקבלה תוצאה בזמן שהוגדר
IOException  → פעולת הקלט או הפלט נכשלה

EOF אינו בהכרח שגיאה.

Handle, File Descriptor וסגירת משאבים

כאשר תוכנה פותחת קובץ, socket או מצלמה, מערכת ההפעלה מחזירה לה handle או file descriptor שמייצג את המשאב הפתוח.

File descriptor אינו כתובת RAM ואינו מיקום בדיסק. הוא אינדקס בטבלת משאבים שמערכת ההפעלה מנהלת עבור התהליך.

משאב פתוח יכול להחזיק:

לכן חשוב לסגור אותו.

ב-Java משתמשים ב-try-with-resources:

try (
    InputStream input = Files.newInputStream(source);
    OutputStream output = Files.newOutputStream(target)
) {
    input.transferTo(output);
}

גם אם תתרחש חריגה, Java תנסה לסגור את המשאבים.

ה-Garbage Collector אינו תחליף ל-`close()`. הוא מנהל בעיקר זיכרון Java ואינו מבטיח שמשאב חיצוני ייסגר ברגע שסיימנו להשתמש בו.

העיקרון שמחבר הכול

קלט ופלט אינם רק קריאה וכתיבה. הם מפגש בין רכיבים שפועלים במהירויות שונות ובעלי מגבלות שונות.

התקן מייצר מידע, controller מנהל אותו, driver מתרגם אותו, מערכת ההפעלה מתזמנת ושומרת אותו ב-buffers, והאפליקציה קוראת או מעבדת אותו.

בכל שכבה צריך להתמודד עם אותן שאלות:

כאשר מבינים את השאלות האלה, תקלות שנראות אקראיות מתחילות להיות צפויות: פיגור בווידאו, זיכרון שמתמלא, קובץ שנכתב חלקית, התקן שנשאר תפוס או thread שממתין לנצח.

זו המשמעות האמיתית של הבנת קלט ופלט: לא רק לדעת לקרוא ל-`read()` או `write()`, אלא להבין את המערכת השלמה שמאחוריהן.