xref: /illumos-gate/usr/src/lib/libnisdb/nis_hashitem.c (revision 7dbbfe7762f9eabac3999ee1a8b38311d428f7a8)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License, Version 1.0 only
6  * (the "License").  You may not use this file except in compliance
7  * with the License.
8  *
9  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10  * or http://www.opensolaris.org/os/licensing.
11  * See the License for the specific language governing permissions
12  * and limitations under the License.
13  *
14  * When distributing Covered Code, include this CDDL HEADER in each
15  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16  * If applicable, add the following below this CDDL HEADER, with the
17  * fields enclosed by brackets "[]" replaced with your own identifying
18  * information: Portions Copyright [yyyy] [name of copyright owner]
19  *
20  * CDDL HEADER END
21  */
22 /*
23  * Copyright 2005 Sun Microsystems, Inc.  All rights reserved.
24  * Use is subject to license terms.
25  */
26 
27 #include <string.h>
28 #include <pthread.h>
29 #include <syslog.h>
30 #include <rpcsvc/nis.h>
31 
32 #include "nis_hashitem.h"
33 
34 /* We're the magician, so undefine the define-magic */
35 #undef	NIS_HASH_ITEM
36 #undef	NIS_HASH_TABLE
37 #undef	nis_insert_item
38 #undef	nis_find_item
39 #undef	nis_pop_item
40 #undef	nis_remove_item
41 
42 #define	set_thread_status(msg, state)
43 
44 /*
45  * The hash table routines below implement nested (or recursive)
46  * one-writer-or-many-readers locking. The following restrictions
47  * exist:
48  *
49  *	Unless an item destructor has been established, an item
50  *	MUST NOT be removed from a list (__nis_pop_item_mt() or
51  *	(__nis_remove_item_mt()) when the thread performing
52  *	the deletion is holding a read-only lock on the item.
53  *	Doing so will result in the thread blocking in
54  *	pthread_cond_wait() waiting for itself to signal on
55  *	the condition variable. Deletion when the invoking
56  *	thread is holding a write lock (any level of nesting),
57  *	or no lock, is OK.
58  */
59 
60 void
61 __nis_init_hash_table(__nis_hash_table_mt *table,
62 			void (*itemDestructor)(void *)) {
63 
64 	int	errorcode;
65 
66 	if (table != 0) {
67 		errorcode = pthread_mutex_init(&table->lock, 0);
68 		if (errorcode != 0) {
69 			syslog(LOG_WARNING, "__nis_init_hash_table: "
70 			    "(table->lock) pthread_mutex_init returned %d (%s)",
71 			    errorcode, strerror(errorcode));
72 		}
73 
74 		errorcode = pthread_cond_init(&table->cond, 0);
75 		if (errorcode != 0) {
76 			syslog(LOG_WARNING, "__nis_init_hash_table: "
77 			    "(table->cond) pthread_cond_init returned %d (%s)",
78 			    errorcode, strerror(errorcode));
79 		}
80 
81 		errorcode = pthread_mutex_init(&table->traverser_id_lock, 0);
82 		if (errorcode != 0) {
83 			syslog(LOG_WARNING, "__nis_init_hash_table: "
84 			    "(table->traverser_id_lock) "
85 			    "pthread_mutex_init returned %d (%s)",
86 			    errorcode, strerror(errorcode));
87 		}
88 
89 		table->traversed = 0;
90 		table->locked_items = 0;
91 		(void) memset(table->keys, 0, sizeof (table->keys));
92 		table->first = 0;
93 		table->destroyItem = itemDestructor;
94 	}
95 }
96 
97 int
98 __nis_lock_hash_table(__nis_hash_table_mt *table, int traverse, char *msg) {
99 
100 	pthread_t	myself = pthread_self();
101 
102 	if (table != 0) {
103 		if (traverse) {
104 			/*
105 			 * We want exclusive access to everything in the
106 			 * table (list). Wait until there are no threads
107 			 * either traversing the list, or with exclusive
108 			 * access to an item.
109 			 */
110 			set_thread_status(msg, "table WL");
111 			(void) pthread_mutex_lock(&table->lock);
112 			set_thread_status(msg, "table L");
113 			while ((table->traversed != 0 &&
114 					table->traverser_id != myself) ||
115 				table->locked_items != 0) {
116 				set_thread_status(msg, "traverse cond_wait");
117 				MT_LOG(1, (LOG_NOTICE,
118 					"%d: lh table 0x%x trav cond wait",
119 					myself, table));
120 				(void) pthread_cond_wait(&table->cond,
121 							&table->lock);
122 			}
123 			set_thread_status(msg, "traverser_id WL");
124 			(void) pthread_mutex_lock(&table->traverser_id_lock);
125 			set_thread_status(msg, "traverser_id L");
126 			table->traversed = 1;
127 			table->traverser_id = myself;
128 			(void) pthread_mutex_unlock(&table->traverser_id_lock);
129 			set_thread_status(msg, "traverser_id U");
130 		} else {
131 			/*
132 			 * Called from the nis_*_item() functions. If no one's
133 			 * locked the table, lock it. If the table already is
134 			 * being traversed by us, do nothing. Otherwise, wait
135 			 * for the lock.
136 			 */
137 			set_thread_status(msg, "non-traverse TL");
138 			if (pthread_mutex_trylock(&table->lock) == EBUSY) {
139 				int	dolock = 1;
140 				/* Already locked; find out if it's us */
141 				set_thread_status(msg, "traverser_id L");
142 				(void) pthread_mutex_lock(
143 						&table->traverser_id_lock);
144 				if (table->traversed != 0 &&
145 					table->traverser_id == myself) {
146 					/* It's us. No action. */
147 					dolock = 0;
148 				}
149 				(void) pthread_mutex_unlock(
150 						&table->traverser_id_lock);
151 				set_thread_status(msg, "traverser_id U");
152 				/* Not us. Wait for the lock */
153 				if (dolock) {
154 					MT_LOG(1, (LOG_NOTICE,
155 					"%d: lh table 0x%x cond wait",
156 						myself, table));
157 					set_thread_status(msg, "table WL");
158 					(void) pthread_mutex_lock(&table->lock);
159 					set_thread_status(msg, "table L");
160 				}
161 			}
162 		}
163 		MT_LOG(1, (LOG_NOTICE, "%d: lh table %s lock acquired 0x%x",
164 		myself, traverse?"traverse":"non-traverse", table));
165 		return (1);
166 	} else {
167 		return (0);
168 	}
169 }
170 
171 int
172 __nis_ulock_hash_table(__nis_hash_table_mt *table, int traverse, char *msg) {
173 
174 	int	dounlock = 0;
175 
176 	if (table != 0) {
177 		if (traverse) {
178 			/*
179 			 * Since we're keeping track of who's traversing the
180 			 * table in order to avoid recursive locking in the
181 			 * nis_*item() functions, we might as well sanity check
182 			 * here.
183 			 */
184 			set_thread_status(msg, "traverser_id WL");
185 			(void) pthread_mutex_lock(&table->traverser_id_lock);
186 			set_thread_status(msg, "traverser_id L");
187 			if (table->traversed != 0 &&
188 				table->traverser_id == pthread_self()) {
189 				table->traversed = 0;
190 				/*
191 				 * Leave traverser_id as it is, so that it
192 				 * possible to see which thread last held
193 				 * the traverser lock.
194 				 */
195 				dounlock = 1;
196 				/* Wake up other traversers-to-be */
197 				set_thread_status(msg, "table cond_signal");
198 				(void) pthread_cond_signal(&table->cond);
199 			}
200 			(void) pthread_mutex_unlock(&table->traverser_id_lock);
201 			set_thread_status(msg, "traverser_id U");
202 		} else {
203 			/*
204 			 * Called from the nis_*_item() functions. If we're
205 			 * traversing the table, leave it locked.
206 			 */
207 			set_thread_status(msg, "traverser_id WL");
208 			(void) pthread_mutex_lock(&table->traverser_id_lock);
209 			set_thread_status(msg, "traverser_id L");
210 			if (table->traversed == 0) {
211 				dounlock = 1;
212 			}
213 			(void) pthread_mutex_unlock(&table->traverser_id_lock);
214 			set_thread_status(msg, "traverser_id U");
215 		}
216 		if (dounlock) {
217 			(void) pthread_mutex_unlock(&table->lock);
218 			set_thread_status(msg, "table U");
219 		}
220 		MT_LOG(1, (LOG_NOTICE, "%d: lh table %s release 0x%x (%s)",
221 		pthread_self(), traverse?"traverse":"non-traverse", table,
222 			dounlock?"unlocked":"still held"));
223 		return (1);
224 	} else {
225 		return (0);
226 	}
227 }
228 
229 static __nis_hash_item_mt **
230 __find_item_mt(nis_name name, __nis_hash_table_mt *table, int *keyp) {
231 
232 	int			key = 0;
233 	unsigned char		*s;
234 	__nis_hash_item_mt	*it, **pp;
235 
236 	/* Assume table!=0, table lock held */
237 
238 	for (s = (unsigned char *)name;  *s != 0;  s++) {
239 		key += *s;
240 	}
241 	key %= (sizeof (table->keys) / sizeof (table->keys[0]));
242 
243 	if (keyp != 0) {
244 		*keyp = key;
245 	}
246 	for (pp = &table->keys[key];  (it = *pp) != 0;  pp = &it->next) {
247 		if (strcmp(name, it->name) == 0) {
248 			break;
249 		}
250 	}
251 
252 	return (pp);
253 }
254 
255 /*
256  * The 'readwrite' argument is interpreted as follows on a successful
257  * return:
258  *
259  *	< 0	Exclusive access to item
260  *	0	Item must not be used or referenced in any way
261  *	> 0	Non-exclusive access (read-only) to item
262  *
263  * Except when 'readwrite' ==  0, the caller must explicitly release the
264  * item (__nis_release_item()).
265  */
266 int
267 __nis_insert_item_mt(void *arg, __nis_hash_table_mt *table, int readwrite) {
268 
269 	__nis_hash_item_mt	*item = arg;
270 	int			key;
271 	__nis_hash_item_mt	**pp;
272 
273 	if (item == 0 || __nis_lock_hash_table(table, 0, "nitmt") == 0)
274 		return (0);
275 
276 	if ((*(pp = __find_item_mt(item->name, table, &key))) != 0) {
277 		(void) __nis_ulock_hash_table(table, 0, "nitmt");
278 		return (0);
279 	}
280 
281 	(void) pthread_cond_init(&item->lock, 0);
282 	item->readers = item->writer = 0;
283 	item->last_reader_id = item->writer_id = INV_PTHREAD_ID;
284 	if (readwrite < 0) {
285 		item->writer = 1;
286 		item->writer_id = pthread_self();
287 		table->locked_items++;
288 	} else if (readwrite > 0) {
289 		item->readers = 1;
290 		item->last_reader_id = pthread_self();
291 		table->locked_items++;
292 	}
293 	item->next	= *pp;
294 	*pp		= item;
295 	item->keychain	= key;
296 
297 	if (table->first)
298 		table->first->prv_item = item;
299 
300 	item->nxt_item	= table->first;
301 	item->prv_item	= NULL;
302 	table->first	= item;
303 
304 	(void) __nis_ulock_hash_table(table, 0, "nitmt");
305 
306 	return (1);
307 }
308 
309 void
310 __nis_insert_name_mt(nis_name name, __nis_hash_table_mt *table) {
311 
312 	__nis_hash_item_mt	*item;
313 
314 	if (name == 0 || table == 0)
315 		return;
316 
317 	if ((item = malloc(sizeof (*item))) == 0) {
318 		syslog(LOG_WARNING, "__nis_insert_name_mt: malloc failed\n");
319 		return;
320 	}
321 
322 	if ((item->name = strdup(name)) == 0) {
323 		syslog(LOG_WARNING, "__nis_insert_name_mt: strdup failed\n");
324 		free(item);
325 		return;
326 	}
327 
328 	if (! __nis_insert_item_mt(item, table, 0)) {
329 		free(item->name);
330 		free(item);
331 	}
332 }
333 
334 /*
335  * readwrite:	< 0	Exclusive access
336  *		0	No access; must not use returned item in any way,
337  *			other than to confirm existence indicated by a non-NULL
338  *			return value.
339  *		> 0	Non-exclusive (read-only) access
340  *
341  * If trylock != 0 and *trylock != 0 and the item exists but the requested
342  * lock type cannot be acquired, set *trylock = -1 and return 0.
343  */
344 void *
345 __nis_find_item_mt(nis_name name, __nis_hash_table_mt *table, int readwrite,
346 			int *trylock) {
347 
348 	__nis_hash_item_mt	*item;
349 	pthread_t		me = pthread_self();
350 
351 	if (name == 0 || __nis_lock_hash_table(table, 0, "nfimt") == 0)
352 		return (0);
353 
354 	/*
355 	 * Block waiting for more favorable conditions unless:
356 	 *
357 	 *	The item doesn't exist anymore
358 	 *
359 	 *	'readwrite' == 0 (verify existence only)
360 	 *
361 	 *	There's a writer, but it's us
362 	 *
363 	 *	There are no writers, and we're satisfied by RO access
364 	 *
365 	 *	A trylock was requested
366 	 */
367 	while ((item = *__find_item_mt(name, table, 0)) != 0) {
368 		if (readwrite == 0 ||
369 				(item->writer == 0 && item->readers == 0))
370 			break;
371 		if (item->writer == 0 && readwrite > 0)
372 			break;
373 		if ((item->writer != 0 && item->writer_id == me))
374 			break;
375 		if (trylock != 0 && *trylock != 0) {
376 			*trylock = -1;
377 			(void) __nis_ulock_hash_table(table, 0, "nfimt");
378 			return (0);
379 		}
380 		(void) pthread_cond_wait(&item->lock, &table->lock);
381 	}
382 
383 	if (item != 0) {
384 		if (readwrite < 0) {
385 			if (item->writer == 0) {
386 				item->writer_id = me;
387 				table->locked_items++;
388 			}
389 			item->writer++;
390 		} else if (readwrite > 0) {
391 			if (item->readers == 0) {
392 				table->locked_items++;
393 			}
394 			item->last_reader_id = me;
395 			item->readers++;
396 		}
397 	}
398 
399 	(void) __nis_ulock_hash_table(table, 0, "nfimt");
400 
401 	return (item);
402 }
403 
404 void *
405 __nis_pop_item_mt(__nis_hash_table_mt *table) {
406 
407 	__nis_hash_item_mt	*item, *cur, *prev;
408 	pthread_t		mtid;
409 
410 	if (__nis_lock_hash_table(table, 0, "npimt") == 0)
411 		return (0);
412 
413 	/* Wait until the first item isn't in use by another thread */
414 	mtid = pthread_self();
415 	while ((item = table->first) != 0) {
416 		if (table->destroyItem != 0)
417 			break;
418 		if (item->readers == 0 && item->writer == 0)
419 			break;
420 		if (item->writer != 0 && item->writer_id == mtid)
421 			break;
422 		(void) pthread_cond_wait(&item->lock, &table->lock);
423 	}
424 
425 	/* List might be empty now */
426 	if (item == 0) {
427 		__nis_ulock_hash_table(table, 0, "npimt");
428 		return (0);
429 	}
430 
431 	prev = 0;
432 	for (cur = table->keys[item->keychain]; cur;
433 					prev = cur, cur = cur->next) {
434 		if (cur == item) {
435 			if (prev)
436 				prev->next = cur->next;
437 			else
438 				table->keys[cur->keychain] = cur->next;
439 			if (cur->prv_item)
440 				cur->prv_item->nxt_item = cur->nxt_item;
441 			else
442 				table->first = cur->nxt_item;
443 			if (cur->nxt_item)
444 				cur->nxt_item->prv_item = cur->prv_item;
445 			break;
446 		}
447 	}
448 
449 	/*
450 	 * We use keychain < 0 to indicate that the item isn't linked
451 	 * into the table anymore.
452 	 */
453 	item->keychain = -1;
454 
455 	/* Adjust the count of locked items in the table */
456 	if (table->locked_items != 0 &&
457 			(item->writer > 0 || item->readers > 0)) {
458 		table->locked_items--;
459 		if (table->locked_items == 0) {
460 			/* Wake up traversers-to-be */
461 			(void) pthread_cond_signal(&table->cond);
462 		}
463 	}
464 
465 	/*
466 	 * Wake up any threads that were waiting for this item. Obviously,
467 	 * such threads must start over scanning the list.
468 	 */
469 	(void) pthread_cond_signal(&item->lock);
470 	(void) pthread_cond_destroy(&item->lock);
471 
472 	/*
473 	 * If the item isn't locked, and an item destructor has been
474 	 * established, invoke the destructor.
475 	 */
476 	if (item->readers == 0 && item->writer == 0 &&
477 			table->destroyItem != 0) {
478 		(*table->destroyItem)(item);
479 		item = 0;
480 	} else {
481 		item->next = 0;
482 		item->prv_item = 0;
483 		item->nxt_item = 0;
484 	}
485 
486 	(void) __nis_ulock_hash_table(table, 0, "npimt");
487 
488 	/*
489 	 * If we get here, and the 'item' is NULL, we've popped the
490 	 * item, but also destroyed it. Returning NULL would make
491 	 * our caller believe the list is empty, so instead, we invoke
492 	 * ourselves to pop the next item.
493 	 */
494 	return ((item != 0) ? item : __nis_pop_item_mt(table));
495 }
496 
497 void *
498 __nis_remove_item_mt(nis_name name, __nis_hash_table_mt *table) {
499 
500 	__nis_hash_item_mt	*nl, **pp;
501 	pthread_t		mtid;
502 
503 	if (__nis_lock_hash_table(table, 0, "nrimt") == 0)
504 		return (0);
505 
506 	/* Find the item, and make sure it's not in use */
507 	mtid = pthread_self();
508 	while ((nl = *(pp = __find_item_mt(name, table, (int *)0))) != 0) {
509 		if (table->destroyItem != 0)
510 			break;
511 		if (nl->readers == 0 && nl->writer == 0)
512 			break;
513 		if (nl->writer != 0 && nl->writer_id == mtid)
514 			break;
515 		(void) pthread_cond_wait(&nl->lock, &table->lock);
516 	}
517 
518 	if (nl == 0) {
519 		(void) __nis_ulock_hash_table(table, 0, "nrimt");
520 		return (0);
521 	}
522 
523 	/* Remove nl from the hash chain */
524 	*pp = nl->next;
525 	nl->next = 0;
526 
527 	/* Remove nl from the linked list of all names */
528 	if (nl->prv_item)
529 		nl->prv_item->nxt_item = nl->nxt_item;
530 	else
531 		table->first = nl->nxt_item;
532 
533 	if (nl->nxt_item)
534 		nl->nxt_item->prv_item = nl->prv_item;
535 	nl->prv_item = 0;
536 	nl->nxt_item = 0;
537 
538 	/* keychain < 0 means not in table anymore */
539 	nl->keychain = -1;
540 
541 	/*
542 	 * If this item was locked, we can now decrement the count of
543 	 * locked items for the table.
544 	 */
545 	if (table->locked_items != 0 &&
546 		(nl->writer > 0 || nl->readers > 0)) {
547 		table->locked_items--;
548 		if (table->locked_items == 0) {
549 			/* Wake up traversers-to-be */
550 			(void) pthread_cond_signal(&table->cond);
551 		}
552 	}
553 	(void) pthread_cond_signal(&nl->lock);
554 	(void) pthread_cond_destroy(&nl->lock);
555 
556 	/*
557 	 * If the item isn't locked, and an item destructor has been
558 	 * established, invoke the destructor. In that case, we return
559 	 * NULL, so that our caller doesn't try to reference the
560 	 * deleted item.
561 	 */
562 	if (nl->readers == 0 && nl->writer == 0 && table->destroyItem != 0) {
563 		(*table->destroyItem)(nl);
564 		nl = 0;
565 	}
566 
567 	(void) __nis_ulock_hash_table(table, 0, "nrimt");
568 
569 	return (nl);
570 }
571 
572 /*
573  * Release an item that had been acquired exclusively or non-exclusively.
574  * Note that 'readwrite' can assume any integer value, and thus can be
575  * used to release any level of recursive locking. It's the responsibility
576  * of the caller to make sure that proper nesting is maintained.
577  */
578 int
579 __nis_release_item(void *arg, __nis_hash_table_mt *table, int readwrite) {
580 
581 	__nis_hash_item_mt	*item = arg;
582 	int			wakeup = 0;
583 
584 	if (item == 0 || __nis_lock_hash_table(table, 0, "nreli") == 0)
585 		return (0);
586 
587 	if ((readwrite < 0 && abs(readwrite) > item->writer) ||
588 		(readwrite < 0 && item->writer > 0 &&
589 			item->writer_id != pthread_self()) ||
590 		(readwrite > 0 && readwrite > item->readers)) {
591 		/* Caller confused; ignore */
592 		(void) __nis_ulock_hash_table(table, 0, "nreli");
593 		return (0);
594 	}
595 
596 	if (readwrite < 0) {
597 		item->writer += readwrite;
598 		if (item->writer == 0 && item->keychain >= 0) {
599 			if (table->locked_items != 0)
600 				table->locked_items--;
601 			wakeup = 1;
602 		}
603 	} else if (readwrite > 0) {
604 		item->readers -= readwrite;
605 		item->last_reader_id = INV_PTHREAD_ID;
606 		if (item->readers == 0 && item->keychain >= 0) {
607 			if (table->locked_items != 0)
608 				table->locked_items--;
609 			wakeup = 1;
610 		}
611 	}
612 
613 	if (table->locked_items == 0) {
614 		/* Wake up traversers-to-be */
615 		(void) pthread_cond_signal(&table->cond);
616 	}
617 	if (wakeup) {
618 		/* Wake up anyone else who wants this item */
619 		(void) pthread_cond_signal(&item->lock);
620 	}
621 
622 	/*
623 	 * Delete if no references, not linked into list, and destructor
624 	 * established.
625 	 */
626 	if (item->keychain < 0 &&
627 			item->readers == 0 && item->writer == 0 &&
628 			item->next == 0 &&
629 			item->prv_item == 0 && item->nxt_item == 0 &&
630 			table->destroyItem != 0)
631 		(*table->destroyItem)(item);
632 
633 	(void) __nis_ulock_hash_table(table, 0, "nreli");
634 
635 	return (1);
636 }
637 
638 /*
639  * Return -1 if item checked out for both reading and writing, 1 if
640  * readonly, and 0 otherwise.
641  */
642 int
643 __nis_item_access(void *arg) {
644 
645 	__nis_hash_item_mt	*item = arg;
646 
647 	if (item != 0) {
648 		if (item->writer > 0) {
649 			if (item->writer_id != pthread_self())
650 				abort();
651 			return (-1);
652 		} else if (item->readers > 0) {
653 			return (1);
654 		}
655 	}
656 
657 	return (0);
658 }
659 
660 /*
661  * __nis_scan_table_mt()
662  *
663  * Iterate over all items in a __nis_hash_table_mt. We ignore
664  * first/prv_item/nxt_item and scan in hash-chain order.  The iterator
665  * function should *not* insert or delete items. If the iterator
666  * function returns TRUE the scan terminates. For compatibility with
667  * the existing non-MT nis_scan_table() this function has no return
668  * value.
669  */
670 void
671 __nis_scan_table_mt(
672 	__nis_hash_table_mt	*table,
673 	bool_t		(*func)(__nis_hash_item_mt *, void *),
674 	void		*funcarg)
675 {
676 	int slot;
677 
678 	if (table == 0) {
679 		return;
680 	}
681 
682 	if (__nis_lock_hash_table(table, 1, "nstmt") == 0) {
683 		syslog(LOG_DEBUG, "__nis_scan_table_mt: mutex lock failed ");
684 		return;
685 	}
686 
687 	for (slot = 0;
688 	    slot < sizeof (table->keys) / sizeof (table->keys[0]);
689 	    slot++) {
690 		__nis_hash_item_mt *it;
691 
692 		for (it = table->keys[slot]; it != 0; it = it->next) {
693 			if (TRUE == (*func)(it, funcarg)) {
694 				break;
695 			}
696 		}
697 	}
698 	    if (__nis_ulock_hash_table(table, 1, "nstmt") == 0)
699 		syslog(LOG_DEBUG, "__nis_scan_table_mt: mutex unlock failed ");
700 }
701