Linux thread Analysis
In many classic operating system textbooks, a process is always defined as an execution instance of a program. It does not execute anything, but only maintains various resources required by the application. the thread is the real execution entity. in order for a process to complete some work, the process must contain at least one thread.
The Process maintains resources (static resources) in the program, such as address space, opened file handle set, file system status, and handler for signal processing; resources (Dynamic Resources) related to running maintained by threads, such as running stacks, control information related to scheduling, and signal sets to be processed;
However, Linux kernel has never been a thread. every execution entity is a task_struct structure, usually called a process. A process is an execution unit that maintains execution-related dynamic resources. at the same time, it references the static resources required by the Program (note that processes in linux are described here ).When the system calls clone to create a child process, the child process can selectively share the resources referenced by the parent process. Such a child process is usually called a lightweight process.
Linux threads are implemented by the user-state pthread Library Based on lightweight processes. after pthread is used, in the user's opinion, each task_struct corresponds to a thread, and a group of threads and a group of resources referenced by them are a process.
However, a group of threads does not just reference the same group of resources, but must also be considered as a whole. POSIX standards have the following requirements:
- (1) when viewing the Process List, a related set of task_struct should be displayed as a node in the list;
- (2) signals sent to this "process" (corresponding to the kill System Call) will be shared by the corresponding task_struct group and processed by any of the "Threads;
- (3) signals sent to a "Thread" (corresponding to pthread_kill) will be received by only the corresponding task_struct and processed by itself;
- (4) When the "process" is stopped or continues (corresponding to the SIGSTOP/SIGCONT signal), the corresponding task_struct status will change;
- (5) When a "process" receives a fatal signal (for example, a SIGSEGV signal is received due to a segment error), all corresponding task_struct tasks will exit;
- (6) and so on (the above may be incomplete );
1. Linuxthreads
Before linux 2.6, the implementation of the pthread library was a lib named linuxthreads. linuxthreads uses the lightweight process mentioned above to implement threads, but for those requirements of POSIX, linuxthreads is not implemented except for 5th points (in fact, it is powerless ):
- (1) If program A runs and program A creates 10 threads, 11 A processes are displayed when the ps command is executed in shell instead of 1 (note, not 10, which will be explained below );
- (2) Whether it is kill or pthread_kill, the signal can only be received by a corresponding thread;
- (3) The SIGSTOP/SIGCONT signal only works for one thread.
Fortunately, linuxthreads has implemented 5th points, which I think is the most important. if a thread is "disconnected" and the entire process is still running, many inconsistencies may occur. the process will not be a whole, and the thread cannot be called a thread. maybe this is why linuxthreads has been used for several years even though it is far from POSIX ~. However, linuxthreads has paid a lot to achieve this "5th" and created a major performance bottleneck for linuxthreads.
Next, let's talk about it,Why is there 11 A processes in ps when program A creates 10 threads?. Because linuxthreads automatically creates a management thread. the "5th points" mentioned above is implemented by the management thread. when the program starts to run, there is no management thread (because although the program has been linked to the pthread library, multithreading may not be used ). when the program calls pthread_create for the first time, linuxthreads finds that the management thread does not exist and creates this management thread. this management thread is the son of the first thread (main thread) in the process. then, in pthread_create, a command is sent to the management thread through pipe, telling it to create a thread.That is to say, all threads except the main thread are created by the management thread, and the Management thread is their father.. When any sub-thread exits, the management thread receives the SIGUSER1 signal (this is specified when the sub-thread is created through clone ). in the corresponding sig_handler, the management thread will determine whether the Sub-thread Exits normally. If not, it will kill all the threads and commit suicide. so what about the main thread? The main thread is the father of the Management thread and does not send signals to the management thread when it exits. therefore, the ID of the parent process is checked through getppid in the main loop of the Management thread. If the ID number is 1, the parent has exited, and managed to the init process (process 1 ). at this time, the management thread will also kill all sub-threads and commit suicide. so what if the main thread actively exits by calling pthread_exit? According to posix standards, in this case, other sub-threads should continue to run. therefore, in linuxthreads, the main thread does not actually exit after calling pthread_exit. Instead, it will block in the pthread_exit function and wait until all child threads exit. pthread_exit will cause the main thread to exit. (In this process, the main thread remains in sleep state .)
It can be seen that the thread creation and destruction are completed through the Management thread, so the management thread becomes a performance bottleneck of linuxthreads.The creation and destruction require a communication between processes. After a context switch is performed, the management thread can execute multiple requests, and the Management thread will execute them serially.
2. NPTL
In linux 2.6, glibc has a new pthread Library-NPTL (Native POSIX Threading Library ). NPTL implements all the POSIX 5 requirements mentioned above. however, in fact, it is not so much the NPTL implementation as the Linux kernel implementation.
In linux 2.6, the kernel has the thread group concept. A tgid (thread group id) field is added to the task_struct structure. if the task is a "main thread", its tgid is equal to the pid; otherwise, the tgid is equal to the pid of the process (that is, the pid of the main thread ). in the clone system call, pass the CLONE_THREAD parameter to set the tgid of the new process to the tgid of the parent process (otherwise, the tgid of the new process is set to its own pid ).
There are two similar xxids in task_struct: task> signal> pgid: The pid of the headers in the process group, task> signal> session: the pid of the session headers. These two IDs are used to associate the process group and session.
With tgid, the kernel or related shell program will know whether a tast_struct represents a process or a thread, and when to display them, when should it not be displayed (for example, the thread should not be displayed during ps ).
The getpid (obtain process ID) system call also returns the tgid in tast_struct, while the pid in tast_struct is returned by the gettid system call.
There are also some problems when executing ps commands without displaying sub-threads. For example, when program a. out is running, a thread is created. Assume that the pid of the main thread is 10001, and the sub-thread is 10002. Their tgids are both 10001 ). At this time, if you kill 10002, you can kill the two threads 10001 and 10002 together, even though the process 10002 is invisible when executing the ps command. If you do not know the story behind the linux thread, you will surely feel a strange event.
We can perform the following verification, with the program segment test. cpp as follows:
- # Include <unistd. h>
- # Include <stdlib. h>
- # Include <stdio. h>
- # Include <pthread. h>
- Using namespace std;
- Void * doit (void *);
- Int main (void)
- {
- Pthread_t tid;
- Pthread_create (& tid, NULL, doit, NULL );
- Pause (); // The main thread is suspended. Otherwise, the main thread is terminated and the sub-thread is suspended)
- }
- Void * doit (void * agr)
- {
- Printf ("thread is created! \ N ");
- Pause (); // suspends a thread
- }
This program creates a thread and suspends. The sub-thread outputs "thread is created !" Also suspended. Result 1.
Figure 1
Check the process pid and find that the pid of a. out is 23130. We use kill to terminate the process with pid 23131. Note that this process is not found in ps.), 2.
Figure 2
However, the. out process is terminated. 3. The cause is that 23131 is the pid of the created thread. If the thread terminates abnormally, the process is terminated.
Figure 3
To cope with "signals sent to processes" and "signals sent to threads", two sets of signal_pending are maintained in task_struct. One set is shared by thread groups and the other is unique to threads. the signal sent by kill is placed in the signal_pending shared by the thread group and can be processed by any thread. The signal sent by pthread_kill is the interface of the pthread library, corresponding System Call tkill) is placed in the unique signal_pending of the thread, which can only be processed by the current thread. when the thread stops/continues, or receives a fatal signal, the kernel will apply the processing action to the entire thread group.
3. NGPT
Here, we also mention NGPT (Next Generation POSIX Threads ). the two thread libraries mentioned above use kernel-level threads (each thread corresponds to a scheduling entity in the kernel ), this model is called the 1:1 model (one thread corresponds to one kernel-level thread), while NGPT intends to implement the M: N model (M threads correspond to N kernel-level threads ), that is to say, several threads may be implemented on the same execution entity.
The thread library abstracts several execution entities on the execution entities provided by a kernel and schedules them. the abstract execution entity is called a user-level thread. in general, this can be done by allocating a stack to each user-level thread and then performing context switching through longjmp. (Baidu click "setjmp/longjmp", you will know .) but in fact, there are a lot of details to deal. at present, NGPT does not seem to implement all the expected functions, and is not yet ready to be implemented.
User-level thread switching is obviously faster than kernel-level thread switching. The former may be a simple long jump, while the latter needs to save/load registers, enter and exit the kernel state. (For process switching, you also need to switch the address space .) user-level threads cannot enjoy multi-processor, because multiple user-level threads correspond to one kernel-level thread, and one kernel-level thread can only run on one processor at the same time.
However, the M: N thread model provides such a means to run threads that do not require parallel execution on several user-level threads corresponding to a kernel-level thread, it can save their switching overhead. it is said that some UNIX-like systems (such as Solaris) have already implemented a mature M: N thread model, and its performance has some advantages over linux threads.