mirror of https://gitlab.com/nakst/essence
introduce threadsMutex in Process
This commit is contained in:
parent
7f928c523f
commit
c173fa1fd8
|
@ -113,7 +113,9 @@ struct Process {
|
||||||
MMSpace *vmm;
|
MMSpace *vmm;
|
||||||
HandleTable handleTable;
|
HandleTable handleTable;
|
||||||
MessageQueue messageQueue;
|
MessageQueue messageQueue;
|
||||||
|
|
||||||
LinkedList<Thread> threads;
|
LinkedList<Thread> threads;
|
||||||
|
KMutex threadsMutex;
|
||||||
|
|
||||||
// Creation information:
|
// Creation information:
|
||||||
KNode *executableNode;
|
KNode *executableNode;
|
||||||
|
@ -135,7 +137,7 @@ struct Process {
|
||||||
|
|
||||||
// Termination:
|
// Termination:
|
||||||
bool allThreadsTerminated;
|
bool allThreadsTerminated;
|
||||||
bool terminating; // This never gets set if TerminateProcess is not called, and instead the process is killed because all its threads exit naturally.
|
bool preventNewThreads; // Set by TerminateProcess.
|
||||||
int exitStatus; // TODO Remove this.
|
int exitStatus; // TODO Remove this.
|
||||||
KEvent killedEvent;
|
KEvent killedEvent;
|
||||||
KAsyncTask removeAsyncTask;
|
KAsyncTask removeAsyncTask;
|
||||||
|
@ -167,7 +169,7 @@ struct Scheduler {
|
||||||
#define SPAWN_THREAD_PAUSED (8)
|
#define SPAWN_THREAD_PAUSED (8)
|
||||||
Thread *SpawnThread(const char *cName, uintptr_t startAddress, uintptr_t argument1 = 0,
|
Thread *SpawnThread(const char *cName, uintptr_t startAddress, uintptr_t argument1 = 0,
|
||||||
uint32_t flags = ES_FLAGS_DEFAULT, Process *process = nullptr, uintptr_t argument2 = 0);
|
uint32_t flags = ES_FLAGS_DEFAULT, Process *process = nullptr, uintptr_t argument2 = 0);
|
||||||
void PauseThread(Thread *thread, bool resume /*true to resume, false to pause*/, bool lockAlreadyAcquired = false);
|
void PauseThread(Thread *thread, bool resume /* true to resume, false to pause */);
|
||||||
|
|
||||||
Process *SpawnProcess(ProcessType processType = PROCESS_NORMAL);
|
Process *SpawnProcess(ProcessType processType = PROCESS_NORMAL);
|
||||||
void PauseProcess(Process *process, bool resume);
|
void PauseProcess(Process *process, bool resume);
|
||||||
|
@ -188,13 +190,13 @@ struct Scheduler {
|
||||||
// - If the last handle to the thread has been closed, then RemoveThread is called.
|
// - If the last handle to the thread has been closed, then RemoveThread is called.
|
||||||
// 4. RemoveThread
|
// 4. RemoveThread
|
||||||
// - The thread structure is deallocated.
|
// - The thread structure is deallocated.
|
||||||
void TerminateThread(Thread *thread, bool lockAlreadyAcquired = false);
|
void TerminateThread(Thread *thread);
|
||||||
|
|
||||||
// How process termination works:
|
// How process termination works:
|
||||||
// 1. TerminateProcess (optional)
|
// 1. TerminateProcess (optional)
|
||||||
// - terminating is set to true (to prevent creation of new threads).
|
// - preventNewThreads is set to true.
|
||||||
// - TerminateThread is called on each thread, leading to an eventual call to KillProcess.
|
// - TerminateThread is called on each thread, leading to an eventual call to KillProcess.
|
||||||
// - This is optional because KillProcess is called if all threads get terminated naturally; in this case, terminating is never set.
|
// - This is optional because KillProcess is called if all threads get terminated naturally; in this case, preventNewThreads is never set.
|
||||||
// 2. KillProcess
|
// 2. KillProcess
|
||||||
// - Destroys the handle table and memory space, and sets killedEvent.
|
// - Destroys the handle table and memory space, and sets killedEvent.
|
||||||
// - Sends a message to Desktop informing it the process was killed.
|
// - Sends a message to Desktop informing it the process was killed.
|
||||||
|
@ -373,35 +375,38 @@ void Scheduler::MaybeUpdateActiveList(Thread *thread) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Scheduler::InsertNewThread(Thread *thread, bool addToActiveList, Process *owner) {
|
void Scheduler::InsertNewThread(Thread *thread, bool addToActiveList, Process *owner) {
|
||||||
|
// Adding the thread to the owner's list of threads and adding the thread to a scheduling list
|
||||||
|
// need to be done in the same atomic block.
|
||||||
|
KMutexAssertLocked(&owner->threadsMutex);
|
||||||
|
|
||||||
|
thread->id = __sync_fetch_and_add(&nextThreadID, 1);
|
||||||
|
thread->process = owner;
|
||||||
|
|
||||||
|
thread->item.thisItem = thread;
|
||||||
|
thread->allItem.thisItem = thread;
|
||||||
|
thread->processItem.thisItem = thread;
|
||||||
|
|
||||||
|
owner->threads.InsertEnd(&thread->processItem);
|
||||||
|
|
||||||
KMutexAcquire(&allThreadsMutex);
|
KMutexAcquire(&allThreadsMutex);
|
||||||
allThreads.InsertStart(&thread->allItem);
|
allThreads.InsertStart(&thread->allItem);
|
||||||
KMutexRelease(&allThreadsMutex);
|
KMutexRelease(&allThreadsMutex);
|
||||||
|
|
||||||
KSpinlockAcquire(&lock);
|
|
||||||
|
|
||||||
// New threads are initialised here.
|
|
||||||
thread->id = __sync_fetch_and_add(&nextThreadID, 1);
|
|
||||||
thread->process = owner;
|
|
||||||
|
|
||||||
// Each thread owns a handles to the owner process.
|
// Each thread owns a handles to the owner process.
|
||||||
// This makes sure the process isn't destroyed before all its threads have been destroyed.
|
// This makes sure the process isn't destroyed before all its threads have been destroyed.
|
||||||
OpenHandleToObject(owner, KERNEL_OBJECT_PROCESS, ES_FLAGS_DEFAULT);
|
OpenHandleToObject(owner, KERNEL_OBJECT_PROCESS, ES_FLAGS_DEFAULT);
|
||||||
|
|
||||||
thread->item.thisItem = thread;
|
|
||||||
thread->allItem.thisItem = thread;
|
|
||||||
thread->processItem.thisItem = thread;
|
|
||||||
owner->threads.InsertEnd(&thread->processItem);
|
|
||||||
|
|
||||||
KernelLog(LOG_INFO, "Scheduler", "create thread", "Create thread ID %d, type %d, owner process %d\n", thread->id, thread->type, owner->id);
|
KernelLog(LOG_INFO, "Scheduler", "create thread", "Create thread ID %d, type %d, owner process %d\n", thread->id, thread->type, owner->id);
|
||||||
|
|
||||||
if (addToActiveList) {
|
if (addToActiveList) {
|
||||||
// Add the thread to the start of the active thread list to make sure that it runs immediately.
|
// Add the thread to the start of the active thread list to make sure that it runs immediately.
|
||||||
|
KSpinlockAcquire(&lock);
|
||||||
AddActiveThread(thread, true);
|
AddActiveThread(thread, true);
|
||||||
|
KSpinlockRelease(&lock);
|
||||||
} else {
|
} else {
|
||||||
// Some threads (such as idle threads) do this themselves.
|
// Idle and asynchronous task threads don't need to be added to a scheduling list.
|
||||||
}
|
}
|
||||||
|
|
||||||
KSpinlockRelease(&lock);
|
|
||||||
// The thread may now be terminated at any moment.
|
// The thread may now be terminated at any moment.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -416,12 +421,11 @@ Thread *Scheduler::SpawnThread(const char *cName, uintptr_t startAddress, uintpt
|
||||||
KernelPanic("Scheduler::SpawnThread - Cannot add userland thread to kernel process.\n");
|
KernelPanic("Scheduler::SpawnThread - Cannot add userland thread to kernel process.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
KSpinlockAcquire(&scheduler.lock);
|
KMutexAcquire(&process->threadsMutex);
|
||||||
bool terminating = process->terminating;
|
EsDefer(KMutexRelease(&process->threadsMutex));
|
||||||
KSpinlockRelease(&scheduler.lock);
|
|
||||||
|
|
||||||
if (shutdown && userland) return nullptr;
|
if (shutdown && userland) return nullptr;
|
||||||
if (terminating) return nullptr;
|
if (process->preventNewThreads) return nullptr;
|
||||||
|
|
||||||
Thread *thread = (Thread *) threadPool.Add(sizeof(Thread));
|
Thread *thread = (Thread *) threadPool.Add(sizeof(Thread));
|
||||||
if (!thread) return nullptr;
|
if (!thread) return nullptr;
|
||||||
|
@ -430,6 +434,11 @@ Thread *Scheduler::SpawnThread(const char *cName, uintptr_t startAddress, uintpt
|
||||||
thread->priority = (flags & SPAWN_THREAD_LOW_PRIORITY) ? THREAD_PRIORITY_LOW : THREAD_PRIORITY_NORMAL;
|
thread->priority = (flags & SPAWN_THREAD_LOW_PRIORITY) ? THREAD_PRIORITY_LOW : THREAD_PRIORITY_NORMAL;
|
||||||
thread->cName = cName;
|
thread->cName = cName;
|
||||||
|
|
||||||
|
// If PauseProcess is called while a thread in that process is spawning a new thread, mark the thread as paused.
|
||||||
|
// This is synchronized under the threadsMutex.
|
||||||
|
Thread *parentThread = GetCurrentThread();
|
||||||
|
thread->paused = parentThread && process == parentThread->process && parentThread->paused;
|
||||||
|
|
||||||
// 2 handles to the thread:
|
// 2 handles to the thread:
|
||||||
// One for spawning the thread,
|
// One for spawning the thread,
|
||||||
// and the other for remaining during the thread's life.
|
// and the other for remaining during the thread's life.
|
||||||
|
@ -523,6 +532,8 @@ void CloseHandleToProcess(void *_process) {
|
||||||
void KillProcess(Process *process) {
|
void KillProcess(Process *process) {
|
||||||
KernelLog(LOG_INFO, "Scheduler", "killing process", "Killing process (%d) %x...\n", process->id, process);
|
KernelLog(LOG_INFO, "Scheduler", "killing process", "Killing process (%d) %x...\n", process->id, process);
|
||||||
|
|
||||||
|
KSpinlockAcquire(&scheduler.lock);
|
||||||
|
|
||||||
process->allThreadsTerminated = true;
|
process->allThreadsTerminated = true;
|
||||||
scheduler.activeProcessCount--;
|
scheduler.activeProcessCount--;
|
||||||
|
|
||||||
|
@ -573,16 +584,16 @@ void KillThread(KAsyncTask *task) {
|
||||||
scheduler.allThreads.Remove(&thread->allItem);
|
scheduler.allThreads.Remove(&thread->allItem);
|
||||||
KMutexRelease(&scheduler.allThreadsMutex);
|
KMutexRelease(&scheduler.allThreadsMutex);
|
||||||
|
|
||||||
KSpinlockAcquire(&scheduler.lock);
|
KMutexAcquire(&thread->process->threadsMutex);
|
||||||
thread->process->threads.Remove(&thread->processItem);
|
thread->process->threads.Remove(&thread->processItem);
|
||||||
|
bool lastThread = thread->process->threads.count == 0;
|
||||||
|
KMutexRelease(&thread->process->threadsMutex);
|
||||||
|
|
||||||
KernelLog(LOG_INFO, "Scheduler", "killing thread",
|
KernelLog(LOG_INFO, "Scheduler", "killing thread",
|
||||||
"Killing thread (ID %d, %d remain in process %d) %x...\n", thread->id, thread->process->threads.count, thread->process->id, thread);
|
"Killing thread (ID %d, %d remain in process %d) %x...\n", thread->id, thread->process->threads.count, thread->process->id, thread);
|
||||||
|
|
||||||
if (thread->process->threads.count == 0) {
|
if (lastThread) {
|
||||||
KillProcess(thread->process); // Releases the scheduler's lock.
|
KillProcess(thread->process);
|
||||||
} else {
|
|
||||||
KSpinlockRelease(&scheduler.lock);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MMFree(kernelMMSpace, (void *) thread->kernelStackBase);
|
MMFree(kernelMMSpace, (void *) thread->kernelStackBase);
|
||||||
|
@ -596,12 +607,12 @@ void KillThread(KAsyncTask *task) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Scheduler::TerminateProcess(Process *process, int status) {
|
void Scheduler::TerminateProcess(Process *process, int status) {
|
||||||
KSpinlockAcquire(&scheduler.lock);
|
KMutexAcquire(&process->threadsMutex);
|
||||||
|
|
||||||
KernelLog(LOG_INFO, "Scheduler", "terminate process", "Terminating process %d '%z' with status %i...\n",
|
KernelLog(LOG_INFO, "Scheduler", "terminate process", "Terminating process %d '%z' with status %i...\n",
|
||||||
process->id, process->cExecutableName, status);
|
process->id, process->cExecutableName, status);
|
||||||
process->exitStatus = status;
|
process->exitStatus = status;
|
||||||
process->terminating = true;
|
process->preventNewThreads = true;
|
||||||
|
|
||||||
Thread *currentThread = GetCurrentThread();
|
Thread *currentThread = GetCurrentThread();
|
||||||
bool isCurrentProcess = process == currentThread->process;
|
bool isCurrentProcess = process == currentThread->process;
|
||||||
|
@ -614,7 +625,7 @@ void Scheduler::TerminateProcess(Process *process, int status) {
|
||||||
thread = thread->nextItem;
|
thread = thread->nextItem;
|
||||||
|
|
||||||
if (threadObject != currentThread) {
|
if (threadObject != currentThread) {
|
||||||
TerminateThread(threadObject, true);
|
TerminateThread(threadObject);
|
||||||
} else if (isCurrentProcess) {
|
} else if (isCurrentProcess) {
|
||||||
foundCurrentThread = true;
|
foundCurrentThread = true;
|
||||||
} else {
|
} else {
|
||||||
|
@ -622,18 +633,17 @@ void Scheduler::TerminateProcess(Process *process, int status) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KMutexRelease(&process->threadsMutex);
|
||||||
|
|
||||||
if (!foundCurrentThread && isCurrentProcess) {
|
if (!foundCurrentThread && isCurrentProcess) {
|
||||||
KernelPanic("Scheduler::TerminateProcess - Could not find current thread in the current process?!\n");
|
KernelPanic("Scheduler::TerminateProcess - Could not find current thread in the current process?!\n");
|
||||||
} else if (isCurrentProcess) {
|
} else if (isCurrentProcess) {
|
||||||
// This doesn't return.
|
// This doesn't return.
|
||||||
TerminateThread(currentThread, true);
|
TerminateThread(currentThread);
|
||||||
}
|
}
|
||||||
|
|
||||||
KSpinlockRelease(&scheduler.lock);
|
|
||||||
ProcessorFakeTimerInterrupt(); // Process the asynchronous tasks.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Scheduler::TerminateThread(Thread *thread, bool terminatingProcess) {
|
void Scheduler::TerminateThread(Thread *thread) {
|
||||||
// Overview:
|
// Overview:
|
||||||
// Set terminating true, and paused false.
|
// Set terminating true, and paused false.
|
||||||
// Is this the current thread?
|
// Is this the current thread?
|
||||||
|
@ -648,11 +658,7 @@ void Scheduler::TerminateThread(Thread *thread, bool terminatingProcess) {
|
||||||
// Else, is the user waiting on a mutex/event?
|
// Else, is the user waiting on a mutex/event?
|
||||||
// If we aren't currently executing the thread, unblock the thread.
|
// If we aren't currently executing the thread, unblock the thread.
|
||||||
|
|
||||||
if (!terminatingProcess) {
|
KSpinlockAcquire(&scheduler.lock);
|
||||||
KSpinlockAcquire(&scheduler.lock);
|
|
||||||
} else {
|
|
||||||
KSpinlockAssertLocked(&scheduler.lock);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool yield = false;
|
bool yield = false;
|
||||||
|
|
||||||
|
@ -718,10 +724,8 @@ void Scheduler::TerminateThread(Thread *thread, bool terminatingProcess) {
|
||||||
|
|
||||||
done:;
|
done:;
|
||||||
|
|
||||||
if (!terminatingProcess) {
|
KSpinlockRelease(&scheduler.lock);
|
||||||
KSpinlockRelease(&scheduler.lock);
|
if (yield) ProcessorFakeTimerInterrupt(); // Process the asynchronous task.
|
||||||
if (yield) ProcessorFakeTimerInterrupt(); // Process the asynchronous task.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void NewProcess() {
|
void NewProcess() {
|
||||||
|
@ -971,7 +975,9 @@ void Scheduler::CreateProcessorThreads(CPULocalStorage *local) {
|
||||||
KernelPanic("Scheduler::CreateProcessorThreads - Maximum processor count (%d) exceeded.\n", local->processorID);
|
KernelPanic("Scheduler::CreateProcessorThreads - Maximum processor count (%d) exceeded.\n", local->processorID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KMutexAcquire(&kernelProcess->threadsMutex);
|
||||||
InsertNewThread(idleThread, false, kernelProcess);
|
InsertNewThread(idleThread, false, kernelProcess);
|
||||||
|
KMutexRelease(&kernelProcess->threadsMutex);
|
||||||
|
|
||||||
local->asyncTaskThread = SpawnThread("AsyncTasks", (uintptr_t) AsyncTaskThread, 0, SPAWN_THREAD_MANUALLY_ACTIVATED);
|
local->asyncTaskThread = SpawnThread("AsyncTasks", (uintptr_t) AsyncTaskThread, 0, SPAWN_THREAD_MANUALLY_ACTIVATED);
|
||||||
local->asyncTaskThread->type = THREAD_ASYNC_TASK;
|
local->asyncTaskThread->type = THREAD_ASYNC_TASK;
|
||||||
|
@ -1101,8 +1107,8 @@ void Scheduler::CrashProcess(Process *process, EsCrashReason *crashReason) {
|
||||||
scheduler.PauseProcess(GetCurrentThread()->process, false);
|
scheduler.PauseProcess(GetCurrentThread()->process, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Scheduler::PauseThread(Thread *thread, bool resume, bool lockAlreadyAcquired) {
|
void Scheduler::PauseThread(Thread *thread, bool resume) {
|
||||||
if (!lockAlreadyAcquired) KSpinlockAcquire(&lock);
|
KSpinlockAcquire(&lock);
|
||||||
|
|
||||||
if (thread->paused == !resume) {
|
if (thread->paused == !resume) {
|
||||||
return;
|
return;
|
||||||
|
@ -1143,39 +1149,20 @@ void Scheduler::PauseThread(Thread *thread, bool resume, bool lockAlreadyAcquire
|
||||||
AddActiveThread(thread, false);
|
AddActiveThread(thread, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!lockAlreadyAcquired) KSpinlockRelease(&lock);
|
KSpinlockRelease(&lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Scheduler::PauseProcess(Process *process, bool resume) {
|
void Scheduler::PauseProcess(Process *process, bool resume) {
|
||||||
Thread *currentThread = GetCurrentThread();
|
KMutexAcquire(&process->threadsMutex);
|
||||||
bool isCurrentProcess = process == currentThread->process;
|
LinkedItem<Thread> *thread = process->threads.firstItem;
|
||||||
bool foundCurrentThread = false;
|
|
||||||
|
|
||||||
{
|
while (thread) {
|
||||||
KSpinlockAcquire(&scheduler.lock);
|
Thread *threadObject = thread->thisItem;
|
||||||
EsDefer(KSpinlockRelease(&scheduler.lock));
|
thread = thread->nextItem;
|
||||||
|
PauseThread(threadObject, resume);
|
||||||
LinkedItem<Thread> *thread = process->threads.firstItem;
|
|
||||||
|
|
||||||
while (thread) {
|
|
||||||
Thread *threadObject = thread->thisItem;
|
|
||||||
thread = thread->nextItem;
|
|
||||||
|
|
||||||
if (threadObject != currentThread) {
|
|
||||||
PauseThread(threadObject, resume, true);
|
|
||||||
} else if (isCurrentProcess) {
|
|
||||||
foundCurrentThread = true;
|
|
||||||
} else {
|
|
||||||
KernelPanic("Scheduler::PauseProcess - Found current thread in the wrong process?!\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!foundCurrentThread && isCurrentProcess) {
|
KMutexRelease(&process->threadsMutex);
|
||||||
KernelPanic("Scheduler::PauseProcess - Could not find current thread in the current process?!\n");
|
|
||||||
} else if (isCurrentProcess) {
|
|
||||||
PauseThread(currentThread, resume, false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Thread *Scheduler::PickThread(CPULocalStorage *local) {
|
Thread *Scheduler::PickThread(CPULocalStorage *local) {
|
||||||
|
@ -1361,7 +1348,7 @@ void Scheduler::Shutdown() {
|
||||||
KSpinlockAcquire(&lock);
|
KSpinlockAcquire(&lock);
|
||||||
Process *process = allProcesses.firstItem->thisItem;
|
Process *process = allProcesses.firstItem->thisItem;
|
||||||
|
|
||||||
while (process && (process->terminating || process == kernelProcess)) {
|
while (process && (process->preventNewThreads || process == kernelProcess)) {
|
||||||
LinkedItem<Process> *item = process->allItem.nextItem;
|
LinkedItem<Process> *item = process->allItem.nextItem;
|
||||||
process = item ? item->thisItem : nullptr;
|
process = item ? item->thisItem : nullptr;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1209,7 +1209,7 @@ SYSCALL_IMPLEMENT(ES_SYSCALL_PROCESS_GET_STATE) {
|
||||||
state.id = process->id;
|
state.id = process->id;
|
||||||
state.executableState = process->executableState;
|
state.executableState = process->executableState;
|
||||||
state.flags = (process->allThreadsTerminated ? ES_PROCESS_STATE_ALL_THREADS_TERMINATED : 0)
|
state.flags = (process->allThreadsTerminated ? ES_PROCESS_STATE_ALL_THREADS_TERMINATED : 0)
|
||||||
| (process->terminating ? ES_PROCESS_STATE_TERMINATING : 0)
|
| (process->preventNewThreads ? ES_PROCESS_STATE_TERMINATING : 0)
|
||||||
| (process->crashed ? ES_PROCESS_STATE_CRASHED : 0)
|
| (process->crashed ? ES_PROCESS_STATE_CRASHED : 0)
|
||||||
| (process->messageQueue.pinged ? ES_PROCESS_STATE_PINGED : 0);
|
| (process->messageQueue.pinged ? ES_PROCESS_STATE_PINGED : 0);
|
||||||
|
|
||||||
|
@ -1275,7 +1275,6 @@ SYSCALL_IMPLEMENT(ES_SYSCALL_SYSTEM_TAKE_SNAPSHOT) {
|
||||||
|
|
||||||
while (item && index < count) {
|
while (item && index < count) {
|
||||||
Process *process = item->thisItem;
|
Process *process = item->thisItem;
|
||||||
if (process->terminating) goto next;
|
|
||||||
|
|
||||||
{
|
{
|
||||||
snapshot->processes[index].pid = process->id;
|
snapshot->processes[index].pid = process->id;
|
||||||
|
@ -1292,7 +1291,6 @@ SYSCALL_IMPLEMENT(ES_SYSCALL_SYSTEM_TAKE_SNAPSHOT) {
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
next:;
|
|
||||||
item = item->nextItem;
|
item = item->nextItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue