Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to analyze multithreaded programming in Linux system

2025-02-28 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/02 Report--

The content of this article mainly focuses on how to analyze the multithreaded programming of Linux system. The content of the article is clear and clear. It is very suitable for beginners to learn and is worth reading. Interested friends can follow the editor to read together. I hope you can get something through this article!

Multithread programming under Linux

1 introduction

Thread technology was proposed as early as the 1960s, but the real application of multithreading to the operating system was in the mid-1980s, and solaris is a leader in this area. Traditional Unix also supports the concept of threads, but only one thread is allowed in a process, so multithreading means multiple processes. Today, multithreading technology is supported by many operating systems, including Windows/NT and, of course, Linux.

Why introduce threads after having the concept of process? What are the benefits of using multithreading? What kind of system should choose multithreading? We must first answer these questions.

One of the reasons for using multithreading is that it is a very "frugal" multitasking operation compared to processes. We know that under the Linux system, starting a new process must be assigned to its independent address space, and establish a large number of data tables to maintain its code segment, stack segment and data segment, which is an "expensive" multitasking way. While multiple threads running in a process use the same address space with each other and share most of the data, the space it takes to start a thread is much less than the space it takes to start a process, and the time it takes for threads to switch from one to another is much less than the time it takes to switch between processes. According to statistics, generally speaking,  Huan Huan  phlegm  and Huan Chi charcoal ? 0 times, of course, in the specific system, this data may be quite different.

The second reason for using multithreading is the convenient communication mechanism between threads. For different processes, they have independent data space, and data transmission can only be carried out through communication, which is not only time-consuming, but also very inconvenient. Threads are not, because the data space is shared between threads in the same process, so the data of one thread can be directly used by other threads, which is not only fast, but also convenient. Of course, the sharing of data also brings some other problems, some variables cannot be modified by two threads at the same time, and the data declared as static in some subroutines are more likely to bring a catastrophic blow to multithreaded programs, which are the most important things to pay attention to when writing multithreaded programs.

In addition to the advantages mentioned above, without comparing with processes, multithreaded programs, as a multitasking and concurrent way of working, certainly have the following advantages:

1) improve the response of the application. This is particularly meaningful for graphical interface programs, when an operation takes a long time, the whole system will wait for this operation, when the program will not respond to keyboard, mouse, menu operations, but the use of multi-threading technology, time-consuming operations (time consuming) placed in a new thread, can avoid this awkward situation.

2) make the multi-CPU system more effective. The operating system ensures that when the number of threads is not greater than the number of CPU, different threads run on different CPU.

3) improve the program structure. A long and complex process can be divided into several threads and become several independent or semi-independent running parts. such a program will be easy to understand and modify.

Let's first try to write a simple multithreaded program.

2 simple multithreaded programming

Multithreading in Linux system follows the POSIX thread interface, which is called pthread. To write a multithreaded program under Linux, you need to use the header file pthread.h and the library libpthread.a when connecting. By the way, the implementation of pthread under Linux is achieved through the system call clone (). Clone () is a system call unique to Linux and is used in a similar way to fork. For more information about clone (), you can check the documentation. Let's show one of the simplest multithreaded programs, example1.c.

/ * example.c*/ # include # include void thread (void) {int i; for (I = 0; I printf ("This is a pthread.\ n");} int main (void) {pthread_t id; int i, ret; ret = pthread_create (& id, NULL, (void *) thread, NULL); if (ret! = 0) {printf ("Create pthread error!\ n") Exit (1);} for (I = 0; I printf ("This is the main process.\ n"); pthread_join (id, NULL); return (0);} 12345678910111213141516171819202122

We compile this program:

Gcc example1.c-lpthread-o example1

Running example1, we get the following result:

This is the main process.

This is a pthread.

This is the main process.

This is the main process.

This is a pthread.

This is a pthread.

Run it again, and we might get the following result:

This is a pthread.

This is the main process.

This is a pthread.

This is the main process.

This is a pthread.

This is the main process.

The two results are different, which is the result of two threads competing for CPU resources. In the above example, we use two functions, pthread_create and pthread_join, and declare a variable of type pthread_ t.

Pthread_t is defined in the header file / usr/include/bits/pthreadtypes.h:

Typedef unsigned long int pthread_t

It is the identifier of a thread. The function pthread_create is used to create a thread whose prototype is:

Extern int pthread_create _ P ((pthread_t * _ thread, _ _ const pthread_attr_t * _ _ attr)

Void (_ _ start_routine) (void *), void * _ arg))

