马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
fork 之后父子进程里同一个变量的地址居然打印出来一模一样,这是初学者看到的第一件「见鬼」的事。地址相同,值却互不影响——这背后是虚拟地址和写时拷贝(COW,Copy-On-Write)两件事叠在一起。这篇用两段可运行代码把现象、证据和原理一次拆开。
一、fork 怎么创建进程
Linux 提供多种创建进程的接口,fork、vfork、exec 系列,还有更底层的 clone,常用的就是 fork。调用之后程序被分叉成两个:父子两个进程,而 fork 在两边返回不同的值——父进程里返回子进程的 PID,子进程里返回 0,失败则返回负值。
二、最小测试程序
/*
* ex020101_fork.c
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(int argc, char* argv[])
{
int pid = -1;
pid = fork();
if (pid == 0)
{
// in child
printf("here in child, my pid is %d\n", getpid());
sleep(10);
}
else if (pid > 0)
{
// in parent
printf("here in parent, my pid %d, child pid is %d\n", getpid(), pid);
sleep(10);
}
else
{
// error
printf("fork error[%s]\n", strerror(errno));
}
return 0;
}
编译运行,同时开两个终端:一个跑程序,一个 ps -ef | grep ex0201 | grep -v grep 看进程信息。
[user@host]$ gcc ex020101_fork.c -o ex020101
[user@host]$ ./ex020101
here in parent, my pid 56657, child pid is 56658
here in child, my pid is 56658
ps 输出里两行进程,第二列是 PPID(父进程 PID),第三列是当前进程 PID。第一行是子进程,第二行是父进程,父进程的父进程就是当时所在的终端进程。
三、地址为什么一样:上变量用例
接下来加一个全局变量和一个动态分配的内存在父子进程里各打一遍地址和值:
/* ex020102_forkvar.c 关键片段 */
int g_flag = 1;
int main(int argc, char* argv[])
{
int pid = -1;
char* prompt = NULL;
prompt = (char*)malloc(64);
if (NULL == prompt)
{
printf("memory maybe not enough.\n");
return -1;
}
strncpy(prompt, "I am a starting process.", 64);
printf("g_flag:%d, addr:%p; prompt:%s, addr:%p\n", g_flag, &g_flag, prompt, prompt);
pid = fork();
if (pid == 0)
{
printf("[pid:%d] g_flag:%d, addr:%p; prompt:%s, addr:%p\n",
getpid(), g_flag, &g_flag, prompt, prompt);
g_flag = 2;
free(prompt);
prompt = NULL;
sleep(10);
}
else if (pid > 0)
{
printf("[pid:%d] g_flag:%d, addr:%p; prompt:%s, addr:%p\n",
getpid(), g_flag, &g_flag, prompt, prompt);
sleep(10);
}
printf("[pid:%d] g_flag:%d, addr:%p; prompt:%s, addr:%p\n",
getpid(), g_flag, &g_flag, prompt, prompt);
if (NULL != prompt)
free(prompt);
return 0;
}
四、运行结果与三点结论
gcc ex020102_forkvar.c -o ex020102
./ex020102
g_flag:1, addr:0x60106c; prompt:I am a starting process., addr:0x9372a0
here in parent, my pid 57198, child pid is 57199
[pid:57198] g_flag:1, addr:0x60106c; ...
here in child, my pid is 57199
[pid:57199] g_flag:1, addr:0x60106c; ...
[pid:57198] g_flag:1, addr:0x60106c; ...
[pid:57199] g_flag:2, addr:0x60106c; prompt null), addr nil)
从输出能读出四条信息:
- 父进程 PID 57198,子进程 PID 57199。
- 全局变量
g_flag 地址 0x60106c,指针变量 prompt 地址 0x9372a0。
- 子进程里把
g_flag 改成 2,同时 free(prompt) 并把它置空。
- 父进程里这两个值完全没有变化,仍是 g_flag=1、prompt 有效。
最反直觉的那条:子进程继承到的变量地址和值都和父进程一模一样,可一旦改自己的,父进程那边纹丝不动。
五、原理:写时拷贝 + 虚拟地址
- 完全拷贝内存内容:fork 时子进程拿到和父进程一样的内存布局,所以初始地址和值天然相同。但为了创建速度,内核用的是写时拷贝策略——页表先共享只读,谁真要写了,内核才给那一页复制一份。
- 我们看到的是虚拟地址:进程地址由基地址开始映射,父子的基址相同,于是变量虚拟地址一致。虚拟地址通过页表映射到物理页。
- 虚拟地址相同不代表物理地址相同:改的那一次触发缺页异常,内核把该页复制给子进程,两边从此各用各的物理页。这就是「地址一样、值各走各」的全部秘密。
- 空间大小:32 位机器上每个进程虚拟空间 4 GB,64 位上就大得多,基本用不完。
六、对并发编程的意义
用多进程做并发时,fork 出来的子进程继承父进程的内存状态,可以直接接着用已初始化的变量、配置、缓冲,这是多进程模型省心的地方;代价是修改不回传——父进程拿不到子进程的改动,需要结果就得走 IPC(管道、共享内存、消息队列等)。所以常见写法是:fork 之前把初始化做完,fork 之后各干各的,靠进程间通信回传结果。另外别忘了子进程 exit 时父进程要 wait 回收,避免留下僵尸进程。
qidao123.com企服评测-应用市场 编辑提示 |