Multithreading the KDC server - a design
Rahul Srinivas
srahul at novell.com
Wed Apr 6 00:32:39 EDT 2005
Hi Ken,
Thanks for your comments.
> First, you're talking a lot about 1.3.5.
The reason I am talking about version 1.3.5 is that at Novell we are
working on the 1.3.5 codebase to integrate it with eDirectory. The
development work started before the release of version 1.4. So, we have
stuck to version 1.3.5. But, our goal is to port the changes to version
1.4 before submitting the patch.
> A bunch of changes went into
> library code in 1.4 to make it thread-safe, so 1.4 would be a much
> better starting point for the work. Otherwise you wind up having to
> pull in a lot of the changes between 1.3.5 and 1.4 anyways, or redo a
> bunch of the work.
I have implemented a prototype of the proposed design. I have pulled in
multi-threading code from version 1.4 into 1.3.5. The changes to the
library code have also been back-ported.
> You don't indicate whether the thread interface you use would be an
> extension of what I've done so far (which may be a bit complicated to
> sort through at first glance, I admit), or directly using the POSIX
> interfaces. The former would make it easier for somebody to someday
> port the KDC to Windows, I guess, but extensions would need to be made
> (e.g., thread creation and monitors are outside of its current scope),
> and the POSIX approach is likely to get done a lot faster.
The thread interface used in the KDC would be an extension of the
interface available in version 1.4. The following are the additional APIs
that would be used.
1. Thread creation - pthread_create()
2. Recursive mutex - code would be added to KDC for implementing this. (Or
would it be better to add to the k5-threads.h ?)
3. The monitor used for managing the request queue can be implemented
using mutexes and condition variables. The thread interface in version
1.4 does not include condition variable operations. This would be added
to the KDC code (Or should the thread interface be extended).
> 2.4, 2.5: It's also possible in 1.4 for the replay cache and lookaside
> cache to be turned off.
This would require some additional work while porting the changes from
1.3.5 to 1.4.
> 3.1: Again, which thread interface? The shim layer I wrote does not
> support recursive mutexes.
The following is how recursive mutexes would be implemented.
1. If PTHREAD_MUTEX_RECURSIVE_NP is defined (it is defined in pthread.h on
linux), the pthread_mutex_t member of the k5_mutex_t would be
re-initialized to be a recursive mutex. Since pthread.h is present, I
am assuming that mutex.os.p exists and is of type pthread_mutex_t.
2. If PTHREAD_MUTEX_RECURSIVE_NP is not defined, a recursive mutex is
implemented using a mutex 'm' and a thread-specific counter 'c' as
follows.
initial state:
m is unlocked
c == 0 for all threads
LOCK:
1. If (current thread's) c != 0, go to 3
2. lock(m)
3. c = c + 1
UNLOCK:
1. If (current thread's) c > 1, go to 3
2. unlock (m)
3. c = c - 1
> 3.4: The variables set by the signal handlers need to be changed in a
> thread-safe way, and the code checking it should probably delay
> operations (at least the re-opening of the log file) until other
> threads are idle, or at least can't be logging anything.
As stated in section (3.4), all signals (including INT, HUP, TERM) are
blocked in the 'service threads'. So, the signal handlers would always be
executed in the context of the main thread.
I had overlooked the issue of re-opening of the log file. This needs to
be addressed.
> (If the
> database access is doing something that would be bad to exit in the
> middle of, then perhaps exiting should be delayed too. On the other
> hand, we don't want a process to hang around indefinitely, ignoring
> signals telling it to exit, just because some database query is
> hanging.)
In the prototype that I have implemented, the main thread waits for all
service threads to exit before exiting itself. So, the problem arising out
of a database query hanging does exist. It has to be addressed.
> 3.8: Where is gethostbyname called in the KDC?
'gethostbyname()' is used in the function krb5_os_localaddr() (in
lib/krb5/os/localaddr.c). Though it is not called by the KDC, the symbol
krb5_os_localaddr() is present in 'krb5kdc'. I guess this could be
ignored.
>> 3) Section 3.1 -- why is the single mutex to be recursive? The fact
>> that there is proposed to be one single mutex does not lead, in my
>> mind, to the conclusion that this mutex be recursive. Forgive me for
>> saying so, but often the perceived need for a recursive mutex is a
>> byproduct of an incomplete design. There are times when they are a
>> great choice, and this may be one of them -- but the document does not
>> explain why. I think the choice of a recursive mutex needs
>> explanation.
>
> I'm inclined to agree here. Not so much from experience, as from
> principles; there are probably cases where a recursive mutex makes
> sense, but as it can also encourage sloppiness or at least make missing
> unlock calls harder to spot, I think it needs justification.
The reason for using recursive mutexes is as follows. Consider the
following three functions
f1(){
...
b = 2;
...
}
f2(){
a = 1;
f1();
}
f3(){
f1();
}
The updating of (a, b) needs to be atomic. In order to ensure this
atomicity, locking can be done in two ways.
1. f1(){
...
b = 2;
...
}
f2(){
LOCK;
a = 1;
f1();
UNLOCK;
}
f3(){
UNLOCK;
f1();
UNLOCK;
}
2. f1(){
...
LOCK; //recursive
b = 2;
UNLOCK; //recursive
...
}
f2(){
LOCK; //recursive
a = 1;
f1();
UNLOCK; //recursive
}
f3(){
f1();
}
The advantages of the second type of locking are.
1. The critical-section size is smaller (note the extra lines around
'b=2' and the function call itself)
2. Ensuring that a variable is threadsafe is easier. We don't have to
look at the functions calling f1() to find out if 'b' is threadsafe.
In the KDC code, such atomic operations on global variables would be
required in a few places if the server is made multithreaded. For example,
in accept_tcp_connection(), when tcp_data_counter >
max_tcp_data_connections, the following two actions have to be performed
atomically.
1. looking up 'connections' for the oldest connection (inside
accept_tcp_connection())
2. deleting it from 'connections' (inside kill_tcp_connection())
Otherwise, if multiple threads choose the same connection for deletion,
one of them could end up with an invalid handler when the other deletes
it.
It is possible to do the same using non-recursive mutexes, but it would
need rewriting some of the code to group multiple accesses to global
variables into a single function. But I feel that for an initial
implementation, adding lock and unlock calls to existing code would be
simpler.
Also, missing unlock calls would not be hard to spot if a simple rule is
followed - "locks are released in the function in which they are
acquired".
>> 5) Section 3.5 mentions that an alternative is to have separate
>> mutexes for each global variable. In general this is very attractive
>> to me! More locks (i.e., fine-grained locks) usually yields higher
>> concurrency and higher throughput.
>>
>> In the extreme, however, one would probably NOT want separate locks
>> for EACH global variable. If two or more global variables' values need
>> to be coordinated, then a common lock for that set of related globals
>> would be appropriate. E.g., queue A and counter A could be governed by
>> lock A, while queue B and counter B could be governed by lock B
>> (assuming that the two queues did not need to be synchronized).
>
> Some of them appear to be recording statistics from the replay cache
> which are never reported. So, they should be considered protected by
> the same mutex as the replay cache itself. (Though perhaps we should
> either eliminate them or find a way to report them.) It appears that
> kdc_current_rcname is not settable, so it could probably be dropped.
> Et cetera...
>
> (Minor aside: For cases where a variable is to be deleted, getting that
> as a small, separate patch would be nice. But whatever...)
The main reason for using a single (recursive) lock instead of multiple
locks is that deadlocks would not be an issue, though it does restrict
the concurrency. As Sam pointed out, additional locks could be added in
the future if required.
>> 8) Still in Section 3.6, in the algorithm for the default thread --
>> does step 16 (unlock m) trigger any sort of notification for waiting
>> service threads? Default thread step 15 added work to "Q", so I
>> presume that step 16's unlock of m is intended to pro-actively notify
>> any service threads that were waiting for something to appear in Q.
>> Without active notification (e.g., use a pthread condition variable),
>> the service threads may not wake up at all, or at least in a timely
>> manner.
>
> I assumed that was implied by describing 'm' as a monitor, but maybe
> not...
Yes, notification of waiting threads would be part of unlocking of 'm'.
-Regards,
-Rahul S.
More information about the krbdev
mailing list