Streams. Execution threads


When they talk about processes, they thereby want to note that the operating system maintains their isolation: each process has its own virtual address space, each process is assigned its own resources - files, windows, semaphores, and so on. Such isolation is necessary in order to protect one process from another, since they, sharing all the resources of the VM, compete with each other. In the general case, processes may not be related to each other in any way and may even belong to different users sharing the same computer. In other words, in the case of processes, the OS considers them completely unrelated and independent. In this case, it is the OS that takes on the role of arbiter in competition between processes for resources.

However, it is also desirable to have the ability to use internal parallelism that may exist in the processes themselves. Such internal parallelism occurs quite often, and its use allows you to speed up the implementation of processes. For example, some operations performed by the application may require quite a long time to complete. central processor. In this case, when interactive work with the application, the user is forced to wait a long time for the completion of the ordered operation and cannot control the application until the operation is completed to the very end. Such situations occur quite often, for example, when processing large images in graphic editors. If software modules, performing such lengthy operations, are designed as independent “subprocesses” (the so-called lightweight or lightweight processes), which will be executed in parallel with other similar “subprocesses”, then the user has the opportunity to simultaneously perform several operations within one application (process). Such “subprocesses” are usually called streams or "threads". “Subprocesses” (threads) are called lightweight because the operating system does not have to organize a full-fledged virtual machine for them. Threads do not have their own resources, they develop in the same virtual address space, can use the same files, virtual devices and other resources similar to this process. The only thing they need to have is a processor resource. On a single-processor machine, threads share processor time with each other in the same way as ordinary processes do, but on a multiprocessor machine they can execute simultaneously if they do not encounter competition due to access to other resources.

Multithreading provides the ability to perform several types of operations in parallel in one application program. Parallel computing (and, therefore, more efficient use of CPU resources and lower overall task execution time) is now often implemented at the thread level, and a program designed as several threads within a single process can be executed faster by executing it in parallel. individual parts. In this case, it is desirable to reduce the interaction of threads with each other, since the acceleration from the simultaneous execution of parallel threads can be minimized due to delays in synchronization and data exchange.

An example of the use of multithreading is a spreadsheet or word processor. If they were designed with multi-threading capabilities in mind, then the user can request a recalculation of their worksheet or a merge of multiple documents and simultaneously continue populating a table or open the next document for editing. Multithreading can be used especially effectively to run distributed applications: for example, a multithreaded server can simultaneously execute requests from several clients at once.

So, the “thread” entity was introduced in order to distribute processor time between possible works. The “process” essence suggests that when dispatching, it is necessary to take into account all the resources assigned to it. And when manipulating threads, you can only change their context if there is a switch from one thread to another within the same process. All other computing resources are not affected. Each process always consists of at least from one thread, and only if there is internal parallelism, it becomes possible to “split” this one thread into several parallel ones.

The need for threads arose in single-processor VMs, since threads make it possible to organize calculations more efficiently. To use the advantages of multiprocessor VMs with shared memory, threads are already necessary, since they allow not only to really speed up the execution of those tasks that allow them to be naturally parallelized, but also to load processors, thus eliminating their possible “idleness”.

Each thread runs strictly sequentially and has its own program counter and stack. Threads, like processes, can spawn child threads because every process consists of at least one thread. Like traditional processes (that is, processes consisting of a single thread), each thread can reside in one of active states. While one thread is blocked (or simply in a queue of ready-to-run tasks), another thread in the same process can be running. Threads share processor time in the same way as normal processes do, according to various scheduling options.

All threads have the same virtual address space of their process. This means that they share the same global variables. Because each thread can access every virtual address, one thread can use another thread's stack.

There is no between threads full protection, since this is not necessary. All threads of one process always solve the common task of one user, and the threading mechanism is used here for more quick solution task by parallelizing it. At the same time, it is very important for the programmer to have at his disposal convenient means organizing the interaction of different parts of one program.

Because threads belonging to the same process run in the same virtual address space, it is easy to organize close communication between them (unlike processes that require special communication mechanisms). Moreover, a programmer creating a multi-threaded application can design the operation of multiple process threads in advance so that they can interact in the most beneficial way, rather than compete for resources when this can be avoided.

Summary

The key concepts of operating systems are the concepts of “process” and “resource”. A process is a certain sequence of operations during the execution of a program or part of it in conjunction with the data used. Processes have a number of characteristics and characteristics, the main of which are temporal characteristics, genealogical characteristics, characteristics of effectiveness, time and place of development, connectivity and belonging to the operating system.

