preface
For the concepts of process, multithreading and fork, please see the two articles I wrote before.
Linux: process control (process creation, process termination, process waiting, process program replacement)
Linux: detailed explanation of multithreading (thread concept, thread control - thread creation, thread termination, thread waiting) (I)
1. Talking about fork in multithreading
When the fork function creates a child process, the child process will copy the process virtual address space of the parent process, and will also copy a corresponding memory data from the parent process to the child process. As we know, this is true in the case of a single process, but what if it is in multithreading? In multithreaded code, how do fork functions operate if fork functions are invoked in a worker thread? Do you copy the entire worker thread of the program, or just copy the virtual address space of the worker thread currently created?
In the POSIX standard, fork's behavior is as follows: copy the data of the entire user space (usually using the copy on write strategy, so it can be implemented quickly) and all system objects, and then copy only the current thread to the child process. Here: all other threads in the parent process suddenly evaporate in the child process. The sudden disappearance of other threads is the root of all problems.
On most operating systems, for performance reasons, locks are basically implemented in the user state rather than the kernel state (because it is most convenient to implement in the user state, basically through atomic operation). Therefore, when fork is called, all locks of the parent process will be copied to the child process.
As A result, if thread A locks the locks in the current global variable before creating the child process (in other words, the corresponding locks have been obtained before creating the child process), after creating the child process, the child process and the corresponding parent process are two completely different processes (process independence), However, the child process is copied from the process virtual space of the parent process, which causes that if the parent process has obtained the lock, the corresponding lock in the child process is also locked, but the child process does not know the state of the lock at present. Therefore, once the lock is locked in the child process, deadlock will occur.
To sum up, the causes of deadlock are:
- The memory data of the parent process will be copied to the child process intact
- The child process is generated in the single thread state, and only the thread where the fork function is located is generated.
- If a lock in the parent process (before the child process is created) has been locked, and the child process locks the lock again, a deadlock will occur
2. Simulation of deadlock problem
#include <unistd.h> #include <sys/wait.h> #include <pthread.h> #include <iostream> using namespace std; pthread_mutex_t mt; void* pthreadFork(void * arg) { pthread_detach(pthread_self()); //The reason for waiting 2 seconds is to let pthreadLock get the lock first sleep(2); int ret = fork(); if(ret < 0) { cout << "fork failed" << endl; return 0; } else if(ret == 0) { //child cout << "It's Child !" << endl; while(1) { pthread_mutex_lock(&mt); cout << "It's test pthreadFork_Child " << endl; pthread_mutex_unlock(&mt); } } else { //father cout << "It's father!" << endl; wait(NULL); } return NULL; } void* pthreadLock(void * arg) { pthread_detach(pthread_self()); while(1) { pthread_mutex_lock(&mt); cout << "It's pthreadLock" << endl; //The reason for waiting for 3 seconds is to let the pthreadFork thread create a child process when the lock has been obtained sleep(3); pthread_mutex_unlock(&mt); } return NULL; } int main() { pthread_t pid; pthread_mutex_init(&mt,NULL); int ret = pthread_create(&pid,NULL,pthreadFork,NULL); if(ret < 0) { cout << "pthread failed" << endl; return 0; } ret = pthread_create(&pid,NULL,pthreadLock,NULL); if(ret < 0) { cout << "pthread failed" << endl; return 0; } while(1) { sleep(1); } pthread_mutex_destroy(&mt); return 0; }
Result verification:
Use ps -ef | grep xxx to get the pid number of the current child process, and then use gdb attach pid to debug it
Check the status of the current lock and find that it is already in a deadlock state, or you have obtained an abandoned lock at the beginning (a lock that cannot be locked).
To debug the thread 12576, first use gdb to debug its process
Here, we can easily verify that the child process has deadlock and entered the blocking state, while the thread in the parent process is running normally (further reflecting the independence of the process).
3. Solutions
Solution ①: in multithreading, you can immediately call the exec function (process program replacement) in the child process of fork.
For the concept of process program replacement, see the link I gave in the preface.
Solution ②: use pthread_atfork function.
int pthread_atfork(void (*prepare)(void), void (*parent)(void),void (*child)(void));
- The prepare:prepare processing function is called by the parent process before creating the child process in fork. The task of this function is to get all the locks defined by the parent process.
- The parent:parent processing function is created after the child process is created in fork, but is invoked in the parent process environment before fork returns. Its task is to unlock all locks obtained by prepare.
- The child:child processing function is invoked in the sub process environment before fork returns. Just like the parent processing function, it must also unlock all the locks in all prepare.
Code implementation:
#include <unistd.h> #include <sys/wait.h> #include <pthread.h> #include <iostream> using namespace std; pthread_mutex_t mt; void* pthreadFork(void * arg) { pthread_detach(pthread_self()); sleep(2); int ret = fork(); if(ret < 0) { cout << "fork failed" << endl; return 0; } else if(ret == 0) { //child cout << "It's Child !" << endl; while(1) { pthread_mutex_lock(&mt); cout << "It's test pthreadFork_Child " << endl; pthread_mutex_unlock(&mt); } } else { //father cout << "It's father!" << endl; wait(NULL); } return NULL; } void* pthreadLock(void * arg) { pthread_detach(pthread_self()); while(1) { pthread_mutex_lock(&mt); cout << "It's pthreadLock" << endl; sleep(3); pthread_mutex_unlock(&mt); //The reason for sleeping for 1 second here is that the prepare function can get the lock sleep(1); } return NULL; } void prepare(void) { pthread_mutex_lock(&mt); cout << "prepare: Get mutex success" << endl; } void child(void) { pthread_mutex_unlock(&mt); cout << "child: release mutex success" << endl; } void parent(void) { pthread_mutex_unlock(&mt); cout << "parent: release mutex success" << endl; } int main() { pthread_t pid; int ret = pthread_atfork(prepare,parent,child); pthread_mutex_init(&mt,NULL); ret = pthread_create(&pid,NULL,pthreadFork,NULL); if(ret < 0) { cout << "pthread failed" << endl; return 0; } ret = pthread_create(&pid,NULL,pthreadLock,NULL); if(ret < 0) { cout << "pthread failed" << endl; return 0; } while(1) { sleep(1); } pthread_mutex_destroy(&mt); return 0; }
Result verification:
It can be clearly found that the child process can operate the lock accordingly, which can solve this kind of problem.