UNIX再学习 -- 函数 fork 和 vfork

一、进程标识

每个进程都有一个非负整数形式的唯一编号,即 PID。PID 在任何时刻都是唯一的,但是可以重用,当进程终止并被回收以后,其 PID 就可以为其它进程所用。进程的 PID 由系统内核根据延迟重用算法生成,以确保新进程的 PID 不同于最近终止进程的 PID。 

1、系统中有些 PID 是专用的

(1)0 号进程,调度进程

通常是调度进程,常常被称为交换进程(swapper)。该进程是内核的一部分,所有进程的根进程,它并不执行任何磁盘上的程序,因此也被称为系统进程。

(2)1 号进程,init进程

通常是 init 进程,在自举过程结束时由内核调用。该进程的程序文件在 UNIX 的早起版本中是 /etc/init,在较新版本中是 /sbin/init。此进程负责在自举内核后启动一个 UNIX 系统。init 通常读取与系统有关的初始化文件(/etc/rc*文件或 /etc/inittab 文件,以及在 /etc/init.d 中的文件),并将系统引导到一个状态(如多用户)init 进程决不会终止。它是一个普通的用户进程(与交换进程不同,它不是内核中的系统进程),但是它以超级用户特权运行

(3)2号进程,页守护进程

负责虚拟内存系统的分页操作

(4)其他

除调度进程以外,系统中的每个进程都有唯一的父进程,对于一个子进程而言,其父进程的 PID 即是它的 PPID。
进程 0 是系统内部的进程,它负责启动进程 1 (inti),也会启动进程 2 ,而其他所有的进程都是进程 1 / 进程 2 直接/间接 地启动起来。 

2、获取进程 ID 函数

#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); 返回:调用进程的进程 ID
pid_t getppid(void); 返回:调用进程的父进程 ID
uid_t getuid(void); 返回:调用进程的实际用户 ID
uid_t geteuid(void); 返回:调用进程的有效用户 ID
gid_t getgid(void); 返回:调用进程的实际组 ID
gid_t getegid(void); 返回:调用进程的有效组 ID
注意,这些函数都没有出错返回

(1)示例说明

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main (void)
{
	printf ("pid = %d\n", getpid ());
	printf ("ppid = %d\n", getppid ());
	printf ("uid = %d\n", getuid ());
	printf ("euid = %d\n", geteuid ());
	printf ("gid = %d\n", getgid ());
	printf ("egid = %d\n", getegid ());
	return 0;
}
输出结果:
pid = 3028
ppid = 2808
uid = 0
euid = 0
gid = 0
egid = 0
//每次执行结果都不一定相同

二、函数 fork

#include <unistd.h>
pid_t fork(void);

1、函数功能

主要用于以复制正在调用进程的方式去创建一个新的进程,新进程叫做子进程,原来的进程叫做父进程,

2、返回值

成功时父进程返回子进程的 ID,子进程返回 0,失败返回 -1

3、创建新进程

由 fork 创建的新进程被称为子进程。fork 函数调用一次,但返回两次。
两次返回的区别是:子进程的返回值是 0,而父进程的返回值则是新建子进程的进程 ID
将新建子进程 ID 返回给父进程的理由:因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的 ID。
fork 使子进程得到返回值 0 的理由:一个进程只会有一个父进程,所以子进程总是可以调用 getppid 以获得其父进程的进程 ID(进程 ID 0 总是由内核交换进程使用,所以一个子进程的进程 ID 不可能为 0)。
子进程和父进程继续执行 fork 调用之后的指令。子进程是父进程的不完全副本。子进程的数据区、bbs区、堆栈区(包括 I/O 流缓冲区),甚至参数和环境区都从父进程拷贝,唯有代码区与父进程共享。

(1)示例说明:

参看:fork()函数详解

1》》创建新进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int n = 10;  //数据段
int m;  //bbs段
const int i = 10;  //代码段
int main (void)
{
	pid_t pid;
	int cnt = 0; //栈区

	pid = fork ();
	if  (pid == -1)
		perror ("fail to fork"), exit (1);
	else if (pid == 0)
	{
		printf("The returned value is %d\nIn child process!!\nMy PID is %d\n",
				pid, getpid());
		cnt++;
		n++;
		m++;

	}
	else
	{
		sleep (3); //可以保证子进程先被调度
		printf("The returned value is %d\nIn father process!!\nMy PID is %d\n",
				 pid, getpid());
		cnt++;
		n++;
		m++;
	}

	printf ("cnt = %d, n = %d, m = %d, i = %d\n", cnt, n, m, i);
	return 0;
}
输出结果:
The returned value is 0
In child process!!
My PID is 3010
cnt = 1, n = 11, m = 1, i = 10
The returned value is 3010
In father process!!
My PID is 3009
cnt = 1, n = 11, m = 1, i = 10
2》》示例解析
上例很好的说明了,fork 函数调用一次,返回两次。在子进程中返回 0, 父进程中返回子进程 ID,错误返回 -1。
子进程是父进程的不完全副本。子进程的数据区、bbs区、堆栈区(包括 I/O 流缓冲区),甚至参数和环境区都从父进程拷贝唯有代码区与父进程共享。  
因为,代码区是可执行指令 、字面值常量 、具有常属性且被 初始化的全局、静态全局 和 静态局部变量。
再有我在父进程使用了 sleep (3); 来确保子进程先调度(但是有时不一定保证 3 秒已经足够)。因为,一般来说,在 fork 之后是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法。如果要求父进程和子进程之间相互同步,则要求某种形式的进程间通信,后续会讲这些。

