The image below shows all the connections between I/O and Notifier components relevant to the generation and handling of file events. A textual explanation follows behind the image.
Note: The image shown here is down-scaled to fit most screens. Click on the image itself to get it in full size.
File events are setup by a tcl script using the command fileevent. This in turn internally sets up the necessary data structures for ChannelHandlers and EventScripts and also records the interest in events using UpdateInterest. The main action of UpdateInterest is to compute which events are actually requested and to deliver this information to the (platform-dependent) channel driver for the channel. The driver then uses Tcl_CreateFileHandler to record this fact in the event source for file events inside of the notifier. It also passes a reference to Tcl_NotifyChannel as the designed callback. Tcl_CreateFileHandler just records all this information in a FileHandler structure and causes a recomputation of the select masks used later. Beyond that it does nothing.
The generation and handling of events essentially begins with Tcl_DoOneEvent which uses Tcl_ServiceEvent to dispatch the first event found in the event queue. Only if the event queue is empty it will try and use the other parts of the notifier to wait for more events coming from the outside. This behaviour of Tcl_DoOneEvent means that the creation and queueing of events inside of an event handler is a Bad Thing(TM) as this will drown out the generation of any other events like for channels and timers.
To generate events in case of an empty queue Tcl_DoOneEvent will first ask all registered event sources to setup themselves, followed by a Tcl_WaitForEvent. When that command returns all registered event sources are asked to queue any events they have detected during the Tcl_WaitForEvent.
In the unix implementation of Tcl_WaitForEvent the select system call is used to wait on file ids and/or a timeout. This means that there is no separate event source for file events, it is integrated with the notifier. The queued events contain a reference to a FileHandlerEventProc as their callback and Tcl_ServiceEvent will invoke this callback when the event reaches the front of the event queue. FileHandlerEventProc will then look through the registered FileHandler structures for the channel to be notified and invokes the callback recorded there, which is Tcl_NotifyChannel.
The above means that the moment Tcl_NotifyChannel is called the system can assume that it is inside of an executing event handler. Tcl_NotifyChannel does not queue events, it is called when queued events are actually dispatched.
There is a second path generating file events. Actually they are timer events but as they result in the execution of Tcl_NotifyChannel at the end of the path the generic part of the I/O system (and the tcl scripts called by it) see them as file events. This path begins in UpdateInterest which creates a timer via Tcl_CreateTimer when the input buffer of the channel contains data and there is interest in readable events. The new timer is passed a reference to ChannelTimerProc and records it in the TimerHandler structure created by Tcl_CreateTimer.
Note: While the timer is active the generic I/O system will tell the involved channel driver that there is no interest in the real thing, i.e. read events from the OS. This interest will be reinstated by the timer when he finds that he has drained the buffers.
The timer management is a separate event source and provides TimerSetupProc and TimerCheckProc as the interface to the generic notifier. When they are called was described earlier.
TimerSetupProc determines if there are pending timers, computes a timeout value and transfers it into the notifier via Tcl_SetMaxBlockTime. This information is used by the select inside of Tcl_WaitForEvent to return if there were no status changes on the watched ids during that time.
TimerCheckProc determines if one or more timers expired and (like Tcl_WaitForEvent) uses Tcl_QueueEvent to queue the appropriate events, passing them a reference to TimerEventProc as their callback. When such an event arrives at the front of the queue Tcl_ServiceEvent executes the callback, TimerEventProc, which in turn executes the callback recorded in the associated TimerHandler structure.
In our case this is ChannelTimerProc. This procedure checks if the buffers are now drained and invokes Tcl_NotifyChannel to initiate the usual processing of a file event if not. It will also recreate the timer in that case, before the invocation of Tcl_NotifyChannel. In the case of finding empty buffers ChannelTimerProc will call UpdateInterest to reinstate the interest in the real read events from the OS.