Table of Contents
内存呢?是重复的
CoW(写时复制)
文件在fork上不重复
使用fork创建任务
亲子代号不同
例如:2个使用管道进行通信的进程:
推荐阅读
要在Linux / Unix中创建子进程,可以使用clone(2)或fork(2)。我们使用clone(2)是实现多线程和名称空间。我们使用fork(2)创建一个“真实”(子)进程,即单独的地址空间和资源
在这篇文章中,我将介绍叉子,它的样式和陷阱
让我们从一个简单的例子开始:
void main(void) { puts("hello"); fork(); puts("bye"); }如果运行此代码,您将在输出中看到:
hello bye bye您会看到两次“再见”,因为fork返回了两次-当我们调用fork时,它将创建一个子进程并返回两次-一个返回给父进程,另一个返回给子进程。在父进程上,fork返回子进程ID,在子进程上,它返回0
通话一次,返回两次-WTF ???
在运行复杂算法时,您想要使用多个处理器,但是需要使用并行模式编写代码。fork帮助您实现一个简单的模式– fork – join
例如,我们有一个大数组(在共享内存中),我们想在每个数组元素中计算一些东西,用fork的方法很简单:
#include<stdio.h> #include <sys/types.h> #include <sys/wait.h> void main(void) { int x,status; puts("parent only"); x=fork(); if(x>0){ printf("parent: do half task \n"); wait(&status); } else { printf("child: do half task \n"); exit(0); } puts("parent only"); }如您所见,在fork之前,我们只有一个过程,我们创建一个子进程,该子进程完成工作并退出(将值返回给父进程),而父进程完成工作并等待子进程完成工作,因此在if之后仅声明其一个过程
exit关闭该过程并将结果发送给父级。父级从等待系统调用时通过状态参数知道其子级为何终止(正常或通过信号)和返回值(或信号号)(有关详细信息和宏,请参见手册页)
如果我们声明一个数组并在fork之后更改它,我们可以看到它是重复的:
#include<stdio.h> #include <sys/types.h> #include <sys/wait.h> void main(void) { int x,status; int arr[100000]={1,2,3}; x=fork(); if(x>0){ arr[1]=200; wait(&status); } else { sleep(3); printf("child: arr[1]=%d \n",arr[1]); // print 2 exit(0); } }子进程正在等待3秒钟,以确保父进程更改了值并打印数组元素。结果是arr [1] = 2,因为每个进程都有自己的内存空间。看起来内核复制了fork上的内存。
为了节省时间,内核仅复制其映射。例如,数组有100页映射,内核将复制TLB条目并将所有映射更改为只读。如果两个进程都只读取,则它们使用共享内存,但是当一个进程尝试写入时,它会产生页面错误,内核陷阱处理程序将复制该页面并更改读写权限,从而将程序计数器返回到上一个语句以尝试再次。
如果我们测量第一次分配的时间,则会得到更长的时间:
... x=fork(); if(x>0){ arr[1]=200; // page fault, copy page and update TLB arr[2]=300; // one memory access wait(&status); }为了使用Ubuntu 64位进行测试,我们添加了一个简单的汇编函数
#include<stdio.h> #include <sys/types.h> #include <sys/wait.h> static __inline__ unsigned long long rdtsc(void) { unsigned hi, lo; __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi)); return ( (unsigned long long)lo)|( ((unsigned long long)hi)<<32 ); } void main(void) { int x,status; long long t1,t2,t3,t4; int arr[100000]={1,2,3,4,5}; x=fork(); if(x>0){ t1=rdtsc(); arr[1]=20; t2=rdtsc(); arr[2]=30; t3=rdtsc(); arr[3]=40; t4=rdtsc(); printf("t1=%lld\n",t1); printf("t2=%lld\n",t2); printf("t3=%lld\n",t3); printf("t4=%lld\n",t4); wait(&status); } else { printf("child \n"); exit(0); } puts("parent only"); }输出:
t1=40593170692468 t2=40593170692600 t3=40593170692640 t4=40593170692676从输出中可以看到,由于页面错误,我们第一次写入的时间要长3倍
不能fork?
fork在某些情况下失败:如果我们达到了允许的最大用户进程(请参阅ulimit -a),内存不足,没有MMU等等(请参见手册页)
如果父进程占用的系统内存超过50%,也会失败。举个例子:
void main(void) { int x,status; int arr1[50000000]; puts("bye"); memset(arr1,0,sizeof(arr1)); x=fork(); ... }该程序声明一个200MB的数组,并使用memset清除它以使其映射到物理内存。正如我们所看到的,在fork上,内存不是重复的,而是系统检查我们是否具有所需的可用内存,以防子代写入。如果系统没有200MB可用空间,则fork将失败
在以下情况下,以上行为会产生一个奇怪的错误:
父级填充200MB数组并进行fork在fork上,系统有250 MB可用空间– fork成功返回由于写时复制机制,不会消耗内存另一个进程消耗200MB子进程访问数组元素,并且在尝试处理页面错误时内核崩溃了让我们在512MB的Qemu图像上进行测试:
# cat /proc/meminfo代码示例:
#include<stdio.h> #include <sys/types.h> #include <sys/wait.h> void main(void) { int x,status; int arr1[50000000]; memset(arr1,0,sizeof(arr1)); x=fork(); if(x>0){ printf("fork returned:%d\n",x); wait(&status); else { int i,r; for(i=0;i<50;i++){ memset(arr1+i*1000000,0,4000000); sleep(30); puts("mem"); } } }运行此程序并在fork之后:
# cat /proc/meminfo我们可以看到仅消耗了200MB。现在运行一个简单的程序以消耗更多的内存:
void main(void) { int x,status; int arr1[52000000]; puts("bye"); memset(arr1,0,sizeof(arr1)); sleep(1000); }并再次检查:
# cat /proc/meminfo现在,子进程每30秒写入4MB并复制页面,当它达到系统限制(仅100MB)时,我们将看到一个内核oops:
app invoked oom-killer: gfp_mask=0x24200ca(GFP_HIGHUSER_MOVABLE), nodemask=0, order=0, oom_score_adj=0 app cpuset=/ mems_allowed=0 CPU: 0 PID: 750 Comm: app Not tainted 4.9.30 #35 Hardware name: ARM-Versatile Express [<8011196c>] (unwind_backtrace) from [<8010cf2c>] (show_stack+0x20/0x24) [<8010cf2c>] (show_stack) from [<803d32a4>] (dump_stack+0xac/0xd8) [<803d32a4>] (dump_stack) from [<8023da88>] (dump_header+0x8c/0x1c4) [<8023da88>] (dump_header) from [<801ef464>] (oom_kill_process+0x3a8/0x4b0) [<801ef464>] (oom_kill_process) from [<801ef8c0>] (out_of_memory+0x124/0x418) [<801ef8c0>] (out_of_memory) from [<801f48b4>] (__alloc_pages_nodemask+0xd6c/0xe0c) [<801f48b4>] (__alloc_pages_nodemask) from [<80219338>] (wp_page_copy+0x78/0x580) [<80219338>] (wp_page_copy) from [<8021a630>] (do_wp_page+0x148/0x670) [<8021a630>] (do_wp_page) from [<8021cdd8>] (handle_mm_fault+0x33c/0xb00) [<8021cdd8>] (handle_mm_fault) from [<80117930>] (do_page_fault+0x26c/0x384) [<80117930>] (do_page_fault) from [<80101288>] (do_DataAbort+0x48/0xc4) [<80101288>] (do_DataAbort) from [<8010dec4>] (__dabt_usr+0x44/0x60) Exception stack(0x9ecc3fb0 to 0x9ecc3ff8) 3fa0: 77aaa7f8 00000000 0007c0f0 77dff000 3fc0: 00000000 00000000 000084a0 00000000 00000000 00000000 2b095000 7e94ad04 3fe0: 00000000 722ed8f8 00008678 2b13c158 20000010 ffffffff Mem-Info: active_anon:124980 inactive_anon:2 isolated_anon:0 active_file:23 inactive_file:31 isolated_file:0 unevictable:0 dirty:0 writeback:0 unstable:0 slab_reclaimable:457 slab_unreclaimable:598 mapped:46 shmem:8 pagetables:323 bounce:0 free:713 free_pcp:30 free_cma:0 Node 0 active_anon:499920kB inactive_anon:8kB active_file:92kB inactive_file:124kB unevictable:0kB isolated(anon):0kB isolated(file):0kB mapped:184kB dirty:0kB writeback:0kB shmem:32kB writeback_tmp:0kB unstable:0kB pages_scanned:56 all_unreclaimable? no Normal free:2852kB min:2856kB low:3568kB high:4280kB active_anon:499920kB inactive_anon:8kB active_file:92kB inactive_file:124kB unevictable:0kB writepending:0kB present:524288kB managed:510824kB mlocked:0kB slab_reclaimable:1828kB slab_unreclaimable:2392kB kernel_stack:344kB pagetables:1292kB bounce:0kB free_pcp:120kB local_pcp:120kB free_cma:0kB lowmem_reserve[]: 0 0 Normal: 7*4kB (UE) 5*8kB (UME) 4*16kB (UME) 1*32kB (U) 2*64kB (UM) 2*128kB (UM) 1*256kB (M) 0*512kB 0*1024kB 1*2048kB (U) 0*4096kB = 2852kB 62 total pagecache pages 0 pages in swap cache Swap cache stats: add 0, delete 0, find 0/0 Free swap = 0kB Total swap = 0kB 131072 pages RAM 0 pages HighMem/MovableOnly 3366 pages reserved 0 pages cma reserved [ pid ] uid tgid total_vm rss nr_ptes nr_pmds swapents oom_score_adj name [ 723] 0 723 598 6 3 0 0 0 syslogd [ 725] 0 725 598 6 4 0 0 0 klogd [ 737] 0 737 621 42 4 0 0 0 sh [ 749] 0 749 51186 50747 103 0 0 0 app [ 750] 0 750 51186 50813 102 0 0 0 app [ 751] 0 751 51186 50752 103 0 0 0 eat Out of memory: Kill process 750 (app) score 386 or sacrifice child Killed process 750 (app) total-vm:204744kB, anon-rss:203180kB, file-rss:72kB, shmem-rss:0kB oom_reaper: reaped process 750 (app), now anon-rss:4kB, file-rss:0kB, shmem-rss:0kB我们可以看到在页面错误(do_page_fault)上生成的oops
为了避免这种情况,我们需要在派生后立即对数组进行预故障处理(每页至少要读取和写入其内容一次)
重要的是要了解文件描述符对象在fork上不重复。通过这种方式,我们可以在子进程和父进程之间共享资源。所有匿名对象(管道,共享内存等)只能使用文件描述符进行共享。一种方法(简单的方法)是在派生之前声明资源,另一种方法是使用unix域套接字发送文件描述符。如果我们打开一个常规文件,则子级和父级将使用相同的内核对象,即位置,标志,权限等将被共享:
例:
#include <stdio.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/stat.h> #include <fcntl.h> void main(void) { int x,status,fd; fd=open("./syslog1",O_RDWR); x=fork(); if(x>0){ char buf[30]; read(fd,buf,29); buf[30]='\0'; puts(buf); wait(&status); } else { char buf[30]; sleep(3); read(fd,buf,29); buf[30]='\0'; puts(buf); exit(0); } }我们打开一个文件,在父级上读取29个字符,在子级上读取29个字符,父级更新位置,以便子级读取父级结束的位置:
输出:
Dec 16 21:46:55 developer-vir tual-machine rsyslogd: [origi
因为fork很奇怪,所以熟悉它的模式很有用。如果我们要创建任务并且拥有共享代码(例如RTOS),但是我们希望将任务创建为单独的进程(而不是线程),那么如果一个任务失败,则不会使所有其他任务崩溃。我们在一个循环上创建任务,然后主进程将等待子退出并重新生成它。
#include "stdio.h" #include <stdlib.h> #include <sys/prctl.h> #include <sys/types.h> #include <sys/wait.h> void task1(void) { prctl(PR_SET_NAME,"task1"); while(1) { puts("task1"); sleep(10); } } void task2(void) { prctl(PR_SET_NAME,"task2"); while(1) { puts("task2"); sleep(10); } } void task3(void) { prctl(PR_SET_NAME,"task3"); while(1) { puts("task3"); sleep(10); } } void task4(void) { prctl(PR_SET_NAME,"task4"); while(1) { puts("task4"); sleep(10); } } void task5(void) { int c=0; prctl(PR_SET_NAME,"task5"); while(1) { c++; if(c==5) exit(12); puts("task5"); sleep(3); } } void (*arr[5])(void) = {task1,task2,task3,task4,task5}; int findpid(int *arr,int size,int val) { int i; for(i=0;i<size;i++) { if(arr[i] == val) return i; } return -1; } int main(void) { int ids[5]; int v,i,status,pid,pos; for(i=0;i<5;i++) { v = fork(); if(v == 0) { arr[i](); exit(0); } ids[i]=v; } while(1) { pid=wait(&status); pos = findpid(ids,5,pid); printf("bye parent %d %d\n",pid,status); printf("Child exist with status of %d\n", WEXITSTATUS(status)); v=fork(); if(v==0) { arr[pos](); exit(0); } ids[pos] = v; } return EXIT_SUCCESS; }如果运行上述程序,则将看到6个进程正在运行。等待子进程退出(通过信号退出或正常退出)并重新创建它的主要过程。如果您向其中一个子进程发送信号,您将看到它重生。
有时我们需要两个具有父子关系的进程(以发送信号或共享资源),但是它们具有完全不同的代码。例如,在无线路由器中,我们具有路由控制过程和Web服务器。我们希望Web服务器进程在每次配置更改时都向路由器控件发送信号。我们可以将fork与execve一起使用来实现它:
父应用:
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main(int argc,char *argv[]) { char buf[20]="hello"; int fd,i=0; fd = atoi(argv[0]); puts("parent started:"); while(1) { i++; sprintf(buf,"hello:%d",i); write(fd,buf,10); sleep(2); } return 0; }编译并调用可执行文件parent_app
子应用:
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main(int argc,char *argv[]) { char buf[11]; int fd; puts("child started"); fd = atoi(argv[0]); while(1) { read(fd,buf,10); puts(buf); } return 0; }编译并命名为child_app
现在编写代码以连接它们:
#include<stdio.h> #include<unistd.h> int main() { int arr[2]; char argv[30]; pipe(arr); if(fork()) { puts("starting parent"); close(arr[0]); sprintf(argv,"%d",arr[1]); execlp("./parent_app",argv,NULL); } else { puts("starting child"); close(arr[1]); sprintf(argv,"%d",arr[0]); execlp("./child_app",argv,NULL); } return 1; }编译并运行它(将以前的可执行文件放在同一目录中)
我们创建一个管道,在父级上,关闭read fd,并将write fd作为参数移至main,在子级上,执行相反的操作
请注意,在此设计中,如果我们要更改通信类型以使用unix域套接字(或基于文件描述符的任何其他对象),则只需更改并编译最后一个程序。
《fork() 成为负担》
