58100b696a6abf2c3f01b13ba6d18ea50bd5060b
[tpg/acess2.git] / Kernel / threads.c
1 /*
2  * Acess2
3  * threads.c
4  * - Common Thread Control
5  */
6 #include <acess.h>
7 #include <threads.h>
8 #include <errno.h>
9
10 // === CONSTANTS ===
11 #define DEFAULT_QUANTUM 10
12 #define DEFAULT_TICKETS 5
13 #define MAX_TICKETS             10
14 const enum eConfigTypes cCONFIG_TYPES[] = {
15         CFGT_HEAPSTR,   // e.g. CFG_VFS_CWD
16         CFGT_INT,       // e.g. CFG_VFS_MAXFILES
17         CFGT_NULL
18 };
19
20 // === IMPORTS ===
21 extern void     ArchThreads_Init(void);
22 extern void     Proc_Start(void);
23 extern tThread  *Proc_GetCurThread(void);
24 extern int      Proc_Clone(Uint *Err, Uint Flags);
25 extern void     Proc_CallFaultHandler(tThread *Thread);
26
27 // === PROTOTYPES ===
28 void    Threads_Init(void);
29  int    Threads_SetName(char *NewName);
30 char    *Threads_GetName(int ID);
31 void    Threads_SetTickets(int Num);
32 tThread *Threads_CloneTCB(Uint *Err, Uint Flags);
33  int    Threads_WaitTID(int TID, int *status);
34 tThread *Threads_GetThread(Uint TID);
35 void    Threads_AddToDelete(tThread *Thread);
36 tThread *Threads_int_GetPrev(tThread **List, tThread *Thread);
37 void    Threads_Exit(int TID, int Status);
38 void    Threads_Kill(tThread *Thread, int Status);
39 void    Threads_Yield(void);
40 void    Threads_Sleep(void);
41 void    Threads_Wake(tThread *Thread);
42 void    Threads_AddActive(tThread *Thread);
43  int    Threads_GetPID(void);
44  int    Threads_GetTID(void);
45 tUID    Threads_GetUID(void);
46  int    Threads_SetUID(Uint *Errno, tUID ID);
47 tGID    Threads_GetGID(void);
48  int    Threads_SetGID(Uint *Errno, tUID ID);
49 void    Threads_Dump(void);
50
51 // === GLOBALS ===
52 // -- Core Thread --
53 // Only used for the core kernel
54 tThread gThreadZero = {
55         NULL, 0,        // Next, Lock
56         THREAD_STAT_ACTIVE,     // Status
57         0,      // Exit Status
58         0, 0,   // TID, TGID
59         0, 0,   // UID, GID
60         0,      // Parent Thread ID
61         "ThreadZero",   // Name
62         
63         0,      // Kernel Stack
64         {0},    // Saved State
65         {0},    // VM State
66         
67         0, 0,   // Current Fault, Fault Handler
68         
69         NULL, NULL,     // Messages, Last Message
70         DEFAULT_QUANTUM, DEFAULT_QUANTUM,       // Quantum, Remaining
71         DEFAULT_TICKETS,
72         {0}     // Default config to zero
73         };
74 // -- Processes --
75 // --- Locks ---
76 tSpinlock       glThreadListLock = 0;   ///\note NEVER use a heap function while locked
77 // --- Current State ---
78 volatile int    giNumActiveThreads = 0;
79 //volatile int  giTotalTickets = 0;
80 volatile int    giFreeTickets = 0;
81 volatile Uint   giNextTID = 1;
82 // --- Thread Lists ---
83 tThread *gActiveThreads = NULL;         // Currently Running Threads
84 tThread *gSleepingThreads = NULL;       // Sleeping Threads
85 tThread *gDeleteThreads = NULL;         // Threads to delete
86  int    giNumCPUs = 1;
87
88 // === CODE ===
89 /**
90  * \fn void Threads_Init(void)
91  * \brief Initialse the thread list
92  */
93 void Threads_Init(void)
94 {
95         ArchThreads_Init();
96         
97         // Create Initial Task
98         gActiveThreads = &gThreadZero;
99         //giFreeTickets = gThreadZero.NumTickets;
100         giNumActiveThreads = 1;
101         
102         #if 1
103         // Create Idle Task
104         if(Proc_Clone(0, 0) == 0)
105         {
106                 tThread *cur = Proc_GetCurThread();
107                 cur->ThreadName = "Idle Thread";
108                 Threads_SetTickets(0);  // Never called randomly
109                 cur->Quantum = 1;       // 1 slice quantum
110                 for(;;) HALT(); // Just yeilds
111         }
112         #endif
113         
114         Proc_Start();
115 }
116
117 /**
118  * \fn void Threads_SetName(char *NewName)
119  * \brief Sets the current thread's name
120  */
121 int Threads_SetName(char *NewName)
122 {
123         tThread *cur = Proc_GetCurThread();
124         if( IsHeap(cur->ThreadName) )
125                 free( cur->ThreadName );
126         // TODO: Possible concurrency issue
127         cur->ThreadName = malloc(strlen(NewName)+1);
128         strcpy(cur->ThreadName, NewName);
129         return 0;
130 }
131
132 /**
133  * \fn char *Threads_GetName(int ID)
134  * \brief Gets a thread's name
135  */
136 char *Threads_GetName(int ID)
137 {
138         if(ID == -1) {
139                 return Proc_GetCurThread()->ThreadName;
140         }
141         // TODO: Find a thread and get its name
142         return NULL;
143 }
144
145 /**
146  * \fn void Threads_SetTickets(int Num)
147  * \brief Sets the 'priority' of a task
148  */
149 void Threads_SetTickets(int Num)
150 {
151         tThread *cur = Proc_GetCurThread();
152         if(Num < 0)     return;
153         if(Num > MAX_TICKETS)   Num = MAX_TICKETS;
154         
155         cur->NumTickets = Num;
156 }
157
158 /**
159  * \fn tThread *Threads_CloneTCB(Uint *Err, Uint Flags)
160  */
161 tThread *Threads_CloneTCB(Uint *Err, Uint Flags)
162 {
163         tThread *cur, *new;
164          int    i;
165         cur = Proc_GetCurThread();
166         
167         new = malloc(sizeof(tThread));
168         if(new == NULL) {
169                 *Err = -ENOMEM;
170                 return NULL;
171         }
172         memcpy(new, cur, sizeof(tThread));
173         
174         new->CurCPU = -1;
175         new->Next = NULL;
176         new->IsLocked = 0;
177         new->Status = THREAD_STAT_ACTIVE;
178         new->RetStatus = 0;
179         
180         // Get Thread ID
181         new->TID = giNextTID++;
182         new->PTID = cur->TID;
183         
184         // Clone Name
185         new->ThreadName = strdup(cur->ThreadName);
186         
187         // Set Thread Group ID (PID)
188         if(Flags & CLONE_VM)
189                 new->TGID = new->TID;
190         else
191                 new->TGID = cur->TGID;
192         
193         // Messages are not inherited
194         new->Messages = NULL;
195         new->LastMessage = NULL;
196         
197         // Set State
198         new->Remaining = new->Quantum = cur->Quantum;
199         new->NumTickets = cur->NumTickets;
200         
201         // Set Signal Handlers
202         new->CurFaultNum = 0;
203         new->FaultHandler = cur->FaultHandler;
204         
205         for( i = 0; i < NUM_CFG_ENTRIES; i ++ )
206         {
207                 switch(cCONFIG_TYPES[i])
208                 {
209                 default:
210                         new->Config[i] = cur->Config[i];
211                         break;
212                 case CFGT_HEAPSTR:
213                         if(cur->Config[i])
214                                 new->Config[i] = (Uint) strdup( (void*)cur->Config[i] );
215                         else
216                                 new->Config[i] = 0;
217                         break;
218                 }
219         }
220         
221         return new;
222 }
223
224 /**
225  * \fn Uint *Threads_GetCfgPtr(int Id)
226  */
227 Uint *Threads_GetCfgPtr(int Id)
228 {
229         if(Id < 0 || Id >= NUM_CFG_ENTRIES) {
230                 Warning("Threads_GetCfgPtr: Index %i is out of bounds", Id);
231                 return NULL;
232         }
233         
234         return &Proc_GetCurThread()->Config[Id];
235 }
236
237 /**
238  * \fn void Threads_WaitTID(int TID, int *status)
239  * \brief Wait for a task to change state
240  */
241 int Threads_WaitTID(int TID, int *status)
242 {       
243         // Any Child
244         if(TID == -1) {
245                 
246                 return -1;
247         }
248         
249         // Any peer/child thread
250         if(TID == 0) {
251                 
252                 return -1;
253         }
254         
255         // TGID = abs(TID)
256         if(TID < -1) {
257                 return -1;
258         }
259         
260         // Specific Thread
261         if(TID > 0) {
262                 tThread *t = Threads_GetThread(TID);
263                  int    initStatus = t->Status;
264                  int    ret;
265                 
266                 if(initStatus != THREAD_STAT_ZOMBIE) {
267                         while(t->Status == initStatus) {
268                                 Threads_Yield();
269                         }
270                 }
271                 
272                 ret = t->RetStatus;
273                 switch(t->Status)
274                 {
275                 case THREAD_STAT_ZOMBIE:
276                         t->Status = THREAD_STAT_DEAD;
277                         if(status)      *status = 0;
278                         Threads_AddToDelete( t );
279                         break;
280                 default:
281                         if(status)      *status = -1;
282                         break;
283                 }
284                 return ret;
285         }
286         
287         return -1;
288 }
289
290 /**
291  * \fn tThread *Threads_GetThread(Uint TID)
292  * \brief Gets a thread given its TID
293  * \param TID   Thread ID
294  */
295 tThread *Threads_GetThread(Uint TID)
296 {
297         tThread *thread;
298         
299         // Search Active List
300         for(thread = gActiveThreads;
301                 thread;
302                 thread = thread->Next)
303         {
304                 if(thread->TID == TID)
305                         return thread;
306         }
307         
308         // Search Sleeping List
309         for(thread = gSleepingThreads;
310                 thread;
311                 thread = thread->Next)
312         {
313                 if(thread->TID == TID)
314                         return thread;
315         }
316         
317         return NULL;
318 }
319
320 /**
321  * \fn void Threads_AddToDelete(tThread *Thread)
322  * \brief Adds a thread to the delete queue
323  */
324 void Threads_AddToDelete(tThread *Thread)
325 {
326         // Add to delete queue
327         if(gDeleteThreads) {
328                 Thread->Next = gDeleteThreads;
329                 gDeleteThreads = Thread;
330         } else {
331                 Thread->Next = NULL;
332                 gDeleteThreads = Thread;
333         }
334 }
335
336 /**
337  * \fn tThread *Threads_int_GetPrev(tThread **List, tThread *Thread)
338  * \brief Gets the previous entry in a thead linked list
339  */
340 tThread *Threads_int_GetPrev(tThread **List, tThread *Thread)
341 {
342         tThread *ret;
343         // First Entry
344         if(*List == Thread) {
345                 return (tThread*)List;
346         } else {
347                 for(ret = *List;
348                         ret->Next && ret->Next != Thread;
349                         ret = ret->Next
350                         );
351                 // Error if the thread is not on the list
352                 if(!ret->Next || ret->Next != Thread) {
353                         return NULL;
354                 }
355         }
356         return ret;
357 }
358
359 /**
360  * \fn void Threads_Exit(int TID, int Status)
361  * \brief Exit the current process
362  */
363 void Threads_Exit(int TID, int Status)
364 {
365         if( TID == 0 )
366                 Threads_Kill( Proc_GetCurThread(), (Uint)Status & 0xFF );
367         else
368                 Threads_Kill( Threads_GetThread(TID), (Uint)Status & 0xFF );
369         // Halt forever, just in case
370         for(;;)
371                 HALT();
372 }
373
374 /**
375  * \fn void Threads_Kill(tThread *Thread, int Status)
376  * \brief Kill a thread
377  * \param Thread        Thread to kill
378  * \param Status        Status code to return to the parent
379  */
380 void Threads_Kill(tThread *Thread, int Status)
381 {
382         tThread *prev;
383         tMsg    *msg;
384         
385         // Kill all children
386         #if 0
387         {
388                 tThread *child;
389                 for(child = gActiveThreads;
390                         child;
391                         child = child->Next)
392                 {
393                         if(child->PTID == Thread->TID)
394                                 Threads_Kill(child, -1);
395                 }
396         }
397         #endif
398         
399         ///\note Double lock is needed due to overlap of lock areas
400         
401         // Lock thread (stop us recieving messages)
402         LOCK( &Thread->IsLocked );
403         
404         // Lock thread list
405         LOCK( &glThreadListLock );
406         
407         // Get previous thread on list
408         prev = Threads_int_GetPrev( &gActiveThreads, Thread );
409         if(!prev) {
410                 Warning("Proc_Exit - Current thread is not on the active queue");
411                 return;
412         }
413         
414         // Clear Message Queue
415         while( Thread->Messages )
416         {
417                 msg = Thread->Messages->Next;
418                 free( Thread->Messages );
419                 Thread->Messages = msg;
420         }
421         
422         Thread->Remaining = 0;  // Clear Remaining Quantum
423         Thread->Quantum = 0;    // Clear Quantum to indicate dead thread
424         prev->Next = Thread->Next;      // Remove from active
425         
426         giNumActiveThreads --;
427         if( Thread != Proc_GetCurThread() )
428                 giFreeTickets -= Thread->NumTickets;
429         
430         // Mark thread as a zombie
431         Thread->RetStatus = Status;
432         
433         // Don't Zombie if we are being killed as part of a tree
434         if(Status == -1)
435         {
436                 Thread->Status = THREAD_STAT_DEAD;
437                 Threads_AddToDelete( Thread );
438         } else {
439                 Thread->Status = THREAD_STAT_ZOMBIE;
440         }
441         
442         // Release spinlocks
443         RELEASE( &Thread->IsLocked );   // Released first so that it IS released
444         RELEASE( &glThreadListLock );
445         
446         //Log("Thread %i went *hurk*", Thread->TID);
447         
448         if(Status != -1)        HALT();
449 }
450
451 /**
452  * \fn void Threads_Yield(void)
453  * \brief Yield remainder of timeslice
454  */
455 void Threads_Yield(void)
456 {
457         Proc_GetCurThread()->Remaining = 0;
458         HALT();
459 }
460
461 /**
462  * \fn void Threads_Sleep(void)
463  * \brief Take the current process off the run queue
464  */
465 void Threads_Sleep(void)
466 {
467         tThread *cur = Proc_GetCurThread();
468         tThread *thread;
469         
470         //Log_Log("Threads", "%i going to sleep", cur->TID);
471         
472         // Acquire Spinlock
473         LOCK( &glThreadListLock );
474         
475         // Get thread before current thread
476         thread = Threads_int_GetPrev( &gActiveThreads, cur );
477         if(!thread) {
478                 Warning("Threads_Sleep - Current thread is not on the active queue");
479                 Threads_Dump();
480                 RELEASE( &glThreadListLock );
481                 return;
482         }
483         
484         // Don't sleep if there is a message waiting
485         if( cur->Messages ) {
486                 RELEASE( &glThreadListLock );
487                 return;
488         }
489         
490         // Unset remaining timeslices (force a task switch on timer fire)
491         cur->Remaining = 0;
492         
493         // Remove from active list
494         thread->Next = cur->Next;
495         
496         // Add to Sleeping List (at the top)
497         cur->Next = gSleepingThreads;
498         gSleepingThreads = cur;
499         
500         // Reduce the active count & ticket count
501         giNumActiveThreads --;
502         //giTotalTickets -= cur->NumTickets;
503         
504         // Mark thread as sleeping
505         cur->Status = THREAD_STAT_SLEEPING;
506         
507         // Release Spinlock
508         RELEASE( &glThreadListLock );
509         
510         while(cur->Status != THREAD_STAT_ACTIVE)        HALT();
511 }
512
513
514 /**
515  * \fn void Threads_Wake( tThread *Thread )
516  * \brief Wakes a sleeping/waiting thread up
517  */
518 void Threads_Wake(tThread *Thread)
519 {
520         tThread *prev;
521         switch(Thread->Status)
522         {
523         case THREAD_STAT_ACTIVE:        break;
524         case THREAD_STAT_SLEEPING:
525                 //Log_Log("Threads", "Waking %i (%p) from sleeping", Thread->TID, Thread);
526                 LOCK( &glThreadListLock );
527                 prev = Threads_int_GetPrev(&gSleepingThreads, Thread);
528                 prev->Next = Thread->Next;      // Remove from sleeping queue
529                 Thread->Next = gActiveThreads;  // Add to active queue
530                 gActiveThreads = Thread;
531                 giNumActiveThreads ++;
532                 // Thread can't be the current, so no need to check
533                 giFreeTickets += Thread->NumTickets;
534                 Thread->Status = THREAD_STAT_ACTIVE;
535                 RELEASE( &glThreadListLock );
536                 break;
537         case THREAD_STAT_WAITING:
538                 Warning("Thread_Wake - Waiting threads are not currently supported");
539                 break;
540         case THREAD_STAT_DEAD:
541                 Warning("Thread_Wake - Attempt to wake dead thread (%i)", Thread->TID);
542                 break;
543         default:
544                 Warning("Thread_Wake - Unknown process status (%i)\n", Thread->Status);
545                 break;
546         }
547 }
548
549 /**
550  * \brief Wake a thread given the TID
551  */
552 void Threads_WakeTID(tTID Thread)
553 {
554         Threads_Wake( Threads_GetThread(Thread) );
555 }
556
557 /**
558  * \fn void Threads_AddActive(tThread *Thread)
559  * \brief Adds a thread to the active queue
560  */
561 void Threads_AddActive(tThread *Thread)
562 {
563         LOCK( &glThreadListLock );
564         Thread->Next = gActiveThreads;
565         gActiveThreads = Thread;
566         giNumActiveThreads ++;
567         // Thread can't be the current, so no need to check
568         giFreeTickets += Thread->NumTickets;
569         //Log("Threads_AddActive: giNumActiveThreads = %i, giTotalTickets = %i",
570         //      giNumActiveThreads, giTotalTickets);
571         RELEASE( &glThreadListLock );
572 }
573
574 /**
575  * \fn void Threads_SetFaultHandler(Uint Handler)
576  * \brief Sets the signal handler for a signal
577  */
578 void Threads_SetFaultHandler(Uint Handler)
579 {       
580         Log_Log("Threads", "Threads_SetFaultHandler: Handler = %p", Handler);
581         Proc_GetCurThread()->FaultHandler = Handler;
582 }
583
584 /**
585  * \fn void Threads_Fault(int Num)
586  * \brief Calls a fault handler
587  */
588 void Threads_Fault(int Num)
589 {
590         tThread *thread = Proc_GetCurThread();
591         
592         Log_Log("Threads", "Threads_Fault: thread = %p", thread);
593         
594         if(!thread)     return ;
595         
596         Log_Log("Threads", "Threads_Fault: thread->FaultHandler = %p", thread->FaultHandler);
597         
598         switch(thread->FaultHandler)
599         {
600         case 0: // Panic?
601                 Threads_Kill(thread, -1);
602                 HALT();
603                 return ;
604         case 1: // Dump Core?
605                 Threads_Kill(thread, -1);
606                 HALT();
607                 return ;
608         }
609         
610         // Double Fault? Oh, F**k
611         if(thread->CurFaultNum != 0) {
612                 Threads_Kill(thread, -1);       // For now, just kill
613                 HALT();
614         }
615         
616         thread->CurFaultNum = Num;
617         
618         Proc_CallFaultHandler(thread);
619 }
620
621 // --- Process Structure Access Functions ---
622 tPID Threads_GetPID(void)
623 {
624         return Proc_GetCurThread()->TGID;
625 }
626 tTID Threads_GetTID(void)
627 {
628         return Proc_GetCurThread()->TID;
629 }
630 tUID Threads_GetUID(void)
631 {
632         return Proc_GetCurThread()->UID;
633 }
634 tGID Threads_GetGID(void)
635 {
636         return Proc_GetCurThread()->GID;
637 }
638
639 int Threads_SetUID(Uint *Errno, tUID ID)
640 {
641         tThread *t = Proc_GetCurThread();
642         if( t->UID != 0 ) {
643                 *Errno = -EACCES;
644                 return -1;
645         }
646         Log("Threads_SetUID - Setting User ID to %i", ID);
647         t->UID = ID;
648         return 0;
649 }
650
651 int Threads_SetGID(Uint *Errno, tGID ID)
652 {
653         tThread *t = Proc_GetCurThread();
654         if( t->UID != 0 ) {
655                 *Errno = -EACCES;
656                 return -1;
657         }
658         Log("Threads_SetGID - Setting Group ID to %i", ID);
659         t->GID = ID;
660         return 0;
661 }
662
663 /**
664  * \fn void Threads_Dump(void)
665  * \brief Dums a list of currently running threads
666  */
667 void Threads_Dump(void)
668 {
669         tThread *thread;
670         tThread *cur = Proc_GetCurThread();
671         
672         Log("Active Threads:");
673         for(thread=gActiveThreads;thread;thread=thread->Next)
674         {
675                 Log(" %i (%i) - %s (CPU %i)",
676                         thread->TID, thread->TGID, thread->ThreadName, thread->CurCPU);
677                 Log("  %i Tickets, Quantum %i", thread->NumTickets, thread->Quantum);
678                 Log("  KStack 0x%x", thread->KernelStack);
679         }
680         Log("Sleeping Threads:");
681         for(thread=gSleepingThreads;thread;thread=thread->Next)
682         {
683                 Log("%c%i (%i) - %s",
684                         (thread==cur?'*':' '),
685                         thread->TID, thread->TGID, thread->ThreadName);
686                 Log("  %i Tickets, Quantum %i", thread->NumTickets, thread->Quantum);
687                 Log("  KStack 0x%x", thread->KernelStack);
688         }
689 }
690
691 /**
692  * \fn tThread *Threads_GetNextToRun(int CPU, tThread *Last)
693  * \brief Gets the next thread to run
694  * \param CPU   Current CPU
695  * \param Last  The thread the CPU was running
696  */
697 tThread *Threads_GetNextToRun(int CPU, tThread *Last)
698 {
699         tThread *thread;
700          int    ticket;
701          int    number;
702         
703         // Note: Enable the code to tell if the switch code has the lock, or
704         // if it's the other code.
705         
706         // Check if the thread list is locked by other code
707         // - If so, don't switch (give it a chance to complete)
708         if( IS_LOCKED(&glThreadListLock) )
709                 return Last;
710         
711         // No active threads, just take a nap
712         if(giNumActiveThreads == 0) {
713                 return NULL;
714         }
715         
716         // Lock thread list
717         // - HLT lock (Used because only another CPU can obtain the lock,
718         //   but it has a potentially long lock period)
719         // - Well, this CPU can obtain the lock, but that is aliveviated by
720         //   the above.
721         TIGHTLOCK( &glThreadListLock );
722         
723         // Special case: 1 thread
724         if(giNumActiveThreads == 1) {
725                 if( gActiveThreads->CurCPU == -1 )
726                         gActiveThreads->CurCPU = CPU;
727                 RELEASE( &glThreadListLock );
728                 if( gActiveThreads->CurCPU == CPU )
729                         return gActiveThreads;
730                 return NULL;    // CPU has nothing to do
731         }
732         
733         // Allow the old thread to be scheduled again
734         if( Last ) {
735                 if( Last->Status == THREAD_STAT_ACTIVE )
736                         giFreeTickets += Last->NumTickets;
737                 Last->CurCPU = -1;
738         }
739
740         // Get the ticket number
741         ticket = number = rand() % giFreeTickets;
742         
743         // Find the next thread
744         for(thread=gActiveThreads;thread;thread=thread->Next)
745         {
746                 if(thread->CurCPU >= 0) continue;
747                 if(thread->NumTickets > number) break;
748                 number -= thread->NumTickets;
749         }
750         // Error Check
751         if(thread == NULL)
752         {
753                 number = 0;
754                 for(thread=gActiveThreads;thread;thread=thread->Next) {
755                         if(thread->CurCPU >= 0) continue;
756                         number += thread->NumTickets;
757                 }
758                 Panic("Bookeeping Failed - giFreeTickets(%i) > true count (%i)",
759                         giFreeTickets, number);
760         }
761         
762         // Make the new thread non-schedulable
763         giFreeTickets -= thread->NumTickets;    
764         thread->CurCPU = CPU;
765         
766         RELEASE( &glThreadListLock );
767         
768         return thread;
769 }
770
771 /**
772  * \fn void Threads_SegFault(tVAddr Addr)
773  * \brief Called when a Segment Fault occurs
774  */
775 void Threads_SegFault(tVAddr Addr)
776 {
777         Warning("Thread #%i committed a segfault at address %p", Proc_GetCurThread()->TID, Addr);
778         Threads_Fault( 1 );
779         //Threads_Exit( 0, -1 );
780 }
781
782 // === EXPORTS ===
783 EXPORT(Threads_GetUID);

UCC git Repository :: git.ucc.asn.au