combinepdf (3).pdf
Document Details
Uploaded by PrizeCombinatorics
A-B Tech
Tags
Related
- omscs-notes-notes_operating-systems_thread-performance-considerations.md at 1e77184ce6ad5faefba8709d6a249888a005f542 · m4ttsch_omscs-notes-notes · GitHub.pdf
- omscs-notes-notes_operating-systems_midterm-exam-review-questions.md at 1e77184ce6ad5faefba8709d6a249888a005f542 · m4ttsch_omscs-notes-notes · GitHub.pdf
- swe-sheet-sol-full-operating-systems.pdf
- Aula 10 - RTOS - Introdução-8-37.pdf
- QNX Overview PDF
- QNX Overview PDF
Full Transcript
What is QNX? l QNX is a company. l QNX is a development platform. l QNX is local. l QNX is a subsidiary of Research in Motion BlackBerry! Introduction to Neutrino How do I pronounce QNX? l Some people say it phonetically – as in it rhymes with UNIX l Others spell it out: Q –...
What is QNX? l QNX is a company. l QNX is a development platform. l QNX is local. l QNX is a subsidiary of Research in Motion BlackBerry! Introduction to Neutrino How do I pronounce QNX? l Some people say it phonetically – as in it rhymes with UNIX l Others spell it out: Q – N – X. l Either way you say it, you are saying it correctly. The QNX Development Platform l QNX is a Software Development Platform. l It consists of the following: l The Momentics Tool Suite l Neutrino RTOS l An incredible amount of documentation and help manuals. QNX Momentics Tool Suite l Momentics is the IDE provided by QNX to facilitate software development for the Neutrino RTOS. l It is an impressive collection of tools that enable development, debugging, profiling, tracing and diagnostics like no other tool on the market. All of this is wrapped up in an IDE based on Eclipse. l Momentics is run on a “development host”. There are 3 choices of development hosts: Linux, Windows and Mac OS. QNX Neutrino RTOS l Neutrino is the name given to the QNX RTOS – Real-Time Operating System. l Neutrino is a microkernel architecture. l Neutrino is one of the most reliable, highly trusted and flexible operating systems on the planet. l From Wikipedia: “The QNX CAR Application Platform is running in over 20 million vehicles as of mid-2011”. “ What is an RTOS? … l A real-time operating system (RTOS) is an operating system (OS) intended to serve real- time application requests. It must be able to process data as it comes in, typically without buffering delays. Processing time requirements (including any OS delay) are measured in tenths of seconds or shorter. “ … What is an RTOS? … l A key characteristic of an RTOS is the level of its consistency concerning the amount of time it takes to accept and complete an application's task; the variability is jitter. A hard real-time operating system has less jitter than a soft real-time operating system. The chief design goal is not high throughput, but rather a guarantee of a soft or hard performance category. An RTOS that can usually or generally meet a deadline is a soft real-time OS, but if it can meet a deadline deterministically it is a hard real- time OS. “ … What is an RTOS? l An RTOS has an advanced algorithm for scheduling. Scheduler flexibility enables a wider, computer-system orchestration of process priorities, but a real-time OS is more frequently dedicated to a narrow set of applications. Key factors in a real-time OS are minimal interrupt latency and minimal thread switching latency; a real-time OS is valued more for how quickly or how predictably it can respond than for the amount of work it can perform in a given period of time.” - Wikipedia – Real-time Operating System Monolithic Kernels vs Microkernels l In a monolithic kernel architecture, the entire OS and it's services run in the same process thread, and share the same memory space. l In a microkernel, only the bare minimum of the OS is in the kernel process. The rest, including device drivers and file access managers reside in external servers that run in the user space. Mono vs Micro – Kernel Monolithic Kernels Advantages l Less software so should be faster l One piece of code so should be smaller in source and binary forms l Less code typically translates to less bugs Monolithic Kernels Disadvantages l Developing in the kernel space is difficult and requires frequent reboots of the system l Small bugs have larger, more pervasive side effects l Kernels become large and hard to maintain l One bug to rule them all – one can bring down the entire system l Not portable, need to be rewritten for each architecture l Difficult to integrate the different modules Microkernel Advantages l Easier to maintain l Patches can developed in separate instances and swapped over to production l Better persistence – if an instance of a server is corrupted, another can be swapped into place. Microkernel Disadvantages l Larger memory footprint l More software needed for interfacing servers with core kernel l Process management can be more complicated Who Wins? l Most researchers have determined that monolithic kernels are no longer desired. l Microkernel and hybrid kernel architectures have become the dominant forces in kernel/OS design. Kernel Examples (Mono) Monolithic Kernels: l UNIX l BSD and it's variants (FreeBSD, NetBSD, OpenBSD, SunOS) l UNIX System V (AIX, HP-UX, Solaris) l Linux (see the Tanenbaum-Torvalds debate) l DOS Kernel Examples (Micro) Microkernels: l QNX l Debian GNU/Hurd l HelenOS l MINIX and MINIX 3 l Symbian Kernel Examples (Hybrid) l BeOS l XNU (BSD based) – used in Darwin, iOS and OS X l NT Kernel (Windows NT and it's variants including XP, 7, 8) The Neutrino System Architecture Processes, Threads, & Signals Introduction to Processes and Threads What is a Process and what is aThread? ▪ When a Program is loaded into memory it creates a Process. ▪ Each Process is given a unique Process Identification (PID) Number when it is started by the Kernel. ▪ Each Process has it’s own private memory stack. ▪ Each Process can have resource descriptors (i.e. file descriptors) to access many different resources. ▪ Memory ▪ Open Files ▪ Timers ▪ Synchronization Objects ▪ Resources within one Process are not accessible by other Processes Processes contain Threads. ▪ A Thread is a single path of instructions to execute. ▪ Processes must have at least one Thread of execution. ▪ All Threads within a Process share the same Memory stack and resource descriptors. ▪ Threads can have unique attributes. ▪ Scheduling priority and algorithm ▪ Register set ▪ Signals ▪ Memory stack ▪ Bottom Line: Threads run code, Processes own resources. Processes and Threads Visualized Creating Processes ⚫ The most basic way to create a new Process is to use the fork() function. ▪ The fork() function creates a duplicate of the currently running Process. ▪ Continues execution after the fork() function call within both Processes. ▪ New “Child” Process inherits all the same resources as the “Parent” Process. ▪ File descriptors ▪ Threads Fork ⚫ When fork() is called… – The “Parent” Process will receive the PID of the newly created Process as the return value from fork(). – The “Child” Process will receive ‘0’ from the call to fork(). – If an error occurs ‘-1’ will be returned and the global errno will be set to the appropriate error. ⚫ Fork is not supported within Multi-Threaded Processes. Fork ⚫When a Process is created it is always forked off from a currently running Process. – This means every Process is a “Child” of some other Process. – When “Child” Processes die they send their return values to their “Parent”. Orphan/Zombies ⚫ If a “Parent” dies before a “Child” dies the “Child” will have no Process to return to. This creates an Orphan Process. ⚫ If a “Parent” doesn’t wait for a “Child” to finish. This creates a Zombie Process. ▪ Zombies are Processes that are no longer active with the majority of their resources freed but an entry in the Process Table persists. ▪ You can find zombie Processes by using the “pidin” command. Detecting Process Termination ⚫ In many cases a “Parent” will need to know when one of its “Child” Processes has died. There are several ways this can be done. – Call one of the wait() functions. ⚫ Wait() – wait for a child process to terminate. ⚫ Waitid() – wait for a child process to change state. ⚫ Waitpid() – wait for a specific child process to stop or terminate. – Receive a “Child” termination Signal. ⚫ When “Child” Processes terminate they send a SIGCHLD signal to the “Parent” Process. Signals ⚫ Signals are a non-blocking form of Inter-Process Communication. ▪ Signals are asynchronous ▪ Signals can be received at any time. ▪ Signals are POSIX compliant. When a Process receives a Signal it has several options on how to handle it. ▪ Ignore the Signal. ▪ Mask the Signal (block) ▪ Terminate the Process (default behaviour for many Signals). ▪ Use a Signal Handler function to process the Signal. Common Signals ⚫ Common Signals ▪ SIGUSR1 & SIGUSR2 – User defined signals. ▪ SIGINT – Sent when you send ctrl + c to a Process. ▪ SIGSTOP – Sent to pause Process execution. ▪ SIGKILL – Terminate a Process by any means necessary. ▪ SIGTERM – Terminate a Process normally. You can not catch SIGSTOP or SIGKILL Signals. To Deliver a Signal From Code ⚫ To send a Signal to any Process you must either have root privileges or be the same user as the Process receiving the Signal. ⚫ Use kill to deliver a Signal from the code. – int kill (pid_t pid, int sig); ⚫ Pid – The PID of the Process you want to Signal. ⚫ Sig – The Signal you want to send ⚫ Returns 0 on success or -1 on failure. ⚫ Use sigqueue to queue a Signal for a Process. – Int sigqueue (pid_t pid, int sig, const union sigval value); ⚫ Pid – The PID of the Process you want to Signal. ⚫ Sig – The Signal you want to send. ⚫ Value – A value to send with the Signal. To Deliver a Signal From Command Line ⚫ If you want to send a Signal to a Process from the command line you can use the kill shell command. Alternatively you can send the SIGINT Signal to a Process by pressing CTRL + C within the running shell. ⚫ The command is Kill –s Example ⚫ Kill –L // to list the signal numbers and name ⚫ ps –e | grep // to get PID #kill –s signal# PID To Receive a Signal ⚫ To receive a Signal from within your application you need to follow a few steps. ▪ Create a handler sigaction struct. ▪ Populate it with the desired signal handler ▪ Setup any additional signal handling options. ▪ Setup the Signal Mask ▪ This is a list of Signals to block while within the signal handler. Call the sigaction function to catch a specific Signal. struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); }; Create “sigaction” Structure ⚫ The sigaction structure contains the following members. – void (*sa_handler)() – This is your signal handler function for none queued Signals. – void(*sa_sigaction)(int signo, siginfo_t* info, void* other) – This is the signal handler function for queuing signals. – sigset_t sa_mask – A set of signals to mask (block) during a signal handler. – int sa_flags – Special flags to affect signal behaviour. ⚫ SA_NOCLDSTOP – Tells the system not to generate a SIGCHLD signal for Child Processes that SIGSTOP. ⚫ SA_SIGINFO – Tells the OS to queue this signal. Create a Signal Mask ⚫ The sigaction structure has a member sa_mask which sets up a Signal mask. This mask is setup with bitwise operations on a variable. Thankfully a number of helper functions have been created to do the bitwise work for us. – Sigemptyset(*sigset_t) – Remove all signals from sigset_t. – Sigaddset(*sigset_t, int signo) – Add the signal in signo to the mask. – Sigfillset(*sigset_t) – Add all signals to sigset_t. – Sigdelset(*sigset_t, int signo) - Remove the signal in signo from the mask. DESCRIPTION The sigfillset() function shall initialize the signal set pointed to by set, such that all signals defined in this volume of IEEE Std 1003.1-2001 are included. Signal Default Description SIGABRT Process abort signal. SIGALRM Alarm clock. SIGCHLD Child process terminated, stopped, SIGCONT Continue executing, if stopped. SIGINT Terminal interrupt signal. SIGKILL Kill (cannot be caught or ignored). SIGPIPE Write on a pipe with no one to read it. SIGQUIT Terminal quit signal. SIGUSR1 User-defined signal 1. SIGUSR2 User-defined signal 2. SIGPOLL ….. …. … https://pubs.opengroup.org/onlinepubs/9699919799/ Call “sigaction” Function ⚫ Finally you call the sigaction function to set up both the signal mask and set a signal handler for a specific signal. ⚫ int sigaction (int signo, const struct sigaction *act, struct sigaction * oact); – Signo is the signal you will be attaching a handler to. – Act is a pointer to a sigaction structure that defines which signal you want to modify. – Oact is a pointer to a sigaction structure which will be filled with the current action for the desired signal. Signal Example Handler Example // Signals are numbered from 1, signal 0 doesn't exist // Main body of "work" during which two signals volatile sig_atomic_t last_received_signal = 0; // will be raised, after 5 and 10 seconds, and which // will print last received signal void signal_catcher(int signo, siginfo_t *info, void *context) for (int i = 1; i si_signo; if (i == 5) } { Tick #1, last caught signal: 0 if (0 != raise(SIGUSR1)) Tick #2, last caught signal: 0 int main (int argc, char** argv) { Tick #3, last caught signal: 0 { perror("Can't raise SIGUSR1"); Tick #4, last caught signal: 0 // Setup a signal handler for SIGUSR1 and SIGUSR2 return EXIT_FAILURE; Tick #5, last caught signal: 30 struct sigaction act; } Tick #6, last caught signal: 30 memset(&act, 0, sizeof (act)); } Tick #7, last caught signal: 30 Tick #8, last caught signal: 30 act.sa_sigaction = signal_catcher; if (i == 10) Tick #9, last caught signal: 30 act.sa_flags = SA_SIGINFO; { Tick #10, last caught signal: 31 if (0 != raise(SIGUSR2)) Tick #11, last caught signal: 31 if (0 != sigaction(SIGUSR1, &act, NULL)) { Tick #12, last caught signal: 31 { perror("Can't raise SIGUSR2"); Tick #13, last caught signal: 31 perror("sigaction () failed installing SIGUSR1 handler"); return EXIT_FAILURE; Tick #14, last caught signal: 31 } Tick #15, last caught signal: 31 return EXIT_FAILURE; } } printf("Tick #%d, last caught signal: %d\n", if (0 != sigaction(SIGUSR2, &act, NULL)) i, last_received_signal); { perror("sigaction() failed installing SIGUSR2 handler"); sleep(1); } return EXIT_FAILURE; } return EXIT_SUCCESS; }//end of main() Other Details ⚫ To mask signals for each thread instead of for the entire process: – use pthread_sigmask(). ⚫ You can use the sigwaitinfo() function to define a list of signals to wait on. Important notes: ⚫ Signal handlers are designed to be small and fast. You should always limit the amount of work you do within the handler. ⚫ Some functions are not safe to call in Signal Handlers. This will be mentioned in their documentation. ⚫ If a Thread is blocked and receives a Signal it will not be blocked once it leaves the Signal Handler. Threads Introduction... ❑ When a program is loaded into memory it becomes a Process. Every Process contains at least one Thread. ❑ Threads are single path of execution. ▪ Threads are the smallest sequence of instructions that can be scheduled within an Operating system. ▪ Threads allow a Process to accomplish more then one task simultaneously.... Introduction ❑ Examples of when you would use multiple Threads. ▪ Higher priority thread to service hardware requests. ▪ Pools of worker Threads to handle multiple service request from your Program. Processes Example - Assembly line drill drill (large drill bit) (small drill bit) stamper conveyor belt conveyor belt motor the gadgets that are being made All content copyright Processes, Threads & Synchronization BlackBerry Limited 2021/12/17 R20 4 Thread Creation... ❑ How do you create Threads in your own Programs? #include pthread_create (pthread_t *tid, pthread_attr_t *attr, void *(*func) (void *), void *arg); ▪ pthread_t *tid – Pointer to a pthread_t object where the function can store the thread ID of the new Thread or NULL. ▪ pthread_attr_t *attr – Pointer to a pthread_atttr_t object which specifies attributes for the new Thread or NULL. ▪ void * (*func)(void *) – The function where the new Thread will begin execution. ▪ void * arg – Pointer to an argument you want to pass to the new Thread or NULL.... Thread Creation ❑ Return values. ▪ eOK – Success ▪ eAGAIN – Insufficient system resources to create the Thread. ▪ eFAULT – An error occurred trying to access the buffers or the function being used for the new Thread. ▪ eINVAL – Invalid Thread attribute in the attr object. The pthread_create function creates a new thread which will begin execution immediacy in the function you specified with the attributes you specified. The new Thread inherits the signal mask from the original Thread with no pending signals. PTHREAD_CANCEL_ENABLE Setting Thread Attributes ❑ The pthread_attr_t object allows us to set the default behaviour for a Thread when it is created. ▪ You can manually edit this object by using bitwise operations to set the desired properties. However this is messy that is why helper functions have been created to do this for you. ▪ You should always start by initializing the default Thread options into the pthread_attr_t object. ▪ Do this by calling pthread_attr_init(attr) ▪ The default Thread attributes are. ▪ Cancellation requests are held pending until a cancellation point (PTHREAD_CANCEL_ENABLE). ▪ Threads are put into a zombie state when they terminate, and they stay in this state until you retrieve their exit status or detach them (PTHREAD_CREATE_JOINABLE). ▪ Threads inherit the scheduling policy of their parent thread (PTHREAD_INHERIT_SCHED). ▪ Threads are scheduled against all threads in the system (PTHREAD_SCOPE_SYSTEM). ▪ The stack attributes are set so that the kernel will allocate a 4 KB stack for new threads and free the stacks when the threads terminate. Setting Attributes … ❑ The following are all Functions you can use to set attributes. ▪ pthread_attr_setdetachstate() ▪ pthread_attr_setinheritsched() ▪ pthread_attr_setschedparam() ▪ pthread_attr_setschedpolicy() ▪ pthread_attr_setstackaddr() ▪ pthread_attr_setstacksize() … Setting Attributes … ❑pthread_attr_setdetachstate(pthread_attr_t* attr, int detachstate) ▪ This function allows you to set the detach state for a thread. By default a Thread does not die until you attach (join) to it and get the return value or you set it the thread to detach. ▪ pthread_attr_t – A pointer to the pthread_attr_t object. ▪ detachstate – One of the following. ▪ PTHREAD_CREATE_JOINABLE – Create the thread in a joinable state. ▪ PTHREAD_CREATE_DETACHED – Create the thread in a detached state. … Setting Attributes … ❑pthread_attr_setinheritsched(pthread_attr_t* attr, int inheritsched) ▪ This function allows you to set the scheduling policy for the new thread. This is the scheduling policy the kernel uses when giving this thread processor time. ▪ pthread_attr_t – A pointer to the pthread_attr_t object. ▪ inheritsched– One of the following. ▪ PTHREAD_INHERIT_SCHED – Inherit the scheduling policy of the parent. ▪ PTHREAD_EXPLICIT_SCHED – Use the scheduling policy set in attr for the new thread. ▪ If this policy is used then you must set the priority and scheduling algorithm. … Setting Attributes … ❑pthread_attr_setschedparam(pthread_attr_t *attr, ❑ const struct sched_param * param) ❑This function allows you to set the sets the scheduling priority attribute in attr using the value from param. ▪pthread_attr_t – A pointer to the pthread_attr_t object. ▪param - points to a user-defined scheduling parameter object … Setting Attributes … ❑ pthread_attr_setschedpolicy(pthread_attr_t* attr, int policy) ▪ This function allows you to set the scheduling algorithm used by the Kernel when scheduling this thread for time on the CPU. ▪ pthread_attr_t – A pointer to the pthread_attr_t object. ▪ policy– One of the following. ◦ SCHED_FIFO — first-in first-out scheduling. ◦ SCHED_RR — round-robin scheduling. ◦ SCHED_OTHER — currently the same as SCHED_RR. ◦ SCHED_NOCHANGE — don't change the policy. ◦ SCHED_SPORADIC — sporadic scheduling. … Setting Attributes ❑ pthread_attr_setstackaddr & pthread_attr_setstacksize ▪ These functions allow you to modify the stack size and location of the new thread. ▪ (rarely necessary) The Basic Example #include void* foo(void *arg) { printf( "This is thread %d\n", pthread_self() ); return( 0 ); } int main(int argc, char *argv[]) { pthread_attr_t attr; pthread_attr_init( &attr ); printf("This is thread %d\n", pthread_self()); pthread_create( NULL, &attr, &foo, NULL); sleep( 10 ); return EXIT_SUCCESS; } Cleaning up Thread Data Structures ❑ After you have started your thread there is still one more thing you should always do. You should clean up the data structures you used in the thread setup process. ❑ Specifically you should clean up your pthread_attr_t object. ▪ This is done by calling pthread_attr_destroy() with your pthread_attr_t object. ▪ However this doesn’t exactly free any memory when using QNX Neutrino. This function is included for POSIX compliance. ▪ To keep your program POSIX compliant you should call pthread_attr_init on the pthread_attr_t object again if you plan to reuse the object. Thread Termination ❑ Sometimes you want to know when your threads terminate. ❑ If you created a thread and left it joinable (you didn’t set it as detached in the attributes) then you can wait for it to die and find out why. ▪ The pthread_join function allows any thread to wait on any other thread and read its return status. ❑ int pthread_join(pthread_t thread, void** value_ptr) ▪ pthread_t – the target thread you want to wait for. ▪ value_ptr – Null or a pointer to a location where the return value from the thread can be placed. ❑ Remember pthread_join will block the calling thread until the target exits. If the target thread has already stopped then pthread_join will immediately return. ❑ Additionally if you would like an asynchronous way to be notified when a thread dies you can register for a Pulse to be sent to you but that is a subject for another lecture. #include int main(void) { #include pthread_t tid; #include pthread_attr_t attr; struct operands toperands; struct operands { int product; int operand1; int operand2; toperands.operand1 = 2; }; toperands.operand2 = 6; void *task(struct operands *toperands) { pthread_attr_init(&attr); return &(toperands->operand1 * toperands->operand2); pthread_create(&tid, &attr, &task, (void*) &toperands); pthread_join(tid, (void**) &product); } printf("From main, product is = %d\n", product); printf("From thread id: %d\n", tid); return EXIT_SUCCESS; } Thread Operations ❑ The following is a list of thread operations you can perform within your programs. ▪ pthread_exit – Send a signal to a thread. ▪ Pthread_cancel – cancel(kill) a thread within the local process. ▪ pthread_kill – Send a signal to the desired thread. ▪ pthread_detach – Make the current thread detachable. ▪ pthread_self – Return the thread id (tid) of the current thread. ▪ pthread_equal – Compare two thread ids to determine if they are equal. ▪ pthread_setschedparam – Set the priority and scheduling algorithm for the current thread. ▪ pthread_getschedparam – Get the priority and scheduling algorithm for the current thread. ❑ Look these functions up in the QNX documentation for more information. Thread Synchronization Introduction All the threads within a process share the same resources. How do threads manager who gets access to which resource at which time? We call programs with more then one thread running at once a “concurrent program”. The answer is “Thread Synchronization”. Any code that access a shared resource is called a “Critical Section” Synchronization is done by using very special objects provided by the kernel. Mutual Exclusion known as a Mutex. Semaphore Conditional Variables Examples of when you would use these. Restricting access to shared memory. Notifying when a memory region is available to read from. Example Lets look at an example of when thread synchronization would be needed. Mutex … – Mutex A method for controlling access to a shared resource which should have mutual exclusion A mutex has two states (locked or unlocked). If a thread tries to lock a mutex, but discovers that it is already locked, the thread will block until the mutex unlocks (i.e. another thread unlocks it) A mutex which controls a shared region should be at the beginning of that region where it is easy to find. pthread_mutex_t *mutex; char *smp, *buff; smp=mmap(0,4096,PROT_READ|PROT_WRITE,MAP_SHARED,mem,0); mutex = (pthread_mutex_t *) smp; buff = smp + sizeof(pthread_mutex_t); … Mutex … ◦ The basic mutex functions are: ◦ pthread_mutex_init() ◦ initializes a mutex ◦ pthread_mutex_lock() ◦ locks a mutex ◦ if the mutex is already locked, then the thread blocks until it has acquired the mutex ◦ pthread_mutex_trylock() ◦ attemps to lock a mutex, it is already locked, this function will fail but the thread will not block ◦ pthread_mutex_unlock() ◦ unlocks a mutex ◦ pthread_mutex_destroy() ◦ releases a mutex when you are finished with it … Mutex … ◦ pthread_mutex_init() #include int pthread_mutex_init( pthread_mutex_t* mutex, const pthread_mutexattr_t* attr ); ◦ mutex points to the location of the mutex being initialized ◦ attr points to a structure which defines the attributes for this mutex ◦ we will use NULL here which will set the default attributes ◦ pthread_mutex_init() returns: ◦ EOK - Success. ◦ EAGAIN - All kernel synchronization objects are in use. ◦ EBUSY - Previously initialized but non-destroyed mutex. ◦ EFAULT - A fault occurred when kernel tried to access mutex or attr. ◦ after initialization the mutex is in the unlocked state ◦ pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; ◦ can be used to initialize statically declared mutexes ◦ pthread_mutex_lock() int pthread_mutex_lock( pthread_mutex_t* mutex ); ◦ mutex points to the location of the mutex being locked ◦ pthread_mutex_lock() returns: ◦ EOK - Success. ◦ EAGAIN - Insufficient system resources available to lock the mutex. ◦ EDEADLK - Calling thread already owns mutex and the mutex doesn't allow recursive behaviour. ◦ EINVAL - Invalid mutex. ◦ pthread_mutex_trylock() int pthread_mutex_trylock( pthread_mutex_t* mutex ); ◦ mutex points to the location of the mutex attempting to locked ◦ pthread_mutex_trylock() returns: ◦ EBUSY - The mutex was already locked. ◦ rest the same as pthread_mutex_lock() return values above ◦ pthread_mutex_unlock() #include int pthread_mutex_unlock( pthread_mutex_t* mutex ); ◦ mutex points to the location of the mutex to unlocked ◦ pthread_mutex_unlock() returns: ◦ EOK - Success. ◦ EINVAL - Invalid mutex. ◦ EPERM - Current thread doesn't own mutex. ◦ pthread_mutex_destroy() #include int pthread_mutex_destroy( pthread_mutex_t* mutex ); ◦ mutex points to the location of the mutex to destroy ◦ pthread_mutex_destroy() returns: ◦ EBUSY - The mutex is locked. ◦ EOK and EINVAL described in pthread_mutex_unlock() above Example Lets revisit our previous example but now using a Mutex object to control access to the shared variable. Things to Consider Try to keep a mutex locked for as short a time as possible. Remember other Threads all block while waiting on a Mutex. If these Threads are critical you could “Mutex Lock” your Program. Risk involving mutex: Example-1 void main() { …. pthread_mutex_t lock; if (pthread_mutex_init(&lock, NULL) != 0) { printf("\n mutex init failed\n"); return 1; } pthread_mutex_lock(&lock); sharedVariable = doSomeThing (); pthread_mutex_unlock(&lock); …. …. } Example-2 Thread 1 Thread 2 pthread_mutex_lock(&m1); pthread_mutex_lock(&m2); pthread_mutex_lock(&m2); pthread_mutex_lock(&m1); pthread_mutex_unlock(&m1); pthread_mutex_unlock(&m2); pthread_mutex_unlock(&m2); pthread_mutex_unlock(&m1); How to Avoid 1. Make sure that whenever threads lock multiple mutexes, they do so in the same order. 2. pthread_mutex_trylock() if(pthread_mutex_trylock(&m1)==0) else break; pthread_mutex_unlock(&m2); Semaphores – Semaphores are very much like a mutex. The key difference is a semaphore allows more then one thread to gain access to it at a time. A semaphore is essentially an integer value that is decremented whenever a thread gets access to it. Once the value hits zero the next thread to try and gain access to the semaphore will block or return an error. Threads “post” and “wait” on a semaphore. – A post will increase a semaphore’s value by one. – A wait will decrement a semaphore by one. If you wait on a semaphore that is positive you will not block. Semaphores are also asynchronous safe. This means you can access them in a signal handler. Semaphores are designed to work between processes. They can even span across a network on Neutrino. Semaphores come in two flavours. Named or unnamed. – Unnamed are designed to work more efficiently within a program. – Named semaphores can span across processes. However a named semaphore requires a kernel lookup. Common Usage The standard method for controlling access to a shared memory region is through semaphores. A semaphore which controls a shared region should be at the beginning of that region where it is easy to find. sem_t *sem; char *smp, *buff; smp=mmap(0,4096,PROT_READ|PROT_WRITE,MAP_SHARED,mem,0); sem = (sem_t *) smp; buff = smp + sizeof(sem_t); Semaphore Functions … The basic semaphore functions are: sem_init() initialize an unnamed semaphore sem_open() create or access a named semaphore sem_post() increment a named or unnamed semaphore sem_wait() decrement a named or unnamed semaphore; if the semaphore = 0, the calling process blocks until another process does a sem_post sem_trywait() decrement a named or unnamed semaphore; if the semaphore = 0, the function call fails rather than blocking sem_destroy() destroy an unnamed semaphore sem_close() close a named semaphore sem_unlink() destroy a named semaphore … Semaphore Functions … sem_init() #include int sem_init(sem_t *sem, int pshared, unsigned value); sem points to the location of the semaphore being initialized pshared indicates whether the semaphore can be shared by other processes (non-zero) or can only be used by the calling process (zero) value is the initial value of the semaphore (zero or positive) sem_init() returns -1 if an error occurs sem_open() #include sem_t* sem_open(const char* sem_name, int oflags,...); sem_name the semaphore name you want to register. oflags combination of O_EXCL and O_CREAT. … This can be mode_t mode or/and unsigned int value … Semaphore Functions … sem_post() #include int sem_post(sem_t *sem); sem points to the location of the semaphore being incremented sem_post() returns 0 if successful, or -1 if an error occurs sem_wait() #include int sem_wait(sem_t *sem); sem points to the location of the semaphore being decremented sem_wait() returns zero if successful, or -1 if an error occurs sem_trywait() #include int sem_wait(sem_t *sem); sem points to the location of the semaphore being decremented sem_trywait() returns zero if successful, or -1 if an error occurs … Semaphore Functions sem_destroy() #include int sem_destroy(sem_t *sem); sem points to the location of the semaphore being destroyed sem_destroy() returns 0 if successful, or -1 if an error occurs sem_close() #include int sem_close(sem_t *sem); sem points to the location of the semaphore being closed sem_close() returns zero if successful, or -1 if an error occurs sem_unlink() #include int sem_unlink(const char* sem_name); sem_name the name of the semaphore being destroyed sem_unlink() returns 0 if successful, or -1 if an error occurs Example Lets take a look at an example. Things to Remember When using an unnamed semaphore a post/wait call will call the kernel directly. When using a named semaphore messages are sent to the mqueue service which then makes the kernel call. Unnamed semaphores will be faster but named semaphores allow you to use them across processes. Condvars Consider a simple case where: – we need to block, waiting for another thread to change a variable: volatile int state; thread_1 () { while (1) { // wait until “state” changes, // then, perform some work } } – condvars provide a mechanism for doing this All content copyright Processes, Threads & Synchronization BlackBerry Limited 2021/12/17 R20 23 Condvars - Example An example: data hardware provider 2 handling thread 1 3 1. a data provider thread gets some data, likely from a client process, and adds it to a queue 2. it tells the hardware handling thread that data’s there 3. the hardware writing thread wakes up, removes the data from the queue and writes it to the hardware To do all this we need two things: a mutex to make sure that the two threads don’t access the queue data structure at the same time a mechanism for the data provider thread to tell the hardware writing thread to wake up All content copyright Processes, Threads & Synchronization BlackBerry Limited 2021/12/17 R20 24 Process Scheduling Scheduling Threads – Sharing Resources ⚫ within multi threaded processes, threads often must share resources (such as memory) - this requires synchronization ⚫ there are different ways of achieving synchronization depending on the restrictions you wish to place on the resource ⚫ we have seen two synchronization objects: – a mutex - allows only one thread access to a resource at a time – a semaphore - allows a fixed number of threads access to a resource at one time ⚫ we now must consider how the OS decides which thread gets a resource if it becomes free and more than one thread is waiting for this resource Sharing Resources … – Thread priorities with synchronization objects ⚫ suppose one thread owns a mutex associated with a particular resource and two other threads are waiting for the same resource – the thread which owns the mutex gives it up when finished with the resource – which thread should get the mutex? ⚫ a “fair” way to decide would be based on – priority ⚫ which thread needs the resource more urgently ⚫ which thread is more important in the overall process – length of wait ⚫ which thread has been waiting the longest ⚫ Neutrino first uses priority level and then length of wait to arbitrate between threads … Sharing Resources … – Neutrino priority levels ⚫ the priority range is 0 (low) to 255 (high) ⚫ the programmer can assign a thread a priority between 1 and 63 – there is a special thread called the “idle thread” which has a priority 0 ⚫ the higher the priority, the more likely the thread will be to receive resources ⚫ if more than one thread has the same priority level, then Neutrino will consider length of wait … Sharing Resources … – CPU usage by threads ⚫ the CPU is actually just another resource ⚫ the kernel is responsible for synchronizing CPU usage among the threads requiring use of this resource ⚫ as with other resources, rules exist to establish which thread can use this resource at any given time ⚫ when the kernel decides that a thread should be given time to use the CPU is must switch contexts – save the currently running thread’s context information ⚫ including register values, flags, stack information, etc. – load the new thread’s context into the CPU Scheduling Algorithms … – Pre-emption ⚫ ifa thread is currently using the CPU and a second thread of greater priority requires CPU time, the first thread is preempted – in other words the kernel switches contexts, resulting in the first thread being put aside while the second thread is given CPU time – when all the threads of higher priority are finished with the CPU, the thread that was preempted is then given CPU time again. This is called resumption … Sharing Resources – Determining CPU usage ⚫ if there is only one thread that currently requires CPU usage, the kernel’s job is simple ⚫ if there are several threads waiting for the CPU, the kernel considers two factors when determining which thread will be given CPU time and by how much(in order of importance): – priority (as seen already with synchronization objects) – scheduling algorithm … Scheduling Algorithms … – Scheduling Policies ⚫ ifthere are multiple threads of the same priority waiting for CPU time and the CPU becomes available, the kernel then considers the scheduling algorithm (policy) associated with the threads. There are two possible policies for threads in Neutrino: – FIFO - First In First Out – RR - Round Robin … Scheduling Algorithms … – FIFO – First In First Out ⚫ a thread is allocated CPU time for as long as it needs it ⚫ CPU will not be free for other threads until current thread is finished – or until a thread of higher priority requires the CPU ⚫ suppose the thread using the CPU contained an infinite loop – no thread of lower priority would ever get CPU time – no thread of the same priority would ever get CPU time Priority Ready Threads 55 A(f)R, B(f) 54 C(f) 53 … Scheduling Algorithms … – Round Robin ⚫ The default scheduling algorithm for new threads. ⚫ a thread is allocated CPU time for a predefined time-slice ⚫ when the thread finishes with the CPU before the time-slice runs out, then the kernel will assign CPU time to another thread based on priority and policy ⚫ if the time-slice runs out before the current thread completes its usage of the CPU, then the kernel checks if any other threads of the same priority are waiting for CPU time. If so, the context will be switched that the first thread will then have to wait for the CPU Priority Ready Threads 55 A(r)R, B(r) 54 C(r) 53 … Scheduling Algorithms … ▪ Sporadic ▪ Sporadic allows a thread to run at a high priority for a set period of time (budget). ▪ Once the budget has been exhausted the thread will drop to a lower priority and continue execution there. ▪ Eventually a “replenishment operation” triggers which bumps the thread back to its full priority and it begins to consume its budget again. Priority Ready Threads Priority Ready Threads 55 A(S)R, B(r) 55 B(r)R 54 C(r) 54 C(r) 53 53 A(s) … Scheduling Algorithms … – summary of kernel CPU time allocation rules ⚫ highest-priority thread given CPU time ⚫ thread will continue to have CPU time until: – it exits – it blocks (we will discuss this shortly) – it uses all it’s time-slice if the policy for this thread was RR ⚫ an example: Priority Ready Threads 55 A(r)R, B(f) 54 C(r) 53 D(f) – what does the chart look like after thread A consumes it’s time-slice? … Scheduling Algorithms … Priority Ready Threads 55 B(f)R, A(r) 54 C(r) 53 D(f) – thread B will run until it exits or a higher priority thread comes along – assume B exits and then A exits, what does the chart look like? Priority Ready Threads 55 54 C(r)R – suppose C consumes it’s time slice, 53 D(f) what happens? … Scheduling Algorithms Priority Ready Threads 55 54 C(r)R – no other thread is at same priority as C, so it continues to use CPU 53 D(f) Priority Ready Threads 55 F(r) – what does the chart look like if thread E(f) starts now with priority 53 and thread F(r) starts with priority 55? 54 53 D(f), E(f)R Priority Ready Threads 55 F(r)R 54 53 D(f), E(f) How to view Thread Priority ▪ If you want to see what the current priority and scheduling algorithm is in use you can use “pidin”. ▪ The fourth column shows the priority and the r/f indicate which algorithm is in use. Manipulating Priority and Scheduling ▪ Now that you know about priority and scheduling algorithms how do you apply them to your programs? ▪ We can set the prority by calling pthread_setschedprio(pthread_t tid, int prio) ▪ Tid – The thread ID of the thread we want to change. This can be recovered from pthread_create or from pthread_self. ▪ Prio – The new priority you want to set the thread at. ▪ We can set the scheduling algorithm for a thread by calling pthread_setschedparam(pthread_t tid, int policy, const struct sched_param* param) ▪ Tid – The thread ID of the thread we want to change. This can be recovered from pthread_create or from pthread_self. ▪ Policy – The new scheduling policy. ▪ Param – A structure used to define specific scheduling policy attributes. Processes and Threads – States … – The State of a Thread ⚫ We’ve already seen that a thread could be running (using the CPU) or in a queue waiting for the CPU. These are two of the possible states of a thread. ⚫ The state refers basically to what the thread is doing (or not doing) at any given time. … Processes and Threads – States … ⚫ Somestates: – RUNNING - thread is currently using the CPU – READY - in a queque, ready and waiting for the CPU – BLOCKED - the thread is waiting for something to happen There are several blocked states, depending on what the thread is waiting for. Two blocked states that we have already encountered are: ⚫ MUTEX - blocked waiting for a mutex to unlock ⚫ SEM - blocked waiting for a semaphore value to increase above 0 we’ll see other blocked states as we go through the course … Processes and Threads – States … – kernel CPU time allocation rules (again) ⚫ highest-priority thread given CPU time ⚫ thread will continue to have CPU time until: – it exits – it blocks – it uses all it’s time-slice if the policy for this thread was RR Priority Ready Threads Blocked Threads ⚫ an example: 26 A(r)R B(f) 25 C(r) 24 D(f) E(r), F(f) – what does the chart look like if thread A blocks? … Processes and Threads – States … Priority Ready ThreadsBlocked Threads – thread C is now highest priority, non-blocked thread 26 B(f), A(r) – what would the chart look like if F unblocks and C 25 C(r)R exits? 24 D(f) E(r), F(f) Priority Ready Threads Blocked Threads 26 B(f), A(r) – suppose B unblocks, what 25 happens? 24 D(f)R, F(f) E(r) … Processes and Threads - States Priority Ready Threads Blocked Threads 26 B(f)R A(r) 25 24 D(f), F(f) E(r) ⚫ Notice: – The blocked threads are not in a queue. They are just waiting to unblock. They could unblock in any order. – Once a thread has unblocked, it moves to the end of the ready queue for its priority level – Our diagrams omit much of the detail that the OS must track, such as: ⚫ why a thread is blocked (MUTEX, SEMAPHORE, etc.) ⚫ which other thread has blocked the thread (e.g. who owns the mutex) ⚫ among other details. Inter-Process Communication Why Inter-Process Communication? … Now that we are able to start up multiple processes, we need to bring them together into a working system One of the key elements is how these processes communicate with each other “Why do processes need to communicate?” e.g. a system retrieves data from some measuring instrument and analyzes it - if one process retrieves the data, and another process analyzes it, the first process must transmit that data to the second. e.g. an assembly line at a bottling plant - one process controls the conveyor belt and a another fills the bottles, then these two processes must be able to synchronize themselves; the conveyor must be motionless when the filling begins and the filling must be finished when the conveyor starts moving again e.g. - In client-server architecture one process (the server) services requests from other processes (the clients); the client must communicate its needs to the server, and the server must communicate the results to the correct client. … Why Inter-Process Communication? … Data-flow requirements – e.g. a system retrieves data from some measuring instrument and analyzes it - if one process retrieves the data, and another process analyzes it, the first process must transmit that data to the second. Synchronization of process steps – e.g. an assembly line at a bottling plant - one process controls the conveyor belt and a another fills the bottles, then these two processes must be able to synchronize themselves; the conveyor must be motionless when the filling begins and the filling must be finished when the conveyor starts moving again On-demand requests in client-server – e.g. - In client-server architecture one process (the server) services requests from other processes (the clients); the client must communicate its needs to the server, and the server must communicate the results to the correct client. … Why Inter-Process Communication? – The needs expressed in the previous three examples can be summarized as the needs to (i) synchronize processes and (ii) communicate data – We will look at several methods of IPC which can be categorized according to two criteria: (i) blocking vs. non-blocking, and (ii) synchronous vs. asynchronous Blocking vs Non-Blocking – blocking vs. non-blocking blocking methods cause the transmitting process to block until the receiving process handles the transmitted data non-blocking methods do not cause the transmitting process to block – synchronous vs. asynchronous in synchronous methods, the receiving process can only handle the transmitted data at a certain point in its code; Can be blocking/nonblocking in asynchronous methods, the receiving process can handle the transmitted data at any point in its code IPC - Message Passing – Message passing is the primary form of IPC used by QNX anything that passes through the micro-kernel, even something as basic as opening a file, does so by message passing! our major concern is how to get user processes to pass messages back and forth – The Basics message passing has at it’s core 3 functions: MsgSend(), MsgReceive() and MsgReply() two supporting functions required in Neutrino are ChannelCreate() and ConnectAttach() Message Passing – Phases – Suppose a client process wants to send a message to server process and have the server do something with it. Phase One - setting up the connection [Hand Shake) The server process must create a channel ChannelCreate() to which the client process will be able to attach to. The client process must connect to the server’s channel ConnectAttach() before it can attempt to send a message. Phase Two - message passing The client process uses a MsgSend() call to transmit the message to the server. The server process uses a MsgReceive() call to copy the message into its own memory. when the server has finished doing whatever it has to do with the client’s message, it issues a MsgReply() call to inform the client that processing is complete, and to transmit any “response” data. Phase Three – Disconnect When the client is done communicating it calls ConnectDetach(). When the server is ready to close the channel it calls ChannelDestroy(). Messages and Process States - Blocking message passing is a blocking form of IPC – in the above example, as soon as the client does its MsgSend(), it blocks and remains blocked until the server makes its MsgReply() call. – If the client process calls MsgSend(), before the server calls its MsgReceive(), it becomes “Send-blocked”. The client process remains Send-blocked until the server process calls MsgReceive(), at which point the client becomes “Reply-blocked”. when the server process calls MsgReply(), the client process unblocks. … Messages and Process States – Blocking – if the server calls MsgReceive() before the client calls its MsgSend() when the server calls MsgReceive() it becomes "Receive-blocked”. The server remains Receive-blocked until the client calls MsgSend(), at that point the server becomes unblocked. Message Passing Functions - Server … int ChannelCreate ( unsigned flags ) – Create a communication channel. flags – a number of flags to change how a channel notifies the calling thread. For now just use 0. Returns the channel ID (chid) of the new channel or -1 if an error occurs and sets the errno. int MsgReceive ( int chid, void * msg, int bytes, struct _msg_info * info) – Receive a message from a client on the communication channel. chid – The channel ID for the communication channel. msg – A buffer to receive the message in. bytes – The size of the buffer to receive the message in. Info – NULL or a pointer to a structure which receives additional information about the message. Returns a receive id (rcvid) used to reply or -1 if an error occurred and set errno. Header file: #include … Message Passing Functions – Server … int ChannelDestroy ( int chid ) – Destroy the communication channel being used. The channel ID of the channel you want to destroy. Returns -1 if an error occurs and sets errno. Int MsgReply ( int rcvid, int status, const void* msg, int size ) – Reply to the client. rcvid - The receive id returned from MsgReceive. msg – The buffer you want to send. size – The size of the buffer you are sending. … Message Passing Functions - Server int MsgError ( int rcvid, int error ) – Will unblock the thread which called MsgSend with an error. rcvid – the receive ID from MsgReceive. error – The error you want returned to the client. Errno: https://developer.blackberry.com/playbook/native/reference/com.qnx.doc.neutrino.lib_ref/topic/e/errno.html Message Passing Functions – Client … int ConnectAttach ( uint32_t nd, pid_t pid, int chid, unsigned index, int flags) Create a connection to an already established communication channel. nd – The node descriptor of the Neutrino machine running the server. Use ND_LOCAL_NODE to indicate the current host. pid – The PID of the server who owns the communication channel. chid – The channel ID of the channel created. Index – The starting index to be used for the connection file descriptor. Use _NTO_SIDE_CHANNEL. flags – Flags that control how the connection works. For now just use 0. Returns the connection ID (coid) you will use to communicate with the server. … Message Passing Functions – Client int MsgSend( int coid, const void* smsg, int sbytes, void* rmsg, int rbytes ) – Send a message using a communication channel. coid – The connection ID you received from ConnectAttach. smsg – A pointer to a send buffer containing the message you want to send. sbytes – The size of the send buffer. rmsg – A pointer to a receive buffer which is filled with the server’s reply. rbytes – The size of the receive buffer. Returns -1 if an error occurs and sets errno. int ConnectDetach ( int coid ) – Detach a communication channel from a channel. coid – The connection ID you received from ConnectAttach. Returns -1 if an error occurs and sets errno. Example Lets review the client.c & server.c source files. Designing a Message Interface A more practical approach to message passing is setting up and using a message passing interface. – We will define message types and structures for data in a header. – All messages will contain a header struct and a data struct. – Use a unique struct for each message type. – Use matching reply structs. All QNX messages used internally within the Operating System use uint16_t type as the first argument in their header. This type will be in the range o to _IO_MAX (511). When creating your own types use a value greater then 511 to avoid confusion with system messages. Example: Handling Messages by Type On the server you can handle each message based on the type. while(1) { rcvid = MsgReceive( chid, &msg, sizeof(msg), NULL ); switch( msg.type ) { case MSG_TYPE_1: handle_msg_type_1(rcvid, &msg); break; case MSG_TYPE_2: … } } Final Thoughts on Message Passing This should be enough to finish Lab 5. We will revisit message passing and describe better ways to pass data. Pulses & Timers Pulses – What is a pulse? Asynchronous, non-blocking form of IPC closely related to Message Passing – like MsgSend() is used to send information to another process that will receive it with MsgReceive() – unlike MsgSend(), pulses are non-blocking for the sending process – pulses have a maximum size of 40 bits – pulses are queued by the receiving process if it isn’t blocked at MsgReceive() – the rcvid for MsgReceive is equal to 0 if a pulse is received Pulses – What is a pulse? Pulses contain the following programmer assigned variables – code - a positive integer between 1 and 127 – value - a union with the following members sival_int - an integer *sival_ptr - a void pointer – the programmer can decide how to use these variables Sending a Pulse – MsgSendPulse() #include int MsgSendPulse(int coid,int priority,int code,int value); used to send a pulse through an existing connection to an existing channel of another process parameters identify the connection and handle messages – coid - connection ID though which we are sending this pulse – priority - user assigned priority level for this pulse – code - a value from 1 to 127 – value - a union with the following members sival_int - an integer *sival_ptr - a void pointer note that unlike MsgSend() there is no place for a reply returns – returns negative value if error occurred Receiving a Pulse – MsgReceive() – Receiving a pulse with MsgReceive is done as follows... rcvid = MsgReceive(chid, rmsg, rbytes, info); if(rcvid == 0) { printf(“pulse code=%d\n”,rmsg.pulse.code); printf(“pulse value=“%d\n”,rmsg.pulse.value.sival_int); } else { printf(“regular message received\n”;... } Timers... – Sleep Functions vs. General Purpose Timers The Sleep Functions – block the calling process for a fixed amount of time – specialized use of timers General Purpose Timers – Allow a thread to continue execution while waiting for notification – Notification can be at the end of a: Fixed interval Some actual point in time has arrived... Timers Using Timers - Theory – When setting up a timer there are a few questions to answer When should the timer go off? Should the timer repeat? How should the timer notify us when it goes off? When should the timer go off? A timer is either relative or absolute: – a relative timer expires “x” seconds from now (Current Time) – an absolute timer goes off at a particular time and date – The clock in QNX gives the number of seconds since January 1, 1970 GMT Should the timer repeat? Periodic timers repeat at regular intervals – NOTE: the initial period does not have to equal the repeating period One-shot timers are just as it says time out occurs ONCE Consider these examples of timers we can set up in QNX: – a timer which expires at 12:45:37.5 PM (EST) on April 22, 2003 – a timer which expires 4 minutes from now, and then repeats every 5 seconds after that – a timer which expires at 3:00:00 AM (GMT) on April 14, 2002 and repeats every 10 minutes after that – a timer which expires 5 milliseconds from now. Notifications How should the timer notify us when it goes off? – There are several notification schemes for general purpose timers Send a pulse Send a signal Start a thread Unblock the current thread (as done with sleep functions) Steps to get a timer up and running Create the timer object Set the time on the timer – Choose a notification scheme – Choose if timer will be relative or absolute – Choose if timer will be one-shot or periodic Start the timer Timing With Sleep()... In several examples during this course we’ve used sleep() to cause a process to stop for a fixed number of seconds – e.g. sleep(5) will block a process for 5 seconds sleep could be unblocked prematurely by a signal or interrupt sleep may block for slightly more than 5 seconds, details to follow…... Timing With Sleep () sleep() on a single task machine – sleep() would simple run though a big loop for (i=0; i