A resource is any object consumed or consumed by a process. Resources are classified according to various properties, the most important of which are reality and activity, duration of existence, nature of use, form of implementation.

During their existence, processes can repeatedly change their state, passing through the stages of creation, readiness, execution, waiting and completion of their work. The process management subsystem plans the execution of processes, that is, it distributes processor time between several simultaneously existing processes in the system, and also deals with the creation and destruction of processes, and provides the processes with the necessary system resources, supports communication between processes. One of the process planning methods aimed at efficient resource loading is the resource queuing method. Processes migrate between different queues under control special program operating system– planner. The most commonly used process scheduling algorithms in practice are quantization-based and priority-based algorithms.

The main types of process planning procedures are preemptive and non-preemptive. With a non-preemptive scheduling procedure (non-preemptive multitasking) active process is executed until he himself, on his own initiative, gives control to the operating system scheduler so that it selects another process ready for execution from the queue. With preemptive scheduling (preemptive multitasking), the decision to switch the processor from executing one process to executing another process is made by the operating system scheduler, and not by the active task itself.

One of the main driving forces that change the states of processes are the so-called interrupts, which are a mechanism for forcing the transfer of control from the executing program to the corresponding interrupt handling program. Interrupts are classified into hardware (which can be external or internal) and software. The order in which interrupts are handled depends on the type of interrupt service discipline used.

The order in which processes are interconnected is determined by synchronization rules. The part of the program in which shared data is accessed is called the critical section (critical area) of the program. Situations where two or more processes process shared data, and final result depends on the ratio of process speeds, called races. Elimination of the effect of races in relation to some resource is ensured different ways mutual exclusion, such as the use of blocking variable mechanisms, semaphores, and mutexes.

A significant problem in synchronizing processes is their deadlocks or deadlocks. The problem of deadlocks requires solving the problems of preventing deadlocks, recognizing deadlocks, and restoring the system after deadlocks. A high-level means of synchronizing processes that eliminates deadlocks is the so-called monitor, which is a set of procedures, variables and data structures.

Threads (“threads”) are a “lightweight” (or “lightweight”) form of processes. Threads, unlike processes, do not have their own resources; they develop in the same virtual address space and can use the same files, virtual devices and other resources as a given process. The only thing they need to have is a processor resource. Multithreading provides the ability to perform several types of operations in parallel in one application program. Parallel computing contributes to more efficient use of CPU resources and significantly reduces the total execution time of programs.

Test questions and assignments

1. Define the concepts of “computing process” and “resource”.

2. How are processes and resources classified?

3. Solving what problems involves process planning?

4. List the types of process states.

5. What is context and process descriptor?

6. What are possible reasons exit of the process from the running state?

7. Describe the most common process planning algorithms.

8. What is the essence of process scheduling algorithms based on quantization?

9. Describe the differences between relative and absolute priorities.

10. Define the concepts of preemptive and non-preemptive multitasking.

11. Define the concept of “interruption”.

12. What stages are implemented by the interrupt handling mechanism?

13. Give examples of external interrupts.

14. What events cause internal interrupts?

15. Explain the concept of software interrupt.

16. Describe the applied interrupt servicing disciplines.

17. What is the process synchronization mechanism?

18. What is a “critical area” of a program and for what purposes is it used?

19. By what mechanisms is interaction between processes carried out?

20. What is the difference between the concepts of process queues and process deadlocks?

21. List ways to overcome deadlock situations when processes interact.

22. Describe the concept of a monitor as a high-level means of synchronizing processes.

23. What is the difference between the concepts “process” and “thread”?

24. For what purpose does the OS use the threading mechanism?

Concept of processes and threads. Job, processes, threads, fibers

One of the basic concepts associated with operating systems is process– an abstract concept that describes the operation of a program. Everything that works on a computer software, including the operating system, can be represented as a set of processes.

The task of the OS is to manage the processes and resources of the computer, or, more precisely, to organize the rational use of resources in the interests of the most efficient execution of processes. To solve this problem, the operating system must have information about the current state of each process and resource. A universal approach to providing such information is to create and maintain tables with information for each management object.

A general idea of ​​this can be obtained from Fig. 5.1, which shows the tables maintained by the operating system: for memory, input/output devices, files (programs and data), and processes. Although the details of such tables may vary from OS to OS, they essentially all support information in these four categories. Having the same hardware resources but running different operating systems, a computer can operate with varying degrees of efficiency. The greatest difficulties in managing computer resources arise in multiprogram operating systems.

Rice. 5.1. OS tables