4、文件共享

上例中如果将可执行文件重定向:
# ./a.out > test.out
# cat test.out 
The returned value is 0
In child process!!
My PID is 3047
cnt = 1, n = 11, m = 1, i = 10
The returned value is 3047
In father process!!
My PID is 3046
cnt = 1, n = 11, m = 1, i = 10
可发现,在重定向父进程的标准输出时,子进程的标准输出也被重定向了。
实际上,fork 的一个特性是进程的所有打开文件描述符都被复制到子进程中。我们说的“复制”是因为对每个文件描述符来说,就好像执行了 dup 函数。父进程和子进程每个相同的打开描述符共享一个文件表项
重要的一点是,父进程和子进程共性同一个文件偏移量。


(1)示例说明

1》》父子进程共享文件表
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <stdbool.h>

#define BUFSIZE 5*5

//定义函数lock设置写锁
bool lock(int fd)
{
	struct flock lock;
	lock.l_type = F_WRLCK;
    lock.l_whence = SEEK_SET;
	lock.l_start = 0;
	lock.l_len = 0;
    lock.l_pid = -1;
    if (fcntl (fd, F_SETLK, &lock) == -1)
	{
        if (errno != EAGAIN)
		{
            perror ("fcntl");
			exit (EXIT_FAILURE);
        }
		return false;
	}
	return true;
}

//定义函数unlock解除写锁
void unlock(int fd)
{
    struct flock lock;
	lock.l_type = F_UNLCK;
	lock.l_whence = SEEK_SET;
	lock.l_start = 0;
	lock.l_len = 0;
	lock.l_pid = -1;
	if (fcntl (fd, F_SETLKW, &lock) == -1)
	{
		perror ("fcntl");
		exit (EXIT_FAILURE);
   }
}

void writedata(int fd, char *buf, char c)
{
	int i = 0;
	//设置写锁,防止子进程和父进程同时写入造成数据混乱
	while(!lock(fd));
	//将缓冲区用要写入的字符填充。
	for (i = 0; i < BUFSIZE - 1; i++)
		buf[i] = c;
	for (i = 0; i < 5; i++)
	{
		int writed;
		//向文件fd中写入字符。
		if ((writed = write (fd, buf + i, 1)) == -1)
        {
			perror ("write");
            exit (EXIT_FAILURE);
        }
		printf("111111111->%c,buf[0]=%c, writed = %d\n", c, *(buf + i), writed);
	}
	//解除写锁
	unlock(fd);
}

