4. 创建进程
4.1 fork、vfork函数
(1)函数原型
头文件 |
#include<unistd.h> #include<sys/types.h> |
函数 |
pid_t fork(void); pid_t vfork(void) |
返回值 |
子进程中为0,父进程中为子进程ID,出错为-1 |
功能 |
创建子进程 |
备注 |
(1)fork创建的新进程被称为子进程,该函数被调用一次,但返回两次。两次返回的区别是:在子进程中的返回值为0,而在父进程的返回值为新子进程的进程ID。 (2)创建子进程,父子进程哪个先运行是根据系统调度且复制父进程的内存空间(数据空间、堆、栈)。 (3)vfork创建子进程,但子进程先运行且不复制父进程的内存空间。 |
【编程实验】父子进程交替输出
//process_fork.c
#include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(int argc, char* argv[]) { printf("pid: %d ", getpid());//只有父进程会执行,子进程不执行。 pid_t pid; pid = fork(); //创建子进程,调用该函数这后会出现父子两个进程。其中 //父进程调用fork后,返回子进程的ID。由于子进程复制了 //父进程的内存空间和寄存器状态。因此也会复制父进程的 //EIP,然后从EIP所指位置开始执行,但函数的返回值为0. //fork之后会运行两个进程(父进程和子进程) if(pid < 0){ perror("fork error"); exit(1); }else if (pid > 0){ //父进程(在父进程中fork返回的是子进程的ID) printf("I am parent process, my pid is %d, ppid is %d, fork return value is %d ", getpid(), getppid(), pid); int i = 0; for(i=0; i<10; i++){ printf("%d: This is a parent process pid is: %d ",i + 1, getpid()); sleep(1); } }else{ //子进程(在子进程中fork返回的是0) printf("I am child process, my pid is %d, ppid is %d, fork return value is %d ", getpid(), getppid(), pid); int i = 0; for(i=0; i<10; i++){ printf("%d: This is a child process pid is: %d ",i + 1, getpid()); sleep(1); } } printf("pid: %d ", getpid()); //父子进程都会执行到这里! sleep(1); exit(0); return 0; }
4.2 子进程的继承
(1)子进程的继承属性(即从父进程复制一份过来)
①用户信息和权限、目录信息、信号信息、环境、资源限制。
②共享存储段、共享代码段(注意:代码段只有一份)、堆、栈和数据段、存储映射。
【编程实验】父子进程的复制(数据段、堆栈)
//process_fork2.c
#include <unistd.h> #include <stdio.h> #include <stdlib.h> int g_v = 30; //注意:是个全局变量 int main(int argc, char* argv[]) { int a_v = 30; static int s_v = 30; printf("pid: %d ", getpid());//只有父进程会执行,子进程不执行。 pid_t pid; pid = fork(); //创建子进程 //fork之后会运行两个进程(父进程和子进程) if(pid < 0){ perror("fork error"); exit(1); }else if (pid > 0){ //父进程(在父进程中fork返回的是子进程的ID) printf("I am parent process, my pid is %d, ppid is %d, fork return value is %d ", getpid(), getppid(), pid); g_v = 50; a_v =50; s_v = 50; //打印各种变量的地址 printf("g_v, %p, a_v: %p, s_v: %p ", &g_v, &a_v, &s_v); }else{ //子进程(在子进程中fork返回的是0) printf("I am child process, my pid is %d, ppid is %d, fork return value is %d ", getpid(), getppid(), pid); g_v = 40; a_v = 40; s_v = 40; //打印各种变量的地址 printf("g_v, %p, a_v: %p, s_v: %p ", &g_v, &a_v, &s_v); } //打印各类变量的值 printf("pid: %d, g_v: %d, a_v: %d, s_v: %d ", getpid(), g_v, a_v, s_v); //父子进程都会执行到这里! sleep(1); exit(0); return 0; } /*输出结果:(注意,父子进程中变量虚拟地址一样,但存放物理内存不同!) pid: 1586 I am parent process, my pid is 1586, ppid is 1476, fork return value is 1587 g_v, 0x80499bc, a_v: 0xbf9f2928, s_v: 0x80499c0 pid: 1586, g_v: 50, a_v: 50, s_v: 50 I am child process, my pid is 1587, ppid is 1586, fork return value is 0 g_v, 0x80499bc, a_v: 0xbf9f2928, s_v: 0x80499c0 pid: 1587, g_v: 40, a_v: 40, s_v: 40 */
(2)子进程特有的属性
①进程ID、锁信息、运行时间、未决信息
(3)操作文件时的内核结构变化
①子进程只继承父进程的文件描述表,不继承但共享文件表项和i-node。实际上fork的一个特性是父进程的所有打开文件描述符都被复制到子进程中。父、子进程的每个相同的打开描述符共一个文件表项。这种共享方式使父、子进程对同一文件使用了同一个文件偏移量。
②父进程创建一个子进程后,文件表项中的引用计数加1。当父进程close操作后,计数减1,子进程还是可以使用文件表项,只有当计数器为0时才会释放文件表项。
【编程实验】文件共享1(父子进程同写一个文件)
//process_fork3.c
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <string.h> int main(int argc, char* argv[]) { FILE* fp = fopen("s.txt", "w"); //标准C库函数方式 int fd = open("s_fd.txt", O_WRONLY | O_CREAT | O_TRUNC, S_IRWXU | S_IRWXG); //系统调用方式 char* s = "hello world!"; ssize_t size = strlen(s) * sizeof(char); /*注意:以下写入文件操作是在fork调用这前*/ //标准IO函数(带缓存),当fork子进程后,缓存也会被复制给 //子进程,所以父子进程当前的缓存内存内容都是一样的,即 //会写入同样的一段内容。 fprintf(fp, "s: %s, pid: %d", s, getpid()); //写入缓存 //内核提供的IO系统调用(不带缓存) write(fd, s, size); //不带缓存,直接写入文件。 pid_t pid; pid = fork(); //创建子进程 //fork之后会运行两个进程(父进程和子进程) if(pid < 0){ perror("fork error"); exit(1); }else if (pid > 0){//父进程(在父进程中fork返回的是子进程的ID) //父子进程的fd指向同一个文件表项,共享文件偏移量 char* context = " I come from parent process!"; write(fd, context, strlen(context)* sizeof(char)); }else{ //子进程(在子进程中fork返回的是0) char* context = " I come from child process!"; //父子进程的fd指向同一个文件表项,共享文件偏移量 write(fd, context, strlen(context)* sizeof(char)); } //父子进程都要执行,由于带缓存,会将之前的内容(相同的)连同以下的 //内容(不同的)写入文件中。 fprintf(fp, " pid: %d ", getpid()); fclose(fp); close(fd); sleep(1); exit(0); return 0; } /*输出结果:(注意,父子进程中变量虚拟地址一样,但存放物理内存不同!) s.txt的内容: : hello world!, pid: 1654 pid: 1654 s: hello world!, pid: 1654 pid: 1655 s_fd.txt的内容: world!I come from parent process! I come from child process! */
【编程实验】文件共享2(父进程调整文件偏移量,子进程写文件)
//process_append.c
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <string.h> int main(int argc, char* argv[]) { if(argc < 2) { fprintf(stderr, "usage: %s file ", argv[0]); exit(1); } int fd = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC); if(fd < 2){ perror("open error"); exit(1); } pid_t pid = fork(); //创建子进程 if(pid < 0){ perror("fork error"); exit(1); }else if (pid > 0) { //parent process //父进程将文件偏移量调整到文件尾部 if(lseek(fd, 0L, SEEK_END) < 0){ perror("lseek error"); exit(1); } }else{ //child process //子进程从文件尾部追加内容 char* str = "hello child "; ssize_t size = strlen(str)* sizeof(char); sleep(3); //休眠,等待父进程调整文件偏移量 //此处的fd是从父进程中复制而来,与父进程的fd指向同一个文件表项 if(write(fd, str, size) != size){ perror("write error"); exit(1); } } printf("pid: %d finish! ", getpid()); close(fd); //父子进程都会执行该行,分别将文件引用计数器减1 sleep(1); return 0; }
4.3 fork的用法
(1)一个父进程希望复制自己,使父子同时执行不同的代码段。这在网络服务进程中最常见——父进程等待客户端的服务请求。当请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求的到达。
(2)一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。