Multiprogramming(multitasking, multitasking) is a way of organizing a computing process in which several programs are alternately executed on one processor. To support multiprogramming, the OS must define internal units of work between which the processor and other computer resources will be divided. In OS batch processing, common in computers of the second and first and third generations, such a unit of work was a task. Currently, most operating systems define two types of units of work: a larger unit called a process or task, and a smaller unit called a task. flow, or a thread. Moreover, the process runs in the form of one or more threads.

At the same time, in some modern operating systems they have again returned to such a unit of work as exercise(Job), for example, in Windows. A job in Windows is a collection of one or more processes that are managed as a single unit. In particular, each task is associated with quotas And limits resources stored in the corresponding job object. Quotas include items such as maximum amount processes (this prevents job processes from creating an uncontrolled number of child processes), the total CPU time available for each process individually and for all processes together, and the maximum amount of memory used for the process and the entire job. Jobs can also restrict their processes for security reasons, such as being granted or denied administrative rights (even if the password is correct).

Processes are considered by the operating system as requests or containers for all types of resources except one - processor time. This most important resource distributed by the operating system between other units of work - threads, which got their name due to the fact that they represent sequences (threads of execution) of commands. Each process starts with a single thread, but new threads can be created (spawned) by the process dynamically. In the simplest case, a process consists of a single thread, and this is how the concept of a “process” was interpreted until the mid-80s (for example, in early versions of UNIX). In some modern operating systems this situation has been preserved, i.e. the concept of "flow" is completely absorbed by the concept of "process".

Typically, a thread runs in user mode, but when it makes a system call, it switches to kernel mode. After finishing system call the thread continues to run in user mode. Each thread has two stacks, one used in kernel mode and one used in user mode. In addition to a state (the current values ​​of all thread objects) identifier and two stacks, each thread has a context (which stores its registers when not running), a private area for its local variables, and may also have its own access token (security information ). When a thread terminates, it may cease to exist. The process terminates when the last active thread ceases to exist.

The relationship between jobs, processes and threads is shown in Fig. 5.2.

Rice. 5.2. Jobs, processes, threads

Switching threads in the OS takes quite a long time, since it requires switching to kernel mode and then returning to user mode. The CPU time spent on scheduling and dispatching threads is quite large. To provide highly lightweight pseudo-parallelism, Windows 2000 (and later versions) use fibers(Fiber), similar to streams, but scheduled in user space by the program that created them. Each thread can have multiple fibers, with the difference that when a fiber is logically blocked, it is placed in a queue of blocked fibers, after which another fiber in the context of the same thread is selected to operate. In this case, the OS “does not know” about the change of fibers, since the same thread continues to work.

Thus, there is a hierarchy of working units of the operating system, which in relation to Windows looks like this (Fig. 5.3).

The question arises: why is such a complex organization of work performed by the operating system necessary? The answer must be sought in the development of the theory and practice of multiprogramming, the goal of which is to ensure maximum effective use main resource computing system– central processor (several central processors).

Therefore, before moving on to consider modern principles control of the processor, processes and threads, you should focus on the basic principles of multiprogramming.

Rice. 5.3. Hierarchy of OS work units

I wrote this document when I was in my third year of university. The thing worked great as a course project [it was also accompanied by a program that demonstrated the capabilities of multi-threaded programming - a small file server [perhaps when I have time, I’ll look for it in the archives and write another article]. So, I completed the third course, and course work stayed. In order not to throw this work into the archives, I decided to present it to you. Therefore, we kindly ask you not to be intimidated by the overly official language - after all, it is a scientific report. Complaints regarding incorrect punctuation are considered last - this is, after all, a scientific report by a physics and technology student:]

Lev Pyakhtin /Lev L. Pyakhtin/, also known as .cens


A little about process architecture
The kernel is a program that is resident and maintains all the tables used to manage the resources and processes of the computer.

In reality, the operating system only manages the process image, or the segments of code and data that define the execution environment, not the process itself. The code segment contains real instructions the central processing unit, which includes both lines written and compiled by the user and system-generated code that allows interaction between the program and the operating system. Data associated with a process is also part of the process image, some of which is stored in registers [registers are areas of memory that can be quickly accessed by the CPU]. To speed up access, registers are stored inside the CPU.

For on-line storage of working data there is a dynamic memory area / a bunch/. This memory is allocated dynamically and its usage varies from process to process. Using the heap, the programmer can provide additional memory to a process.