int main()
{
	int fd = open ("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
	if (fd == -1)
	{
		perror ("open");
		exit (EXIT_FAILURE);
	}

	pid_t pid;
	if ((pid = fork()) < 0)
	{
		perror("fork");
		return 1;
	}
	//创建子进程
	if (pid == 0)
	{
		char buf[BUFSIZE] = {};
		writedata(fd, buf, '0'); //向 fd 写入'0'
	}
	//父进程
	else
	{
		sleep (3);
		char buf[BUFSIZE] = {};
		writedata(fd, buf, '1'); //向 fd 写入 '1'
	}

    if (close (fd) == -1)
	{
        perror ("close");
        exit (EXIT_FAILURE);
	}

	return 0;
}
输出结果:
111111111->0,buf[0]=0, writed = 1
111111111->0,buf[0]=0, writed = 1
111111111->0,buf[0]=0, writed = 1
111111111->0,buf[0]=0, writed = 1
111111111->0,buf[0]=0, writed = 1
111111111->1,buf[0]=1, writed = 1
111111111->1,buf[0]=1, writed = 1
111111111->1,buf[0]=1, writed = 1
111111111->1,buf[0]=1, writed = 1
111111111->1,buf[0]=1, writed = 1

查看 data.txt
# cat data.txt 
0000011111
2》》示例解析
在子进程和父进程中分别使用 writedata 函数向文件 fd 中写入字符 0 和字符 1。由于 fork 函数成功返回以后,系统内核为父进程维护的文件描述符表也被复制到子进程的进程表项中文件表项并不复制所以子进程和父进程写入同一文件

(2)在 fork 之后处理文件描述符有以下两种常用的操作模式 

1》》父进程等待子进程完成。 (重点)
在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏移量已做了相应更新。
上例,就是用的这种方法,子进程写 '0',文件偏移量做相应更新,父进程在其后面继续写入 '1'
2》》父进程和子进程各自执行不同的程序段。
在这种情况下,在 fork 之后,父进程和子进程各自关闭它们不需使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程经常使用的。

5、进阶

(1)fork 失败原因

1》》当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
2》》系统内存不足,这时errno的值被设置为ENOMEM。
例如:
俗称 fork 炸弹
while(1)
{
    fork;  
}
失败后效果:内存耗尽,系统死机无法操作。
参看:ulimit 命令    
系统总线程数达到上限
# cat /proc/sys/kernel/threads-max
15785

用户总进程数达到上限
# ulimit -u
7892
(2)并发运行
一个进程如果希望创建自己的副本并执行一份代码,或希望与另一个进程并发运行,都可以使用 fork 函数。
(3)执行代码
调用 fork 函数前的代码只有父进程执行,fork 函数成功返回后的代码父子进程都会执行,受逻辑控制进入不同分支。
调用fork函数前的代码只有父进程执行

pit_t pid = fork ();
if (pid == -1)
	perror ("fork"), exit (1);
if (pid == 0)
	子进程执行的代码
else
	父进程执行的代码

父子进程都执行的代码
(4)除了打开文件之外,父进程的很多其他属性也由子进程继承,包括:
实际用户ID、实际组ID、有效用户ID、有效组ID。
添加组ID。
进程组ID。
对话期ID。
控制终端。
设置用户ID标志和设置组ID标志。
当前工作目录。
根目录。
文件方式创建屏蔽字。
信号屏蔽和排列。
对任一打开文件描述符的在执行时关闭标志。
环境。
连接的共享存储段。
资源限制。

6、孤儿和僵尸

(1)父子进程

1》》父进程和子进程的关系
UNIX/Linux 系统中的进程存在父子关系,一个父进程可以创建多个子进程,但每个子进程最多只能有一个父进程。整个系统中只有一个根进程,即 PID 为 0  的调度进程。系统中的所有进程构成了一棵以调度进程为根的进程树。

2》》父进程和子进程之间的区别
fork 的返回值不同,子进程返回 0, 而父进程返回新建子进程 ID。
进程 ID 不同
这两个进程的父进程 ID 不同:子进程的父进程 ID 是创建它的进程的 ID,而父进程的父进程 ID 则不变
子进程的tms_utime , tms_stime , tms_cutime以及tms_ustime设置为0。
子进程不继承父进程设置的文件锁
子进程的未处理闹钟被清除。
子进程的未处理信号集设置为空集。

(2)孤儿进程

父进程创建子进程以后,子进程在操作系统的调度下与其父进程同时运行。
如果父进程先于子进程终止,子进程即成为孤儿进程,同时被 init 进程收养,即成为 init 进程的子进程,因此 inti 进程又被成为孤儿院进程。
一个进程成为孤儿进程是正常的,系统中的很多守护进程都是孤儿进程。
1》》示例说明
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main (void)
{
	pid_t pid;
	if ((pid = fork ()) < 0)
		perror ("fork"), exit (1);
	else if (pid == 0)
	{
		sleep (3); 
		printf ("这是子进程 pid = %d", getpid ());
		printf ("父进程的 ppid = %d\n",  getppid ());
	}
	else 
	{
		printf ("这是父进程 ppid = %d\n", getpid ());
	}
	return 0;
}
输出结果:
这是子进程 pid = 2430父进程的 ppid = 2331
这是子进程 pid = 2431父进程的 ppid = 1
2》》示例解析
子进程被暂停 3 秒,所以当父进程退出时,子进程仍然未退出,这样子进程即成为孤儿进程。根据输出结果可知,当暂停 3 秒结束时,子进程的父进程变成了 1即 init 进程,又被称为孤儿进程。

(3)僵尸进程

如果子进程先于父进程终止,但父进程由于某种原因,没有回收子进程的退出状态,子进程即成为僵尸进程。
僵尸进程虽然已经不再活动,但其终止状态仍然保留,也会占用系统资源,直到被其父进程回收才得以释放。
如果父进程直到终止都未回收它的已成僵尸的子进程,init 进程会立即收养并回收这些处于僵尸状态的子进程,因此一个进程不可能既是孤儿进程同时又是僵尸进程。
一个进程成为僵尸进程需要引起注意,如果它的父进程长期运行而不终止,僵尸进程所占用的资源将长期得不到释放
1》》示例说明
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>


int main (void)
{
	pid_t pid;

	pid = fork ();
	if  (pid == -1)
		perror ("fail to fork"), exit (1);
	else if (pid == 0)
	{
		printf ("这是子进程 pid = %d", getpid ());
		printf ("父进程的 ppid = %d\n",  getppid ());
	}
	else
	{
		//while (1);
		sleep (10); 
		printf ("这是父进程 ppid = %d\n", getpid ());

	}

	return 0;
}
输出结果:
这是子进程 pid = 2652父进程的 ppid = 2651
这是父进程 ppid = 2651
2》》示例解析
函数sleep的作用是让父进程休眠指定的秒数,在这10秒内,子进程已经退出,而父进程正忙着睡觉,不可能对它进行收集,这样,我们就能保持子进程 10秒 的僵尸状态。

三、函数 vfork

#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);

1、函数功能:

创建轻量级子进程,vfork 与 fork 的功能基本相同。

2、两者区别:

有以下两点区别:第一,vfork 函数创建的子进程不复制父进程的物理内存,也不拥有自己独立的内存映射,而是与父进程共享全部地址空间;第二,vfork 函数会在创建子进程的同时挂起其父进程,直到子进程终止,或通过 exec 函数启动了另一个可执行程序

3、函数典型用法:

终止 vfork 函数创建的子进程,不要使用 return 语句,也不要调用 exit 函数,而要调用 _exit 函数,以避免对其父进程造成不利影响。
vfork 函数的典型用法就是在所创建的子进程里直接调用 exec 函数启动另外一个进程取代其自身,这比调用 fork 函数完成同样的工作要快得多。
pid_t pid  = vfork ();
if (pid == -1)
	perror ("vfork"), exit (1);
if (pid == 0)
	if (execl ("ls", "ls", "-l", NULL) == -1)
		perror ("execl"), _exit (1);

4、写时复制:

传统意义上的 fork 系统调用,必须把父进程地址空间中的内容一页一页地复制到子进程的地址空间中(代码区除外)。这无疑是十分漫长的过程(在系统内核看来)。
而多数情况下的子进程其实只是想读一读父进程的数据,并不想改变什么。更有甚者,可能连读一读都觉得多余,比如直接通过 exec 函数启动另一个进程的情况。漫长的内核复制在这里显得笨拙毫无意义。
写时复制以惰性优化的方式避免了内存复制所带来的系统开销。在子进程创建伊始,并不复制父进程的物理内存,只复制它的内存映射表即可,父子进程共享同一个地址空间,直到子进程需要写这些数据时,再复制内存亦不为迟。
写时复制带来的好处是,子进程什么时候写就什么时候复制,写几页就复制几页,没有写的就不复制。惰性优化算法的核心思想就是尽一切可能将代价高昂的操作,推迟到非做不可的时候再做,而且最好局限在尽可能小的范围里。
现代版本的 fork 函数已经广泛采用了写时复制技术,从这个意义上讲,vfork 函数的存在纯粹只是一个历史遗留的产物,尽管它的速度还是比 fork 要快一点(连内存映射表都不复制),但它的地位已远不如写时复制技术被应用到 fork 函数的实现中以前那么重要了。

5、示例说明

//示例一
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main(void)
{
	//使用vfork函数创建子进程
	pid_t pid = vfork();
	if(-1 == pid)
	{
		perror("vfork"),exit(-1);
	}
	if(0 == pid) //子进程
	{
		printf("子进程%d开始运行\n",getpid());
		sleep(3);
		printf("子进程结束\n");
		//子进程不退出,则结果不可预知
		_exit(0);//终止子进程
	}
	printf("父进程%d开始执行\n",getpid());
	printf("父进程结束\n");
	return 0;
}
输出结果:
子进程2762开始运行
子进程结束
父进程2761开始执行
父进程结束
//示例二
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main (void)
{
	printf ("父进程开始执行\n");

	pid_t pid = vfork ();
	if (pid == -1)
		perror ("vfork"), exit (1);
	if (pid == 0)
	{
		printf ("子进程开始执行\n");
		if (execl ("/bin/ls", "ls", "-l", NULL) == -1)
			perror ("execl"), _exit (1);
	}
	sleep (1);
	printf ("父进程执行结束\n");
	return 0;
}
输出结果:
父进程开始执行
子进程开始执行
总用量 16
-rwxr-xr-x 1 root root 7380 Apr 20 10:22 a.out
-rw-r--r-- 1 root root  383 Apr 20 10:22 test.c
-rw-r--r-- 1 root root  151 Apr 20 09:56 test.c~
父进程执行结束


  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

聚优致成

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值