The first parameter is the pointer to the thread identifier, the second parameter is used to set the thread properties, the third parameter is the starting address of the thread running function, and the last parameter is the parameter of the running function. Here, our function thread takes no arguments, so the last parameter is set to a null pointer. We also set the second parameter to a null pointer, which will generate the thread of the default property. We will discuss the setting and modification of thread properties in the next section. When the thread is created successfully, the function returns 0. If it is not 0, the thread creation failed. The common error return codes are EAGAIN and EINVAL. The former indicates that the system restricts the creation of new threads, such as too many threads; the latter indicates that the value of the thread property represented by the second parameter is illegal. After the thread is created successfully, the newly created thread runs the function determined by parameters 3 and 4, and the original thread continues to run the next line of code.

The function pthread_join is used to wait for the end of a thread. The function prototype is:

Extern int pthread_join _ P ((pthread_t _ th, void * * _ thread_return))

The first parameter is the identifier of the waiting thread, and the second parameter is a user-defined pointer that can be used to store the return value of the waiting thread. This function is a thread blocking function, and the function that calls it will wait until the end of the waiting thread, and when the function returns, the resources of the waiting thread will be reclaimed. There are two ways to end a thread, one is, as in our example above, the function ends, so does the thread that calls it, and the other is through the function pthread_exit. Its function prototype is:

Extern void pthread_exit _ P ((void * _ retval)) attribute ((noreturn))

The only argument is the function's return code, which is passed to thread_return as long as the second argument in pthread_join, thread_return, is not NULL. Finally, a thread cannot be waited by multiple threads, otherwise the first thread that receives the signal returns successfully, and the rest of the threads that call pthread_join return the error code ESRCH.

In this section, we wrote the simplest thread and mastered the three most commonly used functions, pthread_create,pthread_join and pthread_exit. Next, let's take a look at some common properties of threads and how to set them.

3 modify the properties of a thread

In the example in the previous section, we created a thread with the pthread_create function, in which we used the default parameter, setting the second parameter of the function to NULL. It is true that for most programs, using default properties is sufficient, but it is still necessary to understand the properties of threads.

The attribute structure is pthread_attr_t, which is also defined in the header file / usr/include/pthread.h, and those who like to get to the bottom of the matter can check it out for themselves. The property value cannot be set directly, but must be operated with the relevant function. The initialized function is pthread_attr_init, which must be called before the pthread_create function. Property objects mainly include whether to bind, whether to detach, stack address, stack size, priority. The default properties are unbound, non-detached, default 1m stack, and the same level of priority as the parent process.

With regard to thread binding, another concept is involved: LWP:Light Weight Process. A light process can be understood as a kernel thread, which lies between the user layer and the system layer. The allocation of thread resources and the control of threads are realized by light process, and a light process can control one or more threads. By default, how many light processes are started and which light processes control which threads are controlled by the system, which is called unbound. In the case of binding, as the name implies, a thread is fixed "tied" to a light process. The bound thread has a high response speed, because the scheduling of CPU time slices is light process-oriented, and the bound thread ensures that it always has a light process available when needed. By setting the priority and scheduling level of the bound light process, the bound thread can meet requirements such as real-time response.

The function that sets the thread binding state is pthread_attr_setscope, which takes two parameters, the first is a pointer to the property structure, and the second is the binding type, which has two values: PTHREAD_SCOPE_SYSTEM (bound) and PTHREAD_SCOPE_PROCESS (unbound). The following code creates a bound thread.

# include pthread_attr_t attr;pthread_t tid;/* initialization attribute value, all set to the default value * / pthread_attr_init (& attr); pthread_attr_setscope (& attr, PTHREAD_SCOPE_SYSTEM); pthread_create (& tid, & attr, (void *) my_function, NULL); 123456789

The detached state of a thread determines how a thread terminates itself. In the above example, we adopted the default property of the thread, which is a non-detached state, in which case the original thread waits for the created thread to finish. It is only when the pthread_join () function returns that the created thread is considered terminated and the system resources it occupies can be released. The separation thread is not like this, it is not waiting by other threads, its own run ends, the thread terminates, and immediately releases system resources. Programmers should choose the appropriate separation state according to their own needs. The function to set the thread separation state is pthread_attr_setdetachstate (pthread_attr_t * attr, int detachstate). The second parameter can be selected as PTHREAD_CREATE_DETACHED (detached thread) and PTHREAD_CREATE_ JOINABLE (non-detached thread). One thing to note here is that if you set a thread as a detached thread, and the thread runs very fast, it is likely to terminate before the pthread_create function returns, and after it terminates, it may hand over the thread number and system resources to other threads, so that the thread calling pthread_create gets the wrong thread number. To avoid this situation, you can take some synchronization measures, and one of the easiest ways is to call the pthread_cond_timewait function in the created thread and let the thread wait for a while, leaving enough time for the function pthread_create to return. Setting a waiting time is a common method in multithreaded programming. But be careful not to use functions such as wait (), which sleep the entire process and do not solve the problem of thread synchronization.