Automatically, when the program starts, variables are placed on the stack [the stack serves as a storage for temporary storage of variables and return addresses from procedures]. Typically, when running or waiting to run, processes are in random access memory computer. Quite a large part of it is reserved by the operating system kernel, and only the remaining part can be accessed by users. Several processes can be in RAM at the same time. The memory used by the processor is divided into segments called pages /page/. Each page has specific size, which is recorded by the operating system depending on the type of computer. If all pages are in use and there is a need for new page, then the page that is used less than others is placed in swap area /swap area/, and a new one is created in its place. But if the swap area has not been defined, then using special commands you can place the swap area in a file. But there are such pages that should always be in RAM, which are called nonpreemptable pages/. Typically, such pages are used by the kernel or swap programs. main feature in paged memory usage is that a process may be using more memory than it actually has.

Processes can operate in two modes: systemic And custom. Work in system mode means the process executes system calls. It is the most important because it handles interrupts caused by external signals and system calls, as well as disk access control, distribution of additional dynamic memory and other system resources. The process operates in user mode when it executes user-specified code.

For each process, its own control block is created, which is placed in the system table of processes located in the kernel. This table is an array of process control block structures. Each block contains data:

  • process status word
  • a priority
  • the value of the time slice allocated by the system scheduler
  • system processor utilization
  • dispatch sign
  • ID of the user who owns the process
  • effective user ID
  • real and effective group identifiers
  • process group
  • process ID and parent process ID
  • swap image size
  • size of code and data segments
  • an array of signals awaiting processing.
For the system to function properly, the kernel needs to keep track of all this data.
Creating a process
A process is spawned using a system call fork(). This call checks for the presence free memory, available to host a new process. If the required memory is available, a child process of the current process is created, which is an exact copy of the calling process. At the same time, a corresponding structure is built in the process table for the new process. New structure is also created in the user table. In this case, all its variables are initialized to zero. This process is assigned a new unique identifier, and the parent process identifier is stored in the process control block.

You will say: all this is wonderful, but if new process- always a copy of an existing one, then how do different programs work in the system? And where does the very first of them come from?

Processes that run different programs are created by using the "family of functions" found in the Unix standard library. exec": execl, execlp, execle, execv, execve, execvp. These functions differ in calling format, but ultimately do the same thing: replace within the current process executable code to the code contained in specified file. The file may not only be a Linux binary executable, but also a shell script, and binary file different format [for example, java class, DOS executable]

Thus, the operation of launching a program, which in DOS and Windows is performed as a single whole, in Linux [and in Unix in general] is divided into two: first it is launched, and then it is determined which program will run. Does this make sense and is the overhead too high? After all, creating a copy of a process involves copying a very significant amount of information.

Meaning in this approach there definitely is. Very often, a program must perform some actions before its actual execution begins. For example, create an unnamed channel to communicate with other processes. Such pipes are created by the pipe system call, which will be discussed below. This is implemented very simply - first the processes are “spun off”, then the necessary operations/calls are performed pipe()/ and only after that exec is called.

A similar result [as shown, in particular, Windows example NT ] could be achieved by running the program in one step, but more the hard way. As for overhead costs, they most often turn out to be negligible: when creating a copy of a process, its individual data is not physically copied anywhere. Instead, a technique known as copy-on-write /copy on write/: The data pages of both processes are specially marked, and only when one process tries to change the contents of any of its pages is it duplicated.

Ending the process
A system call is used to terminate the process exit(), which frees all used resources, such as memory and kernel table structures. In addition, the child processes spawned by this process also terminate.

Then the code and data segments are removed from memory, and the process itself enters the state zombie[ in field Stat such processes are marked with the letter " Z". The zombie does not take up any CPU time, but the row in the process table remains and the corresponding kernel structures are not freed. After the parent process exits, the "orphaned" zombie briefly becomes a child init, after which it “finally dies”]. Finally, the parent process must clean up all resources occupied by child processes.

If the parent process terminates before the child for some reason, the latter becomes "orphan" /orphaned process/. Such “Orphans” are also automatically “adopted” by the program init, running in process number 1, which receives a signal about their completion.

Also, the process may fall into a “sleep” that cannot be interrupted: in the field Stat this is indicated by the letter " D". A process in this state does not respond to system requests and can only be destroyed by rebooting the system.

Process interaction
The most common means of interaction between processes is sockets /sockets/. Programs connect to the socket and issue a request to bind to to the right address. Data is then transferred from one socket to another according to the specified address.

A signal informs another process that certain conditions have occurred within the current process that require a response from the current process. Many signal processing programs print a memory dump to analyze the problem.

Channels are implemented in two classes. The first one is created using a system call pipe(). In this case, a special structure in the kernel is initialized to exchange information between processes. The calling process has two file handles, one for reading and one for writing information. Then, when a process spawns a new process, a communication channel is opened between the two processes. Another type of channels are named pipes. When used with management structure The kernel associates a special directory through which two autonomous processes can exchange data. In this case, each process must open a channel in the form regular files[one is for reading, the other is for writing]. I/O operations then proceed as usual.

