一、什么是进程
进程是正在执行的程序实例。执行程序时,内核会将程序代码载入虚拟内存,为程序变量分配空间,在内核中建立相应的数据结构,以记录与进程有关的各种信息(比如,进程ID、用户ID、组ID以及终止状态等)。
简单来说,就是”执行一个程序或命令“就可以出发一个事件而获取一个进程ID。也就是说,程序被触发后,执行者的权限与属性、程序代码与数据等会被加载到内存,操作系统并给予这个内存单元一个标识符(进程ID)。
进程可以使用系统调用fork()来创建一个子进程。子进程获得父进程的数据空间、堆和栈的副本。父进程和子进程并不共享这些存储空间部分,共享正文段,也就是在内存中被标记为只读的程序文本段。例如在Linux shell中键入命令,ps时,shell会创建一个进程,这个子进程执行ps。
kernel@Ubuntu:~/Desktop$ ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 17637 17627 3 80 0 - 6909 wait pts/1 00:00:00 bash
0 R 1000 17650 17637 0 80 0 - 3554 - pts/1 00:00:00 ps
二、系统调用fork()
1. fork()函数被调用一次,但有两个返回值。父进程返回新创建的子进程ID,子进程返回0。此时的0不是进程ID为0的进程(ID为0的进程通常是调度进程,是内核的一部分,它并不执行任何磁盘上的进程,被称为系统进程)。
实例:
#include <unistd.h>
#include <sys/types.h>
#include "err.h"
void err_sys(const char * fmt, ...) __attribute((noreturn));
int globvar = 5;
char buf[] = "global String\n";
int main(void) {
int var;
pid_t pid;
var = 28;
if (write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) - 1) /*将buf缓冲区的字符由标准输出输出到终端*/
err_sys("write error");
printf("before fork");
if ((pid = fork()) < 0) /*创建子进程*/
err_sys("fork erro");
if (!pid) { /*执行子进程*/
globvar++;
var++;
}
else
sleep(2); /*父进程等待子进程执行完成*/
printf("pid = %d, ppid = %d, var = %d, globlvar = %d\n", getpid(), getppid(), var, globvar);
exit(0);
}
结果如下:
kernel@Ubuntu:~/Desktop$ gcc -g fork.c -o fork
kernel@Ubuntu:~/Desktop$ ./fork
global String
before fork
pid = 21398, ppid = 21397, var = 29, globlvar = 6
pid = 21397, ppid = 20518, var = 28, globlvar = 5
2.由结果可以看出,子进程改变了全局变量globvar和局部变量var的值,但对于父进程来说,却是没有改变的。所以说子进程是父进程创建的副本,它们并不共享存储空间的部分。而且在调用fork()之前,有”beforefork” 输出,而在创建子进程之后,子进程并没有输出该字符串。因为printf()是标准I/O函数,而且输出流是连接到终端,通常使用行缓冲。遇到换行符标准I/O执行I/O操作,将缓冲区的内容写到终端,并清理缓冲区。但是将标准输出重定向到一个文件时,在执行fork函数时,该行数据还在缓冲区,然后父进程将数据复制到子进程,该缓冲区的数据也被复制到子进程。
kernel@Ubuntu:~/Desktop$ ./fork > file
kernel@Ubuntu:~/Desktop$ cat file
global String
before fork
pid = 21493, ppid = 21492, var = 29, globlvar = 6
before fork
pid = 21492, ppid = 20518, var = 28, globlvar = 5
因为write是不带缓冲的,其数据只写到标准输出一次,所以子进程数据空间并没有write写入的数据。
3.在fork之后,父进程和子进程的执行顺序是不确定的,这取决于内核的调度算法。因此在pid> 0,父进程执行是休眠2s,让pid == 0时,子进程执行完毕后,再执行父进程。
还有一种情况,fork创建子进程,子进程通过调用exec的一系列函数,执行另一部分程序。
4.fork之后处理文件有两种方式
(1)父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾读、写操作的任一共享描述符的文件偏移量已经做了相应的更新。
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include "err.h"
void err_sys(const char * fmt, ...) __attribute__((noreturn));
int main(void) {
pid_t pid;
off_t currpos;
char buf1[] = "testing the lseek\n";
char buf2[] = "child add the lseek\n";
int fd;
if ((fd = open("/home/kernel/Desktop/testfile", O_RDWR | O_CREAT | O_TRUNC, )) < 0) /* 读、写创建或打开文件 */
err_sys("open error");
if (write(fd, buf1, sizeof(buf1) - 1) != sizeof(buf1) - 1)
err_sys("write error");
currpos = lseek(fd, 0, SEEK_CUR); /* 当前文件偏移量 */
printf("before fork the lseek is %ld\n", currpos);
if ((pid = fork()) < 0)
err_sys("fork error");
if (!pid) {
if (write(fd, buf2, sizeof(buf2) - 1) != sizeof(buf2) - 1)
err_sys("write error");
currpos = lseek(fd, 0, SEEK_CUR); /*子进程文件偏移量 */
printf("now child lseek is %ld\n", currpos);
}
else {
sleep(2);
currpos = lseek(fd, 0, SEEK_CUR); /* 父进程文件偏移量 */
printf("now parent lseek is %ld\n", currpos);
}
exit(0);
}
结果如下:
kernel@Ubuntu:~/Desktop$ gcc -g fork1.c -o fork1
kernel@Ubuntu:~/Desktop$ ./fork1
before fork the lseek is 18
now child lseek is 38
now parent lseek is 38
(2)父进程和子进程各自执行不同的程序段。在这种情况下,在fork之后,父进程和子进程各自关闭它们不需使用的文件描述符,就这样就不会干扰对方使用的描述符,这种方法是网络服务进程经常使用的。
5.现在可以看看父进程和子进程再去创建各自的子进程是什么情况。
#include <unistd.h>
#include <sys/types.h>
#include "err.h"
void err_sys(const char * fmt, ...) __attribute__((noreturn));
int main(void) {
pid_t pid;
int i;
for (i = 0; i < 2; i++) {
if ((pid = fork()) < 0)
err_sys("fork error");
if (!pid)
printf("i = %d\tChild\tpid = %d\tppid = %d\n", i, getpid(), getppid());
else
printf("i = %d\tParent\tpid = %d\tppid = %d\n", i, getpid(), getppid());
}
exit(0);
}
结果如下:
i = 0 Parent pid = 14972 ppid = 12028
i = 0 Child pid = 14973 ppid = 14972
i = 1 Parent pid = 14972 ppid = 12028
i = 1 Parent pid = 14973 ppid = 14972
i = 1 Child pid = 14974 ppid = 14972
i = 1 Child pid = 14975 ppid = 14973
结论:当 i = 0,parent0创建第一个子进程child0。i = 1,parent0与child0会创建各自的子进程child1与child2,此时child0也是一个父进程parent1,所以parent0与parent1会输出。i = 2时,parent0,child0/parent1,child1,child2结束循环并退出。
三、总结
在使用fork函数时,子进程和父进程的执行顺序不定,有可能会出现竞争,有内核的调度算法实现。如果子进程比父进程先结束,并且父进程没有对子进程的资源进行回收,所以会产生僵死进程,并且kill其父进程,僵死进程也可能会还会存在。所以父进程应使用wait或waipid函数获取子进程相关信息,内核可以释放子进程占用的存储区,关闭打开的文件。如果父进程先于子进程结束,init进程会成为子进程的父进程,有init进程收养。
在循环中使用fork时,父进程创建子进程之后,下一次循环父进程还会继续执行与刚才相同的操作,且产生的子进程与上一次产生的子进程属于同一个父进程。上一次产生的子进程也会和父进程在下一次循环产生自己的子进程,直到循环结束。