199 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
			
		
		
	
	
			199 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
			Client Scheduling in X
 | 
						|
			    Keith Packard
 | 
						|
			       SuSE
 | 
						|
			     10/28/99
 | 
						|
 | 
						|
History:
 | 
						|
 | 
						|
Since the original X server was written at Digital in 1987, the OS and DIX
 | 
						|
layers shared responsibility for scheduling the order to service
 | 
						|
client requests.  The original design was simplistic; under the maximum
 | 
						|
first make it work, then make it work well, this was a good idea.  Now 
 | 
						|
that we have a bit more experience with X applications, it's time to
 | 
						|
rethink the design.
 | 
						|
 | 
						|
The basic dispatch loop in DIX looks like:
 | 
						|
 | 
						|
	for (;;)
 | 
						|
	{
 | 
						|
		nready = WaitForSomething (...);
 | 
						|
		while (nready--)
 | 
						|
		{
 | 
						|
			isItTimeToYield = FALSE;
 | 
						|
			while (!isItTimeToYield)
 | 
						|
			{
 | 
						|
				if (!ReadRequestFromClient (...))
 | 
						|
					break;
 | 
						|
				(execute request);
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
WaitForSomething looks like:
 | 
						|
 | 
						|
	for (;;)
 | 
						|
		if (ANYSET (ClientsWithInput))
 | 
						|
			return popcount (ClientsWithInput);
 | 
						|
		select (...)
 | 
						|
		compute clientsReadable from select result;
 | 
						|
		return popcount (clientsReadable)
 | 
						|
	}
 | 
						|
 | 
						|
ReadRequestFromClient looks like:
 | 
						|
 | 
						|
	if (!fullRequestQueued)
 | 
						|
	{
 | 
						|
		read ();
 | 
						|
		if (!fullRequestQueued)
 | 
						|
		{
 | 
						|
			remove from ClientsWithInput;
 | 
						|
			timesThisConnection = 0;
 | 
						|
			return 0;
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if (twoFullRequestsQueued)
 | 
						|
		add to ClientsWithInput;
 | 
						|
 | 
						|
	if (++timesThisConnection >= 10)
 | 
						|
	{
 | 
						|
		isItTimeToYield = TRUE;
 | 
						|
		timesThisConnection = 0;
 | 
						|
	}
 | 
						|
	return 1;
 | 
						|
 | 
						|
Here's what happens in this code:
 | 
						|
 | 
						|
With a single client executing a stream of requests:
 | 
						|
 | 
						|
	A client sends a packet of requests to the server.
 | 
						|
 | 
						|
	WaitForSomething wakes up from select and returns that client
 | 
						|
	to Dispatch
 | 
						|
 | 
						|
	Dispatch calls ReadRequestFromClient which reads a buffer (4K)
 | 
						|
	full of requests from the client
 | 
						|
 | 
						|
	The server executes requests from this buffer until it emptys,
 | 
						|
	in two stages -- 10 requests at a time are executed in the
 | 
						|
	inner Dispatch loop, a buffer full of requests are executed
 | 
						|
	because WaitForSomething immediately returns if any clients
 | 
						|
	have complete requests pending in their input queues.
 | 
						|
 | 
						|
	When the buffer finally emptys, the next call to ReadRequest
 | 
						|
	FromClient will return zero and Dispatch will go back to
 | 
						|
	WaitForSomething; now that the client has no requests pending,
 | 
						|
	WaitForSomething will block in select again.  If the client
 | 
						|
	is active, this select will immediately return that client
 | 
						|
	as ready to read.
 | 
						|
 | 
						|
With multiple clients sending streams of requests, the sequence
 | 
						|
of operations is similar, except that ReadRequestFromClient will
 | 
						|
set isItTimeToYield after each 10 requests executed causing the
 | 
						|
server to round-robin among the clients with available requests.
 | 
						|
 | 
						|
It's important to realize here that any complete requests which have been
 | 
						|
read from clients will be executed before the server will use select again
 | 
						|
to discover input from other clients.  A single busy client can easily
 | 
						|
monopolize the X server.
 | 
						|
 | 
						|
So, the X server doesn't share well with clients which are more interactive
 | 
						|
in nature.
 | 
						|
 | 
						|
The X server executes at most a buffer full of requests before again heading
 | 
						|
into select; ReadRequestFromClient causes the server to yield when the
 | 
						|
client request buffer doesn't contain a complete request.  When
 | 
						|
that buffer is executed quickly, the server spends a lot of time
 | 
						|
in select discovering that the same client again has input ready.  Thus
 | 
						|
the server also runs busy clients less efficiently than is would be
 | 
						|
possible.
 | 
						|
 | 
						|
What to do.
 | 
						|
 | 
						|
There are several things evident from the above discussion:
 | 
						|
 | 
						|
 1	The server has a poor metric for deciding how much work it
 | 
						|
	should do at one time on behalf of a particular client.
 | 
						|
 | 
						|
 2	The server doesn't call select often enough to detect less
 | 
						|
 	aggressive clients in the face of busy clients, especially
 | 
						|
	when those clients are executing slow requests.
 | 
						|
 | 
						|
 3	The server calls select too often when executing fast requests.
 | 
						|
 | 
						|
 4	Some priority scheme is needed to keep interactive clients
 | 
						|
 	responding to the user.
 | 
						|
 | 
						|
And, there are some assumptions about how X applications work:
 | 
						|
 | 
						|
 1	Each X request is executed relatively quickly; a request-granularity
 | 
						|
 	is good enough for interactive response almost all of the time.
 | 
						|
 | 
						|
 2	X applications receiving mouse/keyboard events are likely to
 | 
						|
 	warrant additional attention from the X server.
 | 
						|
 | 
						|
Instead of a request-count metric for work, a time-based metric should be
 | 
						|
used.  The server should select a reasonable time slice for each client
 | 
						|
and execute requests for the entire timeslice before yielding to
 | 
						|
another client.
 | 
						|
 | 
						|
Instead of returning immediately from WaitForSomething if clients have
 | 
						|
complete requests queued, the server should go through select each
 | 
						|
time and gather as many ready clients as possible.  This involves
 | 
						|
polling instead of blocking and adding the ClientsWithInput to
 | 
						|
clientsReadable after the select returns.
 | 
						|
 | 
						|
Instead of yielding when the request buffer is empty for a particular
 | 
						|
client, leave the yielding to the upper level scheduling and allow
 | 
						|
the server to try and read again from the socket.  If the client
 | 
						|
is busy, another buffer full of requests will already be waiting
 | 
						|
to be delivered thus avoiding the call through select and the
 | 
						|
additional overhead in WaitForSomething.
 | 
						|
 | 
						|
Finally, the dispatch loop should not simply execute requests from the
 | 
						|
first available client, instead each client should be prioritized with
 | 
						|
busy clients penalized and clients receiving user events praised.
 | 
						|
 | 
						|
How it's done:
 | 
						|
 | 
						|
Polling the current time of day from the OS is too expensive to
 | 
						|
be done at each request boundary, so instead an interval timer is
 | 
						|
set allowing the server to track time changes by counting invocations
 | 
						|
of the related signal handler.  Instead of using the wall time for
 | 
						|
this purpose, the process CPU time is used instead.  This serves
 | 
						|
two purposes -- first, it allows the server to consume no CPU cycles
 | 
						|
when idle, second it avoids conflicts with SIGALRM usage in other
 | 
						|
parts of the server code.  It's not without problems though; other
 | 
						|
CPU intensive processes on the same machine can reduce interactive
 | 
						|
response time within the X server.  The dispatch loop can now
 | 
						|
calculate an approximate time value using the number of signals
 | 
						|
received.  The granularity of the timer sets the scheduling jitter,
 | 
						|
at 20ms it's only occasionally noticeable.
 | 
						|
 | 
						|
The changes to WaitForSomething and ReadRequestFromClient are
 | 
						|
straightforward, adjusting when select is called and avoiding
 | 
						|
setting isItTimeToYield too often.
 | 
						|
 | 
						|
The dispatch loop changes are more extensive, now instead of
 | 
						|
executing requests from all available clients, a single client
 | 
						|
is chosen after each call to WaitForSomething, requests are
 | 
						|
executed for that client and WaitForSomething is called again.
 | 
						|
 | 
						|
Each client is assigned a priority, the dispatch loop chooses the
 | 
						|
client with the highest priority to execute.  Priorities are
 | 
						|
updated in three ways:
 | 
						|
 | 
						|
 1.	Clients which consume their entire slice are penalized
 | 
						|
 	by having their priority reduced by one until they
 | 
						|
	reach some minimum value.
 | 
						|
 | 
						|
 2.	Clients which have executed no requests for some time
 | 
						|
 	are praised by having their priority raised until they
 | 
						|
	return to normal priority.
 | 
						|
 | 
						|
 3.	Clients which receive user input are praised by having
 | 
						|
 	their priority rased until they reach some maximal
 | 
						|
	value, above normal priority.
 | 
						|
 | 
						|
The effect of these changes is to both improve interactive application
 | 
						|
response and benchmark numbers at the same time.
 |