Message Queue is a mechanism where one process provides a block of data with flags set, and another process searches for a block of data whose flags are set to the required values.

Semaphores represent a means of passing flags from one process to another. By "raising" a semaphore, a process can signal that it is in a certain state. Any other process on the system can find this flag and perform the necessary actions.

Shared Memory allows processes to access the same area of ​​physical memory.

Threads
What is a thread?
Just like a multitasking operating system can do several things at once using different processes, one process can do many things using multiple threads. Each thread is an independently executing thread with its own program counter, register context, and stack. The concepts of process and thread are very closely related and therefore difficult to distinguish; threads are even often called lightweight processes. The main differences between a process and a thread are that each process has its own independent memory area, a table open files, current directory and other kernel level information. Threads are not directly connected to these entities. All threads belonging to this process everything listed above is common because it belongs to this process. Additionally, a process is always a kernel-level entity, meaning the kernel knows of its existence, while a thread is often a user-level entity and the kernel may not know anything about it. In such implementations, all thread data is stored in the user memory area, and accordingly, procedures such as spawning or switching between threads do not require access to the kernel and take an order of magnitude less time.
Thread creation and POSIX API ideology
With the low-level approach to supporting threads in the language that we chose for study, all operations associated with them are expressed explicitly through function calls. Accordingly, now that we have received general idea about what a thread is, it’s time to consider the question of how we can create threads and manage them in our programs. Let me remind you that we are talking about programs in the C language and a thread support interface corresponding to the standard POSIX. According to it, a thread is created using the following call:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void* (*start)(void *), void *arg)

Simplified call pthread_create[&thr,NULL,start,NULL] will create a thread that will begin executing the start function and write the identifier of the created thread to the thr variable. Using this call as an example, we'll take a closer look at several supporting POSIX API concepts so that we won't go into them any further.

The first argument to this thread function is a pointer to a variable of type pthread_t, which will store the identifier of the created thread, which can later be passed to other calls when we want to do something with this thread. Here we encounter the first feature of the POSIX API, namely the opacity of base types. The fact is that we can say practically nothing about the pthread_t type. We don't know if this is an integer or a pointer? We cannot say whether there is order between values ​​of this type, that is, whether it is possible to build a non-decreasing chain from them. The only thing the standard says is that these values ​​can be copied, and that using the int call pthread_equal we can establish that both identifiers thr1 and thr2 identify the same thread [they may well be unequal in the sense of the equality operator]. Most types used in this standard have similar properties; moreover, as a rule, the values ​​of these types cannot even be copied!

The second argument of this attr function is a pointer to a variable of type pthread_attr_t, which specifies a set of some properties of the thread being created. Here we encounter the second feature of the POSIX API, namely the concept of attributes. The fact is that in this API, in all cases when, when creating or initializing an object, it is necessary to specify a set of some additional properties, instead of specifying this set using a set of call parameters, passing a pre-constructed object representing this set of attributes is used. This solution has at least two advantages. Firstly, we can fix a set of function parameters without the threat of changing it in the future when new properties appear on this object. Second, we can reuse the same set of attributes to create multiple objects.

The third argument to the call to pthread_create is a pointer to a function of type void*(). It is this function that the newly created thread begins to perform, and the fourth argument to the pthread_create call is passed as a parameter to this function. Thus, on the one hand, it is possible to parameterize the created thread with the code that it will execute, and on the other hand, to parameterize it with various data transmitted to the code.

The pthread_create function returns zero on success and a non-zero error code on failure. This is also one of the features of the POSIX API, instead of the standard Unix approach of a function returning only some error indicator and setting the error code in the errno variable, Pthreads API functions return an error code as a result of their argument. Obviously, this is due to the fact that with the appearance in the program of several threads calling various functions that return the error code to the same global variable errno, complete confusion ensues, namely, there is no guarantee that the error code that is now in this variable is the result of calling what happened in this and not another thread. And although, due to the huge number of functions that already use errno, the thread library provides an instance of errno for each thread, which in principle could be used in the thread library itself, the creators of the standard chose a more correct and, most importantly, faster approach in which API functions they just return error codes.

Completion of the thread, features of the main thread
The thread ends when the start function returns. Moreover, if we want to get the return value of a function, then we must use the function:

int pthread_join(pthread_t thread, void** value_ptr)