Another attribute that may be commonly used is the priority of the thread, which is stored in the structure sched_param. Use the function pthread_attr_getschedparam and the function pthread_attr_setschedparam to store, generally speaking, we always take priority first, modify the obtained value and then store it back. Here is a simple example.

# include # include pthread_attr_t attr;pthread_t tid;sched_param param;int newprio=20;pthread_attr_init (& attr); pthread_attr_getschedparam (& attr, WRM); param.sched_priority=newprio;pthread_attr_setschedparam (& attr, WRM); pthread_create (& tid, & attr, (void *) myfunction, myarg); 123456789101112

Data processing of 4 threads

Compared with processes, one of the biggest advantages of threads is the sharing of data. Each process shares the data segment inherited at the parent process, so it is convenient to obtain and modify data. But this also brings many problems to multithreaded programming. We must be careful that there are multiple different processes accessing the same variable. Many functions are non-reentrant, that is, you cannot run multiple copies of a function at the same time (unless different data segments are used). Static variables declared in a function often cause problems, as does the return value of the function. Because if you return the address of the space statically declared within the function, when a thread calls the function to get the address and uses the data that the address points to, another thread may call the function and modify the data. Variables shared in a process must be defined with the keyword volatile to prevent the compiler from changing the way they are used during optimization, such as the use of the-OX parameter in gcc. In order to protect variables, we must use semaphores, mutexes and other methods to ensure our correct use of variables. Next, we will introduce step by step the relevant knowledge when dealing with thread data.

4.1 Thread data

In a single-threaded program, there are two basic types of data: global variables and local variables. But in multithreaded programs, there is a third data type: thread data (TSD: Thread-Specific Data). It is very similar to a global variable, within a thread, each function can call it as if it were a global variable, but it is not visible to other threads outside the thread. The need for such data is obvious. For example, our common variable, errno, returns standard error messages. It obviously cannot be a local variable, and almost every function should be able to call it; but it cannot be a global variable, otherwise the error message of thread B is likely to be output in A thread. To implement variables like this, we must use thread data. We create a key for each thread data, which is associated with this key, and in each thread, we use this key to refer to thread data, but in different threads, the key represents different data, in the same thread, it represents the same data content.

There are four main functions related to thread data: create a key, specify thread data for a key, read thread data from a key, and delete a key.

The function prototype for creating the key is:

