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