This function waits for the thread with identifier thread to complete, and writes its return value to the variable pointed to by value_ptr. This releases all resources associated with the thread, and therefore this function can only be called once for a given thread. In fact, it is clear that many resources, such as the stack and thread-specific data, may already be freed when the thread function returns, and to be able to execute the pthread_join function, it is sufficient to store the thread identifier and the return value. However, the standard only says that the resources associated with the thread will be released after calling the pthread_join function.

If we are not satisfied with something about returning a value via pthread_join , for example, we need to receive data in several threads, then we should use some other mechanism, for example, we can organize a queue of returned values, or return a value in a structure the pointer to which is passed to as a thread parameter. That is, the use of pthread_join is a matter of convenience, and not a dogma, unlike the case of the fork() - wait() pair. The point here is that if we want to use a different return mechanism or we are simply not interested in the returned value, then we can disconnect [ detach] thread, thereby saying that we want to release the resources associated with the thread immediately upon completion of the thread function. There are several ways to do this. First, you can immediately create a thread detached by setting the appropriate attribute object when calling pthread_create . Secondly, any thread can be detached by calling the function at any time in its life [that is, before calling pthread_join()]


int pthread_detach(pthread_t thread)

And specifying the thread identifier as a parameter. In this case, the thread may well disconnect itself after receiving its identifier using the pthread_t pthread_self function. It should be emphasized that detaching a thread does not in any way affect its execution, but simply marks the thread as ready to release resources upon completion. In fact, the same pthread_join just receives the return value and detaches the thread.

Let me note that by freed resources we mean, first of all, the stack, the memory in which the thread context, thread-specific data, and the like are stored. This does not include resources allocated explicitly, such as memory allocated via malloc, or files being opened. Such resources must be released explicitly and the responsibility for this lies with the programmer.

In addition to returning from a thread function, there is another way to terminate it, namely by calling something similar to calling exit() for processes:


int pthread_exit(void *value_ptr)

This call terminates the running thread, returning value_ptr as its execution result. In reality, when this function is called, the thread simply does not return from it. You should also pay attention to the fact that the exit() function still terminates the process, that is, it also destroys all threads.

As you know, a C program begins with the execution of the main() function. The thread in which this function is executed is called the main or initial thread [since it is the first thread in the application]. On the one hand, this thread has many of the properties of a regular thread, you can get an identifier for it, it can be detached, and you can call pthread_join for it from some other thread. On the other hand, it has some features that distinguish it from other threads. First, returning from this thread terminates the entire process, which is sometimes convenient since you don't have to explicitly worry about terminating other threads. If we do not want the remaining threads to be destroyed when this thread exits, then we should use the pthread_exit function. Secondly, the function of this thread does not have one parameter of type void* like the others, but a pair of argc-argv . Strictly speaking, the main function is not a thread function, since in most operating systems, it is itself called by some functions that prepare its execution, automatically generated by the compiler. Third, many implementations allocate much more memory to the stack of the initial thread than to the stacks of other threads. Obviously, this is due to the fact that there are already many single-threaded applications [that is, traditional applications] that require a significant amount of stack space, and the author of a new multi-threaded application can be required to have a limited appetite.

Life cycle of a thread
Let's now consider life cycle threads, namely the sequence of states in which the thread remains during its existence. In general, four such conditions can be distinguished:
Thread conditionWhat does it mean
ReadyThe thread is ready to run but is waiting for the processor. It may have just been created, was evicted from the processor by another thread, or has just been unblocked [out of the appropriate state].
/Running/The thread is currently running. It should be noted that on a multiprocessor machine there may be several threads in this state.
BlockedThe thread cannot execute because it is waiting for something. For example, the end of an I/O operation, a signal from a condition variable, receiving a mutex, etc.
TerminatedThe thread was terminated, for example due to a return from a thread function, a call to pthread_exit, or a thread interruption /cancellation/. The thread has not yet been detached and the pthread_join function has not been called on it. As soon as one of these events occurs, the thread ceases to exist.

Various particular implementations can introduce additional states to these four, but all of them will essentially be just substates of these. In general, the transition diagram between these states can be depicted as follows:

Threads can be created by the system, such as the seed thread that is created when a process is created, or can be created through explicit calls to pthread_create() by the user process. However, any thread created begins its life in the "ready" state. After which, depending on the system's scheduling policy, it can either immediately enter the "running" state or enter it after some time. Here it is necessary to pay attention to a typical mistake made by many, which is that in the absence of explicit measures to synchronize the old and new threads, it is assumed that after the pthread_create function returns, the new thread will exist. However, this is not the case, because given a certain scheduling policy and thread attributes, it may well happen that the new thread will already have time to execute by the time this function returns.