Extern int pthread_key_create _ P ((pthread_key_t _ _ key)

Void (_ _ destr_function) (void)

The first parameter is a pointer to a key value, and the second parameter indicates a destructor function. If this parameter is not null, then when each thread ends, the system will call this function to release the block of memory bound to the key. This function is often used with the function pthread_once ((pthread_once_tonce_control, void (* initroutine) (void) so that the key is created only once. The function pthread_once declares an initialization function that executes the function the first time pthread_once is called, and subsequent calls will be ignored by it.

In the following example, we create a key and associate it with some data. We will define a function createWindow, which defines a graphical window (the data type is Fl_Window *, which is the data type in the graphical interface development tool FLTK). Because each thread calls this function, we use thread data.

/ * declare a key * / pthread_key_t myWinKey;/* function createWindow * / void createWindow (void) {Fl_Window * win;static pthread_once_t once= PTHREAD_ONCE_INIT;/* call function createMyKey to create key * / pthread_once (& once, createMyKey); / * win points to a newly created window * / win=new Fl_Window (0,0,100,100, "MyWindow") / * make some possible settings for this window, such as size, location, name, etc. * / setWindow (win); / * bind the window pointer value to the key myWinKey * / pthread_setpecific (myWinKey, win);} / * function createMyKey, create a key and specify destructor * / void createMyKey (void) {pthread_keycreate (& myWinKey, freeWinKey);} / * function freeWinKey, free space * / void freeWinKey (Fl_Window * win) {delete win;} 123456789101113141517181819202122232425

In this way, when you call the function createMyWin in different threads, you can get the window variable that is visible inside the thread, which is obtained through the function pthread_getspecific. In the above example, we have used the function pthread_setspecific to bind thread data to a key. The prototypes of these two functions are as follows:

Extern int pthread_setspecific _ P ((pthread_key_t _ key,__const void * _ pointer))

Extern void * pthread_getspecific _ P ((pthread_key_t _ key))

The parameter meaning and usage of these two functions are obvious. Note that when you use pthread_setspecific to specify new thread data for a key, you must release the original thread data to reclaim space. This procedure function pthread_key_delete is used to delete a key, and the memory occupied by the key will be freed, but it should also be noted that it only releases the memory occupied by the key, not the memory resources occupied by the thread data associated with the key, and it does not trigger the destructor function defined in the function pthread_key_create. The release of thread data must be completed before releasing the key.

4.2 Mutual exclusion lock

Mutexes are used to ensure that only one thread is executing a piece of code at a time. The necessity is obvious: assuming that each thread writes data sequentially to the same file, the result must be catastrophic.

Let's take a look at the following code first. This is a read / write program, they share a buffer, and we assume that a buffer can hold only one piece of information. That is, the buffer has only two states: with or without information.

Void reader_function (void); void writer_function (void); char buffer;int buffer_has_item = 0; mutex;struct timespec delay;void main (void) {pthread_t reader; / * define delay time * / delay.tv_sec = 2; delay.tv_nec = 0; / * initialize a mutex object with default attributes * / pthread_mutex_init (& mutex, NULL) Pthread_create (& reader, pthread_attr_default, (void *) & reader_function), NULL); writer_function ();} void writer_function (void) {while (1) {/ * Lock mutex * / pthread_mutex_lock (& mutex); if (buffer_has_item = = 0) {buffer = make_new_item (); buffer_has_item = 1 } / * Open mutex * / pthread_mutex_unlock (& mutex); pthread_delay_np (& delay);}} void reader_function (void) {while (1) {pthread_mutex_lock (& mutex); if (buffer_has_item = = 1) {consume_item (buffer); buffer_has_item = 0 } pthread_mutex_unlock (& mutex); pthread_delay_np (& delay);} 1234567891011121314151617181920212223242526272830313234353637383940414243

The mutex variable mutex is declared here, and the structure pthread_mutex_t is an undisclosed data type that contains a system-assigned property object. The function pthread_mutex_init is used to generate a mutex. The NULL parameter indicates that the default property is used. If you need to declare a mutex for a specific property, you must call the function pthread_mutexattr_init. The functions pthread_mutexattr_setpshared and pthread_mutexattr_settype are used to set the mutex property. The previous function sets the property pshared, which has two values, PTHREAD_PROCESS_PRIVATE and PTHREAD_PROCESS_SHARED. The former is used to synchronize threads in different processes, and the latter is used to synchronize different threads in this process. In the above example, we are using the default property PTHREAD_PROCESS_ PRIVATE. The latter is used to set the mutex type, and the optional types are PTHREAD_MUTEX_NORMAL, PTHREAD_MUTEX_ERRORCHECK, PTHREAD_MUTEX_RECURSIVE, and PTHREAD_MUTEX_ DEFAULT. They define different upper place and unlock mechanism respectively. In general, the last default attribute is selected.

The pthread_mutex_lock declaration starts locking with mutexes, and the subsequent code is locked until pthread_mutex_unlock is called, that is, it can only be called and executed by one thread at a time. When a thread executes to the pthread_mutex_lock, if the lock is used by another thread at this time, the thread is blocked, that is, the program will wait until another thread releases the mutex. In the above example, we used the pthread_delay_np function to let the thread sleep for a while, just to prevent a thread from occupying the function all the time.

The above example is very simple and will not be introduced any more. what needs to be mentioned is that deadlocks are likely to occur during the use of mutexes: two threads try to occupy two resources at the same time and lock the corresponding mutex locks in different orders. for example, both threads need to lock mutex 1 and mutex lock 2, a thread locks mutex 1 first, b thread locks mutex 2 first, and then deadlocks occur. At this point, we can use the function pthread_mutex_trylock, which is the non-blocking version of the function pthread_mutex_lock. When it finds that deadlock is inevitable, it will return the corresponding information, and the programmer can deal with the deadlock accordingly. In addition, different types of mutexes handle deadlocks differently, but the most important thing is for programmers to pay attention to this in programming.

4.3 conditional variable

In the previous section, we talked about how to use mutexes to share and communicate data between threads. one obvious disadvantage of mutexes is that they have only two states: locked and non-locked. The conditional variable makes up for the deficiency of the mutex by allowing the thread to block and wait for another thread to send a signal, which is often used with the mutex. When used, condition variables are used to block a thread, and when the condition is not met, the thread often unlocks the corresponding mutex and waits for the condition to change. Once another thread changes the condition variable, it notifies the corresponding condition variable to wake up one or more threads that are being blocked by the condition variable. These threads relock the mutex and retest whether the condition is met. Generally speaking, condition variables are used for synchronization between line supports.

The structure of the condition variable is pthread_cond_t, and the function pthread_cond_init () is used to initialize a condition variable. Its prototype is:

Extern int pthread_cond_init _ P ((pthread_cond_t * _ cond,__const pthread_condattr_t * cond_attr))

Where cond is a pointer to the structure pthread_cond_t and cond_attr is a pointer to the structure pthread_condattr_t. Structure pthread_condattr_t is the attribute structure of a condition variable. Like a mutex, we can use it to set whether the condition variable is available within or between processes. The default value is PTHREADPROCESS_PRIVATE, that is, this condition variable is used by threads within the same process. Note that initialization condition variables can be reinitialized or released only if they are not in use. The function that releases a conditional variable is pthread_cond destroy (pthread_cond_t cond).

The function pthread_cond_wait () blocks the thread on a condition variable. Its function prototype is:

Extern int pthread_cond_wait _ P ((pthread_cond_t * _ cond)

Pthread_mutex_t * _ _ mutex))

The thread unlocks the lock pointed to by mutex and is blocked by the condition variable cond. The thread can be awakened by the function pthread_cond_signal and the function pthread_cond_broadcast, but it is important to note that the condition variable only blocks and wakes up the thread, and the specific conditions need to be determined by the user, such as whether a variable is 0 and so on, as we can see in the following example. After the thread is awakened, it will re-check whether the condition is met, and if not, generally speaking, the thread should still be blocked here, waiting to be awakened next time. This process is usually implemented with the whilestatement.

Another function used to block threads is pthread_cond_timedwait (), which is based on:

Extern int pthread_cond_timedwait _ P ((pthread_cond_t * _ cond)

Pthread_mutex_t * _ _ mutex, _ _ const struct timespec * _ abstime))

It has one more time parameter than the function pthread_cond_wait (). After a period of abstime, the blocking is removed even if the condition variable is not satisfied.

The prototype of the function pthread_cond_signal () is:

Extern int pthread_cond_signal _ P ((pthread_cond_t * _ cond))

It is used to release a thread that is blocked on the condition variable cond. When multiple threads block on this condition variable, which thread is awakened is determined by the thread's scheduling policy. It is important to note that this function must be protected with a mutex that protects the condition variable, otherwise the condition satisfaction signal may be issued between the test condition and the call to the pthread_cond_wait function, resulting in unlimited waiting. Here is a simple example of using the function pthread_cond_wait () and the function pthread_cond_signal ().

Pthread_mutex_t count_lock;pthread_cond_t count_nonzero;unsigned count;decrement_count () {pthread_mutex_lock (& count_lock); while (count==0) pthread_cond_wait (& count_nonzero, & count_lock); count=count-1 boot pthreadbare mutextextured unlock (& count_lock);} increment_count () {pthread_mutex_lock (& count_lock); if (count==0) pthread_cond_signal (& count_nonzero); count=count+1;pthread_mutex_unlock (& count_lock);} 123456789101112131415161718

When the count value is 0, the decrement function is blocked at pthread_cond_ wait and opens the mutex count_lock. At this point, when the function increment_count is called, the pthread_cond_signal () function changes the condition variable, telling decrement_count () to stop blocking. Readers can try to have two threads run the two functions separately and see what happens.

The function pthread_cond_broadcast (pthread_cond_t * cond) is used to wake up all threads blocked on the condition variable cond. When awakened, these threads will compete again for the corresponding mutex, so this function must be used with care.

4.4 semaphore

A semaphore is essentially a non-negative integer counter that is used to control access to public resources. When the common resource increases, the function sem_post () is called to increase the semaphore. Common resources can be used only when the signal value is greater than 0, and after use, the function sem_wait () reduces the semaphore. The function sem_trywait () has the same effect as the function pthread_ mutex_trylock (), which is a non-blocking version of the function sem_wait (). Let's introduce some functions related to semaphores one by one, all of which are defined in the header file / usr/include/semaphore.h.

The data type of the semaphore is structured sem_t, which is essentially a long integer number. The function sem_init () initializes a semaphore. Its prototype is:

Extern int sem_init _ P ((sem_t * _ sem, int _ pshared, unsigned int _ value))

Sem is a pointer to the semaphore structure; when pshared is not 0, the semaphore is shared among processes, otherwise it can only be shared by all threads of the current process; value gives the initial value of the semaphore.

The function sem_post (sem_t * sem) is used to increase the value of the semaphore. When there is a thread blocking on this semaphore, calling this function will prevent one of the threads from blocking, and the selection mechanism is also determined by the thread's scheduling policy.

The function sem_wait (sem_t * sem) is used to block the current thread until the value of the semaphore sem is greater than 0. After unblocking, the value of sem is reduced by one, indicating that common resources are reduced after use. The function sem_trywait (sem_t * sem) is a non-blocking version of the function sem_wait (), which directly subtracts the value of the semaphore sem by one.

The function sem_destroy (sem_t * sem) is used to release the semaphore sem.

Let's look at an example of using semaphores. In this example, there are four threads, two of which are responsible for reading data from the file to the common buffer, and the other two are reading data from the buffer for different processing (addition and multiplication).

/ * File sem.c * / # include # define MAXSTACK 100int stack [MAXSTACK] [2]; int size = 0 1.dat sem;/* reads data from the file 1.dat, and each time it is read, the semaphore adds a * / void ReadData1 (void) {FILE * fp = fopen ("1.dat", "r") While (! feof (fp)) {fscanf (fp, "d% d", & stack [size] [0], & stack [size] [1]); sem_post (& sem); + + size;} fclose (fp);} / * read data from file 2.dat * / void ReadData2 (void) {FILE * fp = fopen ("2.dat", "r") While (! feof (fp)) {fscanf (fp, "d% d", & stack [size] [0], & stack [size] [1]); sem_post (& sem); + + size;} fclose (fp) } / * blocking wait for data in the buffer. After reading the data, free up space and continue to wait * / void HandleData1 (void) {while (1) {sem_wait (& sem); printf ("Plus:%d+%d=%d\ n", stack [size] [0], stack [size] [1], stack [size] [0] + stack [size] [1]);-- size } void HandleData2 (void) {while (1) {sem_wait (& sem); printf ("Multiply:%d*%d=%d\ n", stack [size] [0], stack [size] [1], stack [size] [0] * stack [size] [1]);-- size;}} int main (void) {pthread_t T1, T2, T3, T4 Sem_init (& sem, 0,0); pthread_create (& T1, NULL, (void *) HandleData1, NULL); pthread_create (& T2, NULL, (void *) HandleData2, NULL); pthread_create (& T3, NULL, (void *) ReadData1, NULL); pthread_create (& T4, NULL, (void *) ReadData2, NULL) / * prevent the program from exiting prematurely and let it wait here indefinitely * / pthread_join (T1, NULL);} 123456789101112131415161718192021223242526272829303132343536373839404142444464748495051525354556

Under Linux, we generate the executable file sem with the command gcc-lpthread sem.c-o sem. We edit the data files 1.dat and 2.dat beforehand and assume that their contents are 1234567.8910 and-1-2-3-4-5-6-7-8-9-10 respectively. We run sem and get the following results:

Multiply:-1*-2=2

Plus:-1 ±2mm Musi 3

Multiply:910=90

Plus:-9 ±10mm Mui 19

Multiply:-7-800056

Plus:-5 ±6 minutes Murray 11

Multiply:-3*-4=12

Plus:9+10=19

Plus:7+8=15

Plus:5+6=11

From this we can see the competitive relationship between the various threads. The values are not shown in our original order because the size value is arbitrarily modified by each thread. This is often the problem that multithreaded programming should pay attention to.

5 Summary

Multithreaded programming is a very interesting and useful technology. Network ants using multithreaded technology is one of the most commonly used download tools. Grep using multithreaded technology is several times faster than single-threaded grep, and there are many similar examples. I hope you can use multithreading technology to write efficient and practical programs.

6. Signal is a kind of software interrupt, which provides a way to deal with asynchronous events and is the only way to communicate asynchronously between processes. In the Linux system, the extended signal mechanism according to the POSIX standard can not only be used to inform a program of what event has happened, but also to pass data to the process.

6.1. Source of the signal

There are many kinds of signal sources, which can be divided into hardware and software according to the different conditions.

6.1.1, hardware mode

When the user presses a key on the terminal, a signal will be generated. If the key combination is pressed, a SIGINT signal will be generated.

Hardware anomalies generate signals: except for data, invalid storage access, etc. These events are usually detected by hardware (such as CPU) and notified to the Linux operating system kernel, which then generates the corresponding signal and sends the signal to the program in progress at the time of the event.

6.1.2, software mode

The user calls the kill command at the terminal to send a task signal to the process.

The process calls the kill or sigqueue function to send a signal.

Send a signal when a certain software condition is detected, such as when the timer set by alarm or settimer times out, a SIGALRM signal will be generated.

6.2. Types of signals

Enter kill-l under Shell to display all the dependencies supported by the Linux system. The signal list is as follows:

SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL

SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE

SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2

SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT

SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP

SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU

SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH

SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN

SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4

SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8

SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12

SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14

SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10

SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6

SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2

SIGRTMAX-1 64) SIGRTMAX

The value of the signal is defined in signal.h, and there are no 16 and 32 signals in Linux. The meaning of the above signal is as follows:

(1) SIGHUP: when the user exits the Shell, all processes initiated by the Shell will receive this signal, and the default is to terminate the process.

(2) SIGINT: when the user presses the key combination, the client sends this signal to the running program started by the terminal. The default action is to terminate the process.

(3) SIGQUIT: the signal is generated when the user presses the key combination, and the user terminal sends this signal to the running program started by the terminal. The default action is to terminate the process and generate a core file.

(4) SIGILL: CPU detects that a process has executed illegal instructions. The default action is to terminate the process and generate a core file.

(5) SIGTRAP: this signal is generated by breakpoint instructions or other trap instructions. The default action is to terminate the process and generate a core file.

(6) SIGABRT: this signal is generated when the abort function is called. The default action is to terminate the process and generate a core file.

(7) SIGBUS: illegal access to memory address, including memory address alignment (alignment) error, default to terminate the process and generate core files.

(8) SIGFPE: occurs when a fatal arithmetic error occurs. This includes not only floating-point run errors, but also all arithmetic errors such as overflows and divisor 0. The default action is to terminate the process and generate a core file.

(9) SIGKILL: unconditionally terminates the process. This signal cannot be ignored, processed or blocked. The default action is to terminate the process. It provides a way for system administrators to kill any process.

(10) SIGUSR1: a user-defined signal that a program can define and use in a program. The default action is to terminate the process.

(11) SIGSEGV: indicates that the process made invalid memory access. The default action is used to terminate the process and uses this signal. The default action is to terminate the process.

(12) SIGUSR2: this is another user-defined signal that programmers can define and use in the program. The default action is to terminate the process.

(13) SIGPIPE:Broken pipe: writes data to a pipe that does not have a reader. The default action is to terminate the process.

(14) SIGALRM: the timer times out, which is set by the system call alarm. The default action is to terminate the process.

(15) SIGTERM: program end (terminate) signal, which, unlike SIGKILL, can be blocked and processed. It is usually used to require the program to exit normally. This signal is missing when executing the Shell command kill. The default action is to terminate the process.

(16) SIGCHLD: when the subroutine ends, the parent process receives this signal. The default action is to ignore the signal.

(17) SIGCONT: allow a paused process to continue execution.

(18) SIGSTOP: stop (stopped) the execution of the process. Note the difference between it and SIGTERM and SIGINT: the process is not finished, it's just paused. This signal cannot be ignored, processed or blocked. The default is to pause the process.

(19) SIGTSTP: the act of stopping a process, but the signal can be processed and ignored. The signal is sent when the key combination is pressed. The default action is to pause the process.

(20) SIGTTIN: when the background process wants to read data from the user terminal, all processes in the terminal will receive the SIGTTIN signal. The default action is to pause the process.

(21) SIGTTOU: this signal is similar to SIGTIN and is generated when the background process is about to output data to the terminal. The default action is to pause the process.

(22) SIGURG: when there is emergency data on the socket (socket), this signal is sent to the currently running process, reporting the arrival of emergency data. The default action is to ignore the signal.

(23) SIGXCPU: the process execution time exceeds the CPU time assigned to the process, and the system generates the signal and sends it to the process. The default action is to terminate the process.

(24) SIGXFSZ: exceeds the maximum file length limit. The default action terminates the process as yl and generates a core file.

(25) SIGVTALRM: this signal is generated when the virtual clock times out. Similar to SIGALRM, but it only calculates the CPU time that the process is useful. The default action is to terminate the process.

(26) SIGPROF: similar to SIGVTALRM, it includes not only the CPU time consumed by the process, but also the time it takes to execute the system call. The default action is to terminate the process.

(27) SIGWINCH: issued when the window size changes. The default action is to ignore the signal.

(28) SIGIO: this signal instructs the process to issue an asynchronous IO event. The default action is ignored.

(29) SIGPWR: turn it off. The default action is to terminate the process.

(30) the real-time signal of SIGRTMIN~SIGRTMAX:Linux, which has no fixed meaning (or can be used freely by the user). Note that the Linux threading mechanism uses the first three real-time signals. The default action of all real-time signals is to terminate the process.

6.2.1, reliable and unreliable signals

In the Linux system, the reliability of the signal refers to whether the signal will be lost, or whether the signal supports exclusion. The signals between SIGHUP (1) and SIGSYS (31) are inherited from the UNIX system and are unreliable. Linux system defines the signals between SIGRTMIN (33) and SIGRTMAX (64) according to the POSIX standard. They are all reliable signals, also known as real-time signals.

When the event that causes the signal occurs, the kernel generates a signal. After the signal is generated, the kernel usually sets some shape flag in the process table. When the kernel sets this flag, we say that the kernel delivers a signal to a process. The time interval between signal generation (generate) and delivery (delivery), called main signal pending.

A process can call sigpending to set the signal to blocking. If a blocking signal is generated for the process, and the action to the signal is to capture the signal (that is, the signal is not ignored), the kernel will remain pending for the process until the process unblocks the signal or changes its response to the signal to ignore. If this signal occurs multiple times before the process unblocks a signal, it is called a reliable signal if the signal is delivered multiple times (that is, the signal is queued in a pending signal queue); a signal that is delivered only once is called an unreliable signal.

6.2.2, priority of the signal

The signal is essentially a soft interrupt, the interrupt has priority, and the signal also has priority. If a process has multiple pending signals, the kernel will deliver the signals in the order in which they are sent for the same pending real-time signal. If there are multiple pending signals, the smaller the value (or number), the earlier it is delivered. If there are both unreliable signals and reliable signals (real-time signals), although POSIX does not specify this situation, Linux systems, like most operating systems that follow the POSIX standard, will give priority to delivering unreliable signals.

6.3. Response of the process to the signal

When a signal occurs, the user can require the process to respond to the signal in one of three ways.

6.3.1, capture signal: for the signal to be captured, you can specify a signal processing function, which is automatically called when the signal occurs, and the signal processing is realized inside the function.

6.3.2. Ignore signals: most signals can be processed in this way, but SIGKILL and SIGSTOP signals cannot be ignored, and these two signals cannot be captured and blocked. In addition, if you ignore some signal generated by a hardware exception (such as illegal storage access or divided by 0), the behavior of the process is unpredictable.

6.3.3. Handle it in the default way of the system. The default action of most signals is to terminate the process, and the default action of all real-time signals is to terminate the process.

6.4. Default processing of all kinds of signals

The signals that cannot be captured, blocked or ignored by the program are: SIGKILL,SIGSTOP

The signals that cannot be restored to the default action are: SIGILL,SIGTRAP

The default signals that cause the process to abort are: SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGIOT, SIGQUIT, SIGSEGV, SIGTRAP, SIGXCPU, SIGXFSZ

The default signals that cause the process to exit are: SIGALRM, SIGHUP, SIGINT, SIGKILL, SIGPIPE, SIGPOLL, SIGPROF, SIGSYS, SIGTERM, SIGUSR1, SIGUSR2, SIGVTALRM

The signals that cause the process to stop by default are: SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU

The signals ignored by the default process are: SIGCHLD, SIGPWR, SIGURG, SIGWINCH

6.5. Signal processing function and related structure

6.5.1, signal installation

(1), signal ()

# include void (* signal (int signum, void (* handler)) (int) (int); 12

If the function prototype is not easy to understand, you can refer to the following decomposition method:

Typedef void (* sighandler_t) (int)

Sighandler_t signal (int signum, sighandler_t handler))

The first parameter specifies the value of the signal, and the second parameter specifies the processing of the previous signal value, which can be ignored (the parameter is set to SIG_IGN); the signal can be processed in the system default way (the parameter is set to SIG_DFL); or you can implement the processing method yourself (the parameter specifies a function address).

If the signal () call succeeds, it returns the handle value of the last time signal () was called to install the signal signum; if it fails, it returns SIG_ERR.

(2), sigaction ()

# include int sigaction (int signum,const struct sigaction * act,struct sigaction * oldact); 12

The sigaction function is used to change the behavior of a process after receiving a specific signal. The first argument to this function is the value of the signal, which can be used for any specific valid signal except SIGKILL and SIGSTOP (defining your own processing function for these two signals will result in signal installation errors). The second parameter is a pointer to an instance of the structure sigaction, in the instance of the structure sigaction, specifies the processing of a specific signal, which can be null, and the process will process the signal by default; the third parameter oldact points to the object used to save the original processing of the corresponding signal, oldact can be specified as NULL. If the second and third parameters are set to NULL, then this function can be used to check the validity of the signal.

6.5.2. Send signal function

(1) int raise (int sig); sends a specified signal to the current process

(2) int pause (void); suspend the process and wait for the signal

(3) int kill (pid_t pid,int sig); send signal by process number

(4) unsigned int alarm (unsigned int seconds); send SIGALRM signal at a specified time (seconds). Cancel all set alarm requests when seconds is 0

(5) int sigqueue (pid_t pid,int sig,const union sigval val); similar to the kill function, with more union sigval shape numbers attached to the common body, pass the value of the member int sival_int or void * sival_ptr in the common body to int si_int or void * si_ptr in the defined type siginfo_t in the signal processing function

(6) int setitimer (int which,const struct itimerval * value,struct itimerval * oldvalue). Three signal types can be specified according to which: the action time of SIGALRM, SIGVTALRM and SIGPROF; varies with different which values; the member it_interval of struct itimerval defines the interval time, when it_value is 0, to invalidate the timer

(7) void abort (void) will cause the process to terminate unless the SIGABORT signal is captured

6.5.3, signal set and signal set operation

Sigfillset (sigset_t * set); set all signals to the set signal set

Sigemptyset (sigset_t * set); clear all signals from the set signal set

Sigaddset (sigset_t * set,int sig); add sig signal to the set signal set

Sigdelset (sigset_t * set,int sig); delete sig signals from the set signal set

6.5.4. Blocking signal correlation function

Int sigprocmask (int how,const sigset_t * set,sigset_t * set); set the blocking signal set or release the blocking signal set according to the how value

Int sigpending (sigset_t * set); get all the signals in the block

Int sigsuspend (const sigset_t * set); similar to the pause () function!

Thank you for your reading. I believe you have some understanding of "how to analyze multithreaded programming in Linux system". Go to practice quickly. If you want to know more about it, you can follow the website! The editor will continue to bring you better articles!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report