The keyboard sends a scan code for every key event: a "make" code when a key goes down and a "break" code when it comes up, which on the PC is the make code with bit 7 set (Esc is 01h down, 81h up). Holding a key down repeats the make code. The keyboard controller puts the code in its output register, port 60h, and raises IRQ 1, which arrives as vector 09h. The keys added by the 101-key keyboard send an extra E0h byte first (a separate interrupt), and Pause sends a whole sequence.

What the BIOS handler does

StepWhat happens
1read the scan code from port 60h
2AT and later: call INT 15h AH=4Fh with it, so a hook can change or swallow the key
3Shift, Ctrl, Alt, the Lock keys: update the flags at 0040:0017h / 0018h, store nothing
4special combinations: Ctrl+Alt+Del reboots, Ctrl+Break calls INT 1Bh, Print Screen calls INT 05h, Pause waits
5other keys: translate to ASCII by the shift state and store scan code and ASCII at the buffer tail (see INT 16h)
6buffer full: beep and drop the key
7send the end of interrupt to the 8259 (OUT 20h, AL with AL = 20h) and IRET

On the PC and XT the handler must also acknowledge the key to the keyboard logic by pulsing bit 7 of port 61h; the AT's 8042 controller does that itself.

Where the vector points is easy to see; on a plain system it is the BIOS itself, in segment F000h:

prints: F000:E987 on DOSBox-X with nothing hooked, the address of the handler in the IBM BIOS too. A resident program that hooks the keyboard, such as KEYB, shows its own address instead.
MOV AX, 3509h ; DOS: get vector 09h
INT 21h ; ES:BX = the keyboard handler
MOV AX, ES
CALL PHEX4 ; the segment
MOV DL, ":"
MOV AH, 02h ; DOS: print the character in DL
INT 21h
MOV AX, BX
CALL PHEX4 ; the offset
PHEX4 PROC NEAR ; print AX as four hex digits; keeps every register
PUSH AX
MOV AL, AH
CALL PHEX2 ; the high byte first
POP AX
CALL PHEX2 ; then the low byte
RET
PHEX4 ENDP
PHEX2 PROC NEAR ; print AL as two hex digits; keeps every register
PUSH AX
PUSH CX
PUSH DX
MOV DH, AL ; keep the byte
MOV CL, 4
SHR AL, CL ; the high nibble
CALL HEXN1
MOV AL, DH
AND AL, 0Fh ; the low nibble
CALL HEXN1
POP DX
POP CX
POP AX
RET
HEXN1:
ADD AL, "0"
CMP AL, "9"
JBE HEXN2
ADD AL, 7 ; past "9": "A".."F"
HEXN2:
MOV DL, AL
MOV AH, 02h
INT 21h
RET
PHEX2 ENDP

Hooking the keyboard

Games hook 09h to see every press and release, which INT 16h never shows: they keep a table of which keys are down, so that holding two keys at once works. A hook that chains to the BIOS reads port 60h too (reading does not remove the code) and then jumps to the old handler, which does all the rest, EOI included. A hook that does not chain must do everything itself: read port 60h, acknowledge the keyboard on an XT, send the EOI, IRET, and nothing reaches the buffer. This shape counts presses of Esc and passes every key on:

Not runnable as is: it only does something while keys are pressed, and the examples here must not wait for one. Install it like the INT 1Ch handler of INT 08h and restore the vector before exiting.
KEY1 PROC FAR ; installed with INT 21h AX=2509h
PUSH AX
IN AL, 60h ; peek at the scan code
CMP AL, 01h ; Esc pressed (make code)?
JNE CHAIN1
INC CS:ESCS1 ; count it; DS is unknown, use CS
CHAIN1:
POP AX
JMP DWORD PTR CS:OLD9 ; the BIOS handler does the rest, EOI included
KEY1 ENDP
OLD9 DD 0 ; filled from INT 21h AX=3509h
ESCS1 DW 0

Pitfalls

A handler that neither chains nor sends the EOI locks the keyboard (and every lower interrupt) at the first key. Keys stored with INT 16h AH=05h never pass through INT 09h: they go straight into the buffer. A program that takes over the keyboard completely must restore the vector on every way out, and should leave the shift flags sane, or the user returns to DOS with a "stuck" Ctrl. For filtering keys while keeping the BIOS in charge, the INT 15h AH=4Fh hook is the lighter choice on AT-class machines.