目录
3.1 fork()函数之后,子进程会获得父进程所有文件描述符的副本
3.2父、子进程中对应的文件描述符指向了相同的文件表 ——代码测试
一 什么是进程
1.1 进程概念
进程其实就是一个可执行程序的实例,可执行程序就是一个可执行文件,文件是一个 静态的概念,存放磁盘中,如果可执行文件没有被运行,那它将不会产生什么作用,当它被运行之后,它将 会对系统环境产生一定的影响,所以可执行程序的实例就是可执行文件被运行。
进程是一个动态过程,而非静态文件,它是程序的一次运行过程,当应用程序被加载到内存中运行之后 它就称为了一个进程,当程序运行结束后也就意味着进程终止,这就是进程的一个生命周期。
1.2 获取进程PID号 —— getpid()
Linux 系统下的每一个进程都有一个进程号(process ID,简称 PID),进程号是一个正数,用于唯一标 识系统中的某一个进程。
在应用程序中,可通过系统调用 getpid()来获取本进程的进程号,其函数原型如下所示:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
应用代码测试:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
pid_t pid = getpid();
printf("本进程的 PID 为: %d\n", pid);
exit(0);
}
1.3 getppid()获取父进程PID号
除了 getpid()用于获取本进程的进程号之外,还可以使用 getppid()系统调用获取父进程的进程号,其函 数原型如下所示:
#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);
返回值:返回父进程的PID号
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
pid_t pid = getpid(); //获取本进程 pid
printf("本进程的 PID 为: %d\n", pid);
pid = getppid(); //获取父进程 pid
printf("父进程的 PID 为: %d\n", pid);
exit(0);
}
二 fork()创建子进程
一个现有的进程可以调用 fork()函数创建一个新的进程,调用 fork()函数的进程称为父进程,由 fork()函 数创建出来的进程被称为子进程(child process),fork()函数原型如下所示(fork()为系统调用):
2.1 fork()函数原型:
#include <unistd.h>
pid_t fork(void);
返回值:
- 失败返回-1 不创建子进程,并设置 errno
- 返回值为非负数为父进程
- 返回值为0时是子进程
在诸多的应用中,创建多个进程是任务分解时行之有效的方法,譬如,某一网络服务器进程可在监听客 户端请求的同时,为处理每一个请求事件而创建一个新的子进程,与此同时,服务器进程会继续监听更多的 客户端连接请求。在一个大型的应用程序任务中,创建子进程通常会简化应用程序的设计,同时提高了系统 的并发性(即同时能够处理更多的任务或请求,多个进程在宏观上实现同时运行)。
理解 fork()系统调用的关键在于,完成对其调用后将存在两个进程,一个是原进程(父进程)、另一个 则是创建出来的子进程,并且每个进程都会从 fork()函数的返回处继续执行,会导致调用 fork()返回两次值, 子进程返回一个值、父进程返回一个值。在程序代码中,可通过返回值来区分是子进程还是父进程。 fork()调用成功后,将会在父进程中返回子进程的 PID,而在子进程中返回值是 0;如果调用失败,父进 程返回值-1,不创建子进程,并设置 errno。 fork()调用成功后,子进程和父进程会继续执行 fork()调用之后的指令,子进程、父进程各自在自己的进 程空间中运行。事实上,子进程是父进程的一个副本,譬如子进程拷贝了父进程的数据段、堆、栈以及继承 了父进程打开的文件描述符,父进程与子进程并不共享这些存储空间,这是子进程对父进程相应部分存储 空间的完全复制,执行 fork()之后,每个进程均可修改各自的栈数据以及堆段中的变量,而并不影响另一个进程。
2.2 使用示例 1使用 fork()创建子进程。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int pid = fork(); //父进程的返回值pid为子进程的pid号,子进程的返回值为0
switch (pid)
{
case -1:
printf("fork error\n");
break;
case 0:
printf("this is child ;PID = %d\n",getpid());
break;
default:
printf("this is father;PID = %d\n",getpid());
break;
}
return 0;
}
从打印结果可知,fork()之后的语句被执行了两次,所以 switch…case 语句被执行了两次
- 第一次进入 到了"case 0"分支,表示进入子进程
- 第二次进入到了 default 分支,表示当前处于父进程
父进程的返回值pid为子进程的id号,子进程的返回值为0
三 fork创建新进程时发生了什么事?
3.1 fork()函数之后,子进程会获得父进程所有文件描述符的副本
调用 fork()函数之后,子进程会获得父进程所有文件描述符的副本,这些副本的创建方式类似于 dup(), 这也意味着父、子进程对应的文件描述符均指向相同的文件表,如下图所示:
由此可知,子进程拷贝了父进程的文件描述符表,使得父、子进程中对应的文件描述符指向了相同的文件表,也意味着父、子进程中对应的文件描述符指向了磁盘中相同的文件,因而这些文件在父、子进程间实 现了共享,譬如,如果子进程更新了文件偏移量,那么这个改变也会影响到父进程中相应文件描述符的位置 偏移量。
3.2父、子进程中对应的文件描述符指向了相同的文件表 ——代码测试
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define MY_FILE "./file.txt"
int main()
{
char buf [128];
int fd = open(MY_FILE,O_RDWR);
if(-1 == fd)
{
perror("open error? :");
exit(-1);
}
int pid = fork();
switch (pid)
{
case -1:
printf("fork error\n");
break;
case 0:
for(int i = 0;i<4;i++)
{
write(fd," child",strlen(" child"));
}
printf("子进程\n");
close(fd);
break;
default:
printf("父进程\n");
for(int i = 0;i<4;i++)
{
write(fd," father",strlen(" father"));
}
close(fd);
break;
}
read(fd,buf,128);
printf("read :%s\n",buf);
return 0;
}
上述代码中,父进程 open 打开文件之后,才调用 fork()创建了子进程,所以子进程了继承了父进程打 开的文件描述符 fd,我们需要验证的便是两个进程对文件的写入操作是分别各自写入、还是每次都在文件 末尾接续写入
有上述测试结果可知,此种情况下,父、子进程分别对同一个文件进行写入操作,结果是接续写,不管 是父进程,还是子进程,在每次写入时都是从文件的末尾写入,很像使用了 O_APPEND 标志的效果。其原 因也非常简单,子进程继承了父进程的文件描述符,两个文件描述符都指向了一个相同的文件表,意味着它们的文件偏移量是同一个、绑定在了一起,相互影响,子进程改变了文件的位置 偏移量就会作用到父进程,同理,父进程改变了文件的位置偏移量就会作用到子进程
但是执行结果不一定是父进程先执行或者子进程先执行,这个不一定的,这个要看系统的调度了
四 linux 全拷贝和写实拷贝
4.1全拷贝:
传统的fork ()系统调用直接把所有的资源复制给新创建的进程。 这种实现过于简单并且效率低下,因为它拷贝的数据也许并不共享,更糟的情况是,如果新进程打算立即执行一个新的映像,那么所有的拷贝都将前功尽弃。
4.2写实拷贝:
Linux的fork ()使用写时拷贝(copy-on-write)页实现。 写时拷贝是一种可以推迟甚至免除拷贝数据的技术。 内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。 只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。
五 vfork()函数
5.1 vfork函数和fork函数的区别
- 关键区别一:vfork直接使用父进程存储空间,不拷贝
- 关键区别二:vfork保证子进程先运行,当子进程调用exit退出时,父进程才开始执行
5.2 vfork代码测试:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int data =10; //测试数据
int pid = vfork();
switch (pid)
{
case -1:
printf("fork error\n");
break;
case 0:
printf("this is child ;PID = %d\n",getpid());
printf("pid = %d\n",pid);
data =data + 100;
exit(0);
break;
default:
printf("this is father;PID = %d\n",getpid());
printf("pid = %d\n",pid);
printf("data = %d \n",data);
break;
}
return 0;
}
根据上述代码,man函数进来,创建一个int型的变量data,在子进程加100,在父进程中打印,最data为110,证明公用了一块地址
5.3fork()代码测试
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int data = 10;
int pid = fork();
switch (pid)
{
case -1:
printf("fork error\n");
break;
case 0:
printf("this is child ;PID = %d\n",getpid());
printf("pid = %d\n",pid);
data += 100;
printf("子进程的data = %d\n",data);
break;
default:
printf("this is father;PID = %d\n",getpid());
printf("pid = %d\n",pid);
printf("父进程的data = %d\n",data);
break;
}
return 0;
}
而fork()代码中,data在子进程中进行自加100,在打印结果就是子进程打印的data就是100,而父进程的data为10,没有改变,因为在父子进程在写入新的数据的时候,数据会被复制,从而使各个进程拥有各自的拷贝。