È una visione molto semplificata e, per i sistemi operativi moderni, errata. Tra la pressione del tasto e la getch()/getchar() ci passa una marea di astrazioni intermedie che a te non devono interessare.
Il modo più semplice di pensare la questione su un sistema moderno come Linux o Windows NT-like secondo me è questo:
- quando viene premuto un tasto, tramite infinite magie che a te non interessano (interrupt che notifica il driver di tastiera, conversione scancode=>carattere, eventuale giro per le input queue del window manager, dispatch alla finestra corretta, scrittura nell'stdin del programma e chi più ne ha più ne metta) questo "atterra" dentro al buffer del sistema operativo relativo allo standard input del tuo processo, e lì rimane;
- quando tu chiami la getchar(), la libreria C in primo luogo verifica se ci sono caratteri nel buffer tenuto dalla libreria standard (che sta a valle di quello del sistema operativo), e se c'è qualcosa pesca il carattere da restituire da lì;
- se il buffer è vuoto, cerca di riempirlo; per fare ciò, prova a leggere dal buffer del sistema operativo (con una read su sistemi POSIX o con ReadFile su sistemi Windows); normalmente si tratta di una chiamata bloccante - se nel buffer del sistema operativo c'è qualcosa questo viene restituito, altrimenti si ferma tutto finché non arrivano caratteri. Una volta che il buffer della libreria C è stato ri-riempito, torna al passo precedente.
In tutto questo ad un qualche livello (che dipende dal sistema operativo/dalla CRT/...) rientra anche il line-buffering, ovvero il fatto che vengono rese disponibili all'stdin del programma C solo righe intere.
Tutte le eccezioni a questo funzionamento (input non bloccante, lettura da terminale senza echo, lettura senza line-buffering, ...) si ottengono normalmente con meccanismi platform-specific (ioctl/sequenze di escape/... su POSIX, API di console su Windows).