Linux——fork进程复制,fork的写时拷贝技术

前言

进程是一个运行中的程序,每个进程都有一个进程控制块,英文缩写PCB,Linux系统中的进程控制块是一个结构体strut task_struct实现(PCB是进程存在的唯一标志)
数据结构中定义的内容是为后面的管理提供支持的,所以不同的操作系统根据自己的特点又对PCB的内容做了一些调整。
Linux——fork进程复制,fork的写时拷贝技术_第1张图片

一般情况下,PCB中包含以下内容:

  • 进程标识符(内部,外部)
  • 处理机的信息(通用寄存器,指令计数器,PSW,用户的栈指针)。
  • 进程调度信息(进程状态,进程的优先级,进程调度所需的其它信息,事件)
  • 进程控制信息(程序的数据的地址,资源清单,进程同步和通信机制,链接指针)

当线程调用fork时,就为子进程创建了整个进程地址空间的副本,子进程和父进程是完全不同的进程,只要两者都没有对内存作出改动,父进程和子进程之间还可以共享内存页的副本。
子进程通过继承整个地址空间的副本、也从父进程那里继承了所有互斥量、读写锁和条件变量的状态、如果父进程包含多个线程、子进程在fork返回之后,如果紧接着不是马上调用exec的话,就需要清理锁状态。

事实上,子进程执行的代码和父进程一模一样,只是fork的返回值不同

  • 父进程的fork的返回值是子进程的PID(大于0)
  • 子进程的fork的返回值等于0(标志)

Linux——fork进程复制,fork的写时拷贝技术_第2张图片

注:

  • getpid():获取当前进程的PID
  • getppid():获取当前进程父进程的PID

执行逻辑:只有当fork执行完成后,子进程才会被复制出来,子进程不会再去fork,并且子进程也不会从头开始执行,而是从返回值处开始执行。
Linux——fork进程复制,fork的写时拷贝技术_第3张图片
bash就是我们“父进程”的“父进程”

也许有的时候父子进程的执行顺序不一样,是因为父子进程是并发运行的,执行顺序并不一定固定。

注意并发和并行的区别:
Linux——fork进程复制,fork的写时拷贝技术_第4张图片

多线程中某个线程调用了fork创建子进程,在子进程中线程的运行情况是怎样的?

#include  
#include  
#include  
#include  
#include 

#include 

void *fun(void *arg) 
{    
	printf("fun start\n");
	pid_t pid = fork();
	assert(pid != -1);
	
    if(pid == 0)
    {
      	int i = 0;
        for(; i < 3; ++i)        
        {            
        	printf("child: mypid = %d\n", getpid());
        	sleep(1);        
        }    
     }    
     
     else    
     {        
     int i = 0;        
     for(; i < 3; ++i)        
	     {            
	     	printf("father: mypid = %d\n", getpid());            	
	     	sleep(1);        
	     }   
     } 
}
int main() 
{    
	pthread_t id;
	int res = pthread_create(&id, NULL, fun, NULL);
	assert(res == 0);
    int i = 0;
    for(; i < 5; ++i)    
    {        
    	printf("main and  mypid = %d\n", getpid());        
    	sleep(1);    
    }
    pthread_join(id, NULL);
    exit(0);
 }

执行结果及结论
Linux——fork进程复制,fork的写时拷贝技术_第5张图片

多线程中调用fork的锁的继承问题

示例代码

#include  
#include  
#include 
#include 
#include 

#include 

pthread_mutex_t mutex;

void *fun(void *arg) 
{    
	sleep(1);
	printf("fun start\n");


    // pthread_mutex_lock(&mutex);    
    pid_t pid = fork();  // 调用fork时,互斥锁的状态是加锁状态    
    assert(pid != -1);    
    // pthread_mutex_unlock(&mutex);  // 父子进程都会执行
    
    if(pid == 0)    
    {       
    	 pthread_mutex_lock(&mutex);
    	 int i = 0;
    	 for(; i < 3; ++i)        
    	 {            
    	 	printf("child: mypid = %d\n", getpid());            
    	 	sleep(1);        
    	 }        
    	 pthread_mutex_unlock(&mutex);    
    }    
    else    
    {        
    	pthread_mutex_lock(&mutex);
    	int i = 0;        
    	for(; i < 3; ++i)       
        {            
        	printf("father: mypid = %d\n", getpid());
        	sleep(1);        
        }        
        pthread_mutex_unlock(&mutex);    
        } 
     }
int main() 
{    
	pthread_mutex_init(&mutex, NULL);    
	pthread_t id;    
	int res = pthread_create(&id, NULL, fun, NULL);   
    assert(res == 0);
    
    pthread_mutex_lock(&mutex);
    int i = 0;    
    for(; i < 5; ++i)    
    {        
    	printf("main and  mypid = %d\n", getpid());        
    	sleep(1);    
    }    
    pthread_mutex_unlock(&mutex);
    
    pthread_join(id, NULL);
    pthread_mutex_destroy(&mutex);    
    exit(0); 
 }

Linux——fork进程复制,fork的写时拷贝技术_第6张图片
结论
调用fork时,子进程会复制父进程的锁,(子进程会继承父进程互斥锁的状态)
解决方案
使用互斥锁对fork()调用做保护,使fork在调用过程中的锁是被自己加锁的,线程库提供了一个注册方法pthread_atfork()
注册方法

  1. prepare这个方法——对所有的锁执行加锁操作,他会在fork调用之前调用
  2. parent这个方法——对所有的锁执行解锁操作,他会在fork调用之后在父进程中执行
  3. child这个方法——对所有的锁执行解锁操作,他会在fork调用之后在子进程中执行。
int pthread_atfork(void (*prepare)();void (*parent),void (*child));

fork的写时拷贝技术

一种推迟或者免除拷贝的技术。

内核fork()时并不复制整个进程地址空间,而是让父子进程共享一个地址空间——>只有在需要写入时,数据才会被复制,从而使各个进程拥有各自的拷贝数据。

也就是说,只有在需要写入的时候才复制资源,在此之前,以只读方式共享。
Linux——fork进程复制,fork的写时拷贝技术_第7张图片
在进程实体的逻辑页中,假如子进程对0号页和1号页进行了修改,那么就单独按照页表将父子进程的这两个页表单独复制出来,2号页和3号页未被修改,其实是可以共享的,只有需要修改这两个页面时,才单独拷贝出来,相当于将页面的复制时间推迟(这也就是写时拷贝技术的含义),这一点用户也不会感受到。

  • 注意:写时拷贝技术是以“”为单位

你可能感兴趣的:(Linux,linux,运维,服务器)