进程是一个运行中的程序,每个进程都有一个进程控制块,英文缩写PCB
,Linux系统中的进程控制块是一个结构体strut task_struct
实现(PCB是进程存在的唯一标志)
数据结构中定义的内容是为后面的管理提供支持的,所以不同的操作系统根据自己的特点又对PCB的内容做了一些调整。
一般情况下,PCB中包含以下内容:
当线程调用fork时,就为子进程创建了整个进程地址空间的副本,子进程和父进程是完全不同的进程,只要两者都没有对内存作出改动,父进程和子进程之间还可以共享内存页的副本。
子进程通过继承整个地址空间的副本、也从父进程那里继承了所有互斥量、读写锁和条件变量的状态、如果父进程包含多个线程、子进程在fork返回之后,如果紧接着不是马上调用exec的话,就需要清理锁状态。
事实上,子进程执行的代码和父进程一模一样,只是fork的返回值不同
注:
执行逻辑:只有当fork执行完成后,子进程才会被复制出来,子进程不会再去fork,并且子进程也不会从头开始执行,而是从返回值处开始执行。
bash就是我们“父进程”的“父进程”
也许有的时候父子进程的执行顺序不一样,是因为父子进程是并发运行
的,执行顺序并不一定固定。
多线程中某个线程调用了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);
}
示例代码
#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);
}
结论
调用fork时,子进程会复制父进程的锁,(子进程会继承父进程互斥锁的状态)
解决方案
使用互斥锁对fork()调用做保护,使fork在调用过程中的锁是被自己加锁的,线程库提供了一个注册方法pthread_atfork()
注册方法
int pthread_atfork(void (*prepare)();void (*parent),void (*child));
一种推迟或者免除拷贝的技术。
内核fork()时并不复制整个进程地址空间,而是让父子进程共享一个地址空间——>只有在需要写入时,数据才会被复制,从而使各个进程拥有各自的拷贝数据。
也就是说,只有在需要写入的时候才复制资源,在此之前,以只读方式共享。
在进程实体的逻辑页中,假如子进程对0号页和1号页进行了修改,那么就单独按照页表将父子进程的这两个页表单独复制出来,2号页和3号页未被修改,其实是可以共享的,只有需要修改这两个页面时,才单独拷贝出来,相当于将页面的复制时间推迟(这也就是写时拷贝技术的含义),这一点用户也不会感受到。