A running thread will most likely, sooner or later, either go into the "blocked" state, causing an operation to wait for something, such as the end of I/O, the arrival of a signal, or raising a semaphore, or go into the "ready" state after being removed from the processor or more a high-priority thread or simply because it has exhausted its time slice. Here it is necessary to emphasize the difference between preemption/ that is, removal from the processor due to the appearance of a ready, higher-priority task, and removal of the thread due to the expiration of its time slice. The fact is that typical mistake assume that the first implies the second. There are scheduling policies that simply do not support the concept of a time slice. This is, for example, the default scheduling policy for threads in the Solaris OS. This is one of the standard [in the POSIX sense] real-time scheduling policies SCHED_FIFO.

A blocked thread, having waited for the event for which it was waiting, goes into the “ready” state; of course, if there is such an opportunity, it will immediately go into the running state.

Finally, the running thread may terminate in one way or another. For example, as a result of returning from a thread function, calling the pthread_exit function, or forcefully interrupting its execution by calling pthread_cancel . At the same time, if a thread was detached, then it immediately releases all resources associated with it and ceases to exist / In fact, it will most likely simply be reused by the thread support library, since creating a thread is not the cheapest operation /. If the thread has not been detached, it may release some resources, after which it will go into the “completed” state, in which it will remain until it is detached either using pthread_detach or pthread_join . After which it will again release all resources and cease to exist.

Keywords: processes, processes in Unix, processes in Unix, processes in Linux, processes in Linux, pthread_create, pthread_mutex, cl1mp3x, Threads, Threads, Sockets, Queue, Mutexes, Semaphores, Multitasking

All documents and programs on this site are collected ONLY for educational purposes; we are not responsible for any consequences that occurred as a result of the use of these materials/programs. You use all of the above at your own risk.

Any materials from this site cannot be copied without permission from the author or administration.

One of the basic concepts associated with operating systems is process– an abstract concept that describes the operation of a program. All software running on a computer, including the operating system, can be represented as a set of processes.

The task of the OS is to manage the processes and resources of the computer, or, more precisely, to organize the rational use of resources in the interests of the most efficient execution of processes. To solve this problem, the operating system must have information about the current state of each process and resource. A universal approach to providing such information is to create and maintain tables with information for each management object.

A general idea of ​​this can be obtained from Fig. 5.1, which shows the tables maintained by the operating system: for memory, input/output devices, files (programs and data), and processes. Although the details of such tables may vary from OS to OS, they essentially all support information in these four categories. Having the same hardware resources but running different operating systems, a computer can operate with varying degrees of efficiency. The greatest difficulties in managing computer resources arise in multiprogram operating systems.

Rice. 5.1. OS tables

Multiprogramming(multitasking, multitasking) is a way of organizing a computing process in which several programs are alternately executed on one processor. To support multiprogramming, the OS must define internal units of work between which the processor and other computer resources will be divided. In the batch processing OS, common in computers of the second and first and third generations, such a unit of work was a task. Currently, most operating systems define two types of units of work: a larger unit called a process or task, and a smaller unit called a task. flow, or a thread. Moreover, the process runs in the form of one or more threads.

At the same time, in some modern operating systems they have again returned to such a unit of work as exercise(Job), for example, in Windows. A job in Windows is a collection of one or more processes that are managed as a single unit. In particular, each task is associated with quotas And limits resources stored in the corresponding job object. Quotas include items such as the maximum number of processes (this prevents job processes from creating an uncontrolled number of child processes), the total CPU time available for each process individually and for all processes together, and the maximum amount of memory used for the process and the entire job . Jobs can also restrict their processes for security reasons, such as being granted or denied administrative rights (even if the password is correct).

Processes are considered by the operating system as requests or containers for all types of resources except one - processor time. This most important resource is distributed by the operating system between other units of work - threads, which got their name due to the fact that they represent sequences (threads of execution) of commands. Each process starts with a single thread, but new threads can be created (spawned) by the process dynamically. In the simplest case, a process consists of a single thread, and this is how the concept of a “process” was interpreted until the mid-80s (for example, in early versions of UNIX). In some modern operating systems this situation has been preserved, i.e. the concept of "flow" is completely absorbed by the concept of "process".

Typically, a thread runs in user mode, but when it makes a system call, it switches to kernel mode. After the system call completes, the thread continues to execute in user mode. Each thread has two stacks, one used in kernel mode and one used in user mode. In addition to a state (the current values ​​of all thread objects) identifier and two stacks, each thread has a context (which stores its registers when not running), a private area for its local variables, and may also have its own access token (security information ). When a thread terminates, it may cease to exist. The process terminates when the last active thread ceases to exist.

