程序和进程
程序:是指编译好的二进制文件,在磁盘上,不占用系统资源(cpu、内存、打开的文件、设备、锁…)。
进程:是一个抽象的概念,与操作系统原理联系紧密,进程是活跃的程序,占用系统资源,在内存中执行(程序运行起来,产生一个进程)。
fork函数
如果我们想创建一个进程,我们可以执行一个可执行程序来创建进程,比如执行可执行程序 a.out
:
./a.out
除此之外,我么还可以用 fork
函数创建一个进程,其创建一个子进程。
# 创建一个子进程
pid_t fork(void);
# 失败返回-1
# 成功返回:
# 1. 父进程返回子进程的ID(非负)
# 2. 子进程返回 0
pid_t
类型表示进程 ID,但为了表示 -1,它是有符号整型。(0 不是有效进程ID, init最小,为1)
注意返回值,不是 fork
函数能返回两个值,而是 fork
后,fork
函数变为两个,父子需【各自】返回一个。
fork
失败可能有两种原因:
- 当前的进程数已经达到了系统规定的上限,这时
errno
的值被设置为EAGAIN
。 - 系统内存不足,这时
errno
的值被设置为ENOMEM
。
创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过 getpid()
函数获得,还有一个记录父进程 pid
的变量,可以通过 getppid()
函数获得变量的值。
注意此时子进程会和父进程一样继续执行后面代码,下面用一个实例进行演示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
pid_t pid;
printf("pid = %u, xxxxxxxxx\n", getpid());
pid = fork();
if (pid == -1) {
perror("fork error:");
exit(1);
} else if (pid == 0) {
printf("I'm child, pid = %u, ppid = %u\n", getpid(), getppid());
} else {
printf("I'm parent, pid = %u, ppid = %u\n", getpid(), getppid());
sleep(1);
}
printf("pid = %u, YYYYYYYYYYYYY\n", getpid());
return 0;
}
可以看到父进程的 pid=5281
,且只有父进程执行了输出 xxxxxx
,因为这是在 fork()
执行之前的代码,所以子进程不会执行。子进程的 pid=5282
,其 ppid=5281
,且父进程和子进程都输出了 YYY
,这是因为这是 fork()
之后的代码,所以子进程也会执行。
循环创建子进程
如果我们想要循环创建 n 个子进程,是否只需要一个 for 循环即可,即如下代码:
int i;
pid_t pid;
for(i=0; i<n; ++i)
{
pid = fork();
}
答案是否定的,因为子进程会和父进程一样执行之后的代码,那么子进程也会产生子进程,所以进程数量呈指数级上升,上面的代码最后会生成 2n -1 个子进程。
正确循环创建 n(这里n=5) 个子进程的代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
int i;
pid_t pid;
printf("xxxxxxxxxxx\n");
for (i = 0; i < 5; i++) {
pid = fork();
if (pid == -1) {
perror("fork error:");
exit(1);
} else if (pid == 0) {
break;
}
}
if (i < 5) {
sleep(i);
printf("I am %d child, pid = %u\n", i+1, getpid());
} else {
sleep(i);
printf("I am parent\n");
}
return 0;
}
通过判断 fork
函数返回值是否为0,就能知道是否是子进程,是子进程就直接跳出循环,这样子进程就不会在创建子进程了。
父子进程共享
父子进程之间在 fork
后。有哪些相同,哪些相异之处呢?
刚 fork
之后:
- 父子相同处:全局变量、.data、.text、栈、堆、环境变量、用户 ID、宿主目录、进程工作目录、信号处理方式…
- 父子不同处:进程 ID、fork 返回值、父进程 ID、进程运行时间、闹钟(定时器)、未决信号集。
似乎,子进程复制了父进程 0-3G 用户空间内容,以及父进程的 PCB,但 pid 不同。真的每次 fork 一个子进程都要将父进程的 0-3G 地址空间完全拷贝一份,然后在映射至物理内存吗?
当然不是!父子进程间遵循读时共享写时复制的原则。即一个变量如果在子进程中只需要读,那就不需要复制。这样的话,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。
需要注意的是,父子进程还共享文件描述符(打开文件的结构体)与mmap 建立的映射区。
fork
之后父进程先执行还是子进程先执行不确定,取决于内核所使用的调度算法。
gdb调试
使用 gdb 调试代码时,gdb 只能跟踪一个进程。可以在 fork
函数调用之前,通过指令设置 gdb 调试工具跟踪父进程或者跟踪子进程。默认跟踪父进程。
set follow-fork-mode child
命令设置 gdb 在fork
之后跟踪子进程。set follow-fork-mode parent
设置跟踪父进程。
注意,一定要在 fork
函数调用之前设置才有效。
首先需要在编译时使用 -g
参数才能使用 gdb 调试:
不作设置,默认更新父进程:
因为默认更新父进程,所以 fork
之后的子进程直接输出。
如果要追踪子进程的话,就在该子进程 fork
之前输入 set follow-fork-mode child
。
这时对于父进程会直接执行完,然后 n
指令继续执行子进程。