The relationship between jobs, processes and threads is shown in Fig. 5.2.

Rice. 5.2. Jobs, processes, threads

Switching threads in the OS takes quite a long time, since it requires switching to kernel mode and then returning to user mode. The CPU time spent on scheduling and dispatching threads is quite large. To provide highly lightweight pseudo-parallelism, Windows 2000 (and later versions) use fibers(Fiber), similar to streams, but scheduled in user space by the program that created them. Each thread can have multiple fibers, with the difference that when a fiber is logically blocked, it is placed in a queue of blocked fibers, after which another fiber in the context of the same thread is selected to operate. In this case, the OS “does not know” about the change of fibers, since the same thread continues to work.

Thus, there is a hierarchy of working units of the operating system, which in relation to Windows looks like this (Fig. 5.3).

The question arises: why is such a complex organization of work performed by the operating system necessary? The answer must be sought in the development of the theory and practice of multiprogramming, the purpose of which is to ensure the most efficient use of the main resource of a computer system - the central processor (several central processors).

Therefore, before moving on to consider modern principles of managing the processor, processes and threads, we should dwell on the basic principles of multiprogramming.

Rice. 5.3. Hierarchy of OS work units

.
In order to structure your understanding of what threads are (this word is translated into Russian as “threads” almost everywhere, except for books on the Win32 API, where it is translated as “threads”) and how they differ from processes, you can use the following two definitions:

  • Thread is virtual processor, which has its own set of registers similar to those of a real central processor. One of the most important registers for a virtual processor, as well as for a real one, is an individual pointer to current instructions(for example, the individual EIP register on x86 family processors),
  • Process comes first address space. In modern architecture, created by the OS kernel through manipulation of page tables. And secondly, the process should be looked at as a point of binding “resources” in the OS. If we analyze an aspect such as multitasking in order to understand the essence of threads, then at this moment we do not need to think about OS “resources” such as files and what they are tied to.
It is very important to understand that thread is conceptually a virtual processor, and when we write an implementation of threads in the OS kernel or in a user-level library, we solve precisely the problem of “reproduction” of the central processor in many virtual instances that are logically or even physically (on SMP, SMT and multi-core CPU platforms) work in parallel with each other.
At a basic, conceptual level, there is no “context.” Context is simply the name of the data structure into which the OS kernel or our library (implementing threads) saves registers virtual processor, when it switches between them, emulating their parallel operation. Context switching is way to implement threads, rather than the more fundamental concept through which thread should be defined.
When approaching the definition of the concept of thread through the analysis of APIs of specific operating systems, too many entities are usually introduced - here you have processes, and address spaces, and contexts, and switching these contexts, and timer interrupts, and time slices with priorities, and even “resources” , bound to processes (as opposed to threads). And all this is woven into one ball and often we see that we are going in circles while reading the definitions. Unfortunately, this is a common way to explain the essence of threads in books, but this approach greatly confuses novice programmers and ties their understanding to the specifics of the implementation.
It is clear that all these terms have a right to exist and did not arise by chance; behind each of them there is some important essence. But among them it is necessary to highlight major and minor(entered for implementation main entities or hung on top of them, already at the next levels of abstraction).
The main idea of ​​thread is virtualization CPU registers – emulation on one physical processor of several logical processors, each of which has its own register state (including the instruction pointer) and operates in parallel with the others.
The main property of the process in the context of this conversation is that it has its own page tables that form its individual address space. A process is not something executable in itself.
You can say in the definition that “every process on the system always has at least one thread.” Or you can say it differently - address space logically makes no sense to the user, if it is not visible to at least one virtual processor (thread). Therefore, it is logical that all modern operating systems destroy the address space (terminate the process) when the last thread running in a given address space terminates. And you don’t have to say in the process definition that it has “at least one thread.” Moreover, on the lower system level a process (as a rule) can exist as an OS object even without threads.
If you look at the sources, for example, Windows kernels, then you will see that the address space and other structures of the process are constructed before the initial thread (initial thread for this process) is created in it. In fact, initially there are no threads at all in the process. On Windows, you can even create a thread in someone else's address space via the user-level API...
If you look at thread as a virtual processor, then its binding to the address space represents loading of the page table database into a virtual register desired value. :) Moreover, at the lower level this is exactly what happens - every time you switch to a thread associated with another process, the OS kernel reloads the register of the pointer to page tables (on those processors that do not support working with many spaces simultaneously in hardware ).






2024 gtavrl.ru.