进程标识符
每个进程都有一个非负整型表示的唯一的进程 ID。虽然是唯一的,但是进程ID可以重用,当一个进程终止后,其进程ID就可以再次使用了。ID为0的进程通常是调度进程,常常被称为交换进程,该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为系统进程。进程ID为1 通常就是init 进程,在自举过程结束后由内核调用,这个进程我们在后面还会提到。我们通常用下面这些函数返回进程的一些标识符
#include<unistd.h>
pid_t getpid(void);
//返回值:调用进程的进程ID
pid_t getppid(void);
//返回值:调用进程的父进程ID
下面我们开始学习进程控制中的一些重要函数,先介绍fork:
fork 函数
一个现有进程可以调用 fork 函数创建一个新进程
#include<unistd.h>
pid_t fork(void);
//返回值:子进程中返回0,父进程中返回子进程ID,出错返回-1
这个函数有点意思,fork 调用一次,但返回两次。两次返回的唯一区别是子进程的返回值是0,而父进程的返回值则是新进程的进程ID。理由很简单,因为一个进程的子进程可以有很多个,没有函数可以使一个进程获得其所有子进程的进程ID,但一个进程只会有一个父进程,并且子进程可以调用 getppid 以获得其父进程的进程ID。
fork 实现:1. 为子进程分配内存空间,2.将父进程空间的全部内容复制到分配给子进程的内存空间,3.在内核数据结构中创建并正确初始化子进程的进程控制块PCB,4.善后代码。
由于子进程的进程控制块PCB已经产生,子进程已经出生,因此子进程可以被OS调度,子进程首次被调度时,执行的第1条代码在fork 内部,不过从应用程序的角度看,执行的第1条代码是从fork返回,所以fork被调用1次,却返回了2次。
一个fork 函数背后就包含了这一系列复杂的操作。进程创建之后,子进程和父进程将继续执行fork 调用之后的指令。子进程是父进程的副本,子进程会获得父进程数据空间、堆和栈的副本。父、子进程并不共享这些存储空间部分。父、子进程共享正文段(由CPU执行的机器指令部分)。
fork 之后,前后两个进程各自有自己的地址空间,形式上有点像把一个文件拷贝了一个副本。虽然资源也相互独立,但拷贝时父进程执行过程已生成的数据,子进程也拷了一份。简单说来就是一个执行到半路的程序突然多出个孪生兄弟,什么都跟自己一样,但要管自己叫老爸。当然这不是最终目的,最终目的是子进程去执行另外程序(exec)我们后面讨论。
看代码:
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#define BUFFSIZE 1024
int main(void)
{
int fd;
int var = 10;
pid_t pid;
char buf[BUFFSIZE];
// char data[] = "sunburn";
fd = open("examplefiles",O_RDWR);
printf("before fork\n");
if((pid = fork()) < 0)
perror("fork error\n");
else if(0 == pid)//child
{
var++;
read(fd, buf, 10);
write(STDOUT_FILENO, buf, 10);
write(STDOUT_FILENO, "\n", 1);
}
else//parent
{
sleep(2);
lseek(fd, -5, SEEK_CUR);
read(fd, buf, 10);
write(STDOUT_FILENO, buf, 10);
write(STDOUT_FILENO, "\n", 1);
}
printf("pid = %d, ppid = %d, var = %d\n", getpid(), getppid(), var);
return 0;
}
上面“examplefiles”的文件内容是 abcdefghijklmnopqrstuvwxyz。
fork 会导致子进程继承父进程打开的文件描述符,其本质是将父进程的整个文件描述符表复制一份,放到子进程的PCB中。因此父、子进程中相同文件描述符整数指向的是同一个文件表元素,这将导致父(子)进程读取文件后,子(父)进程将读取同一文件的后续内容。
所以上面的输出结果就是:
这里调整了文件的偏移量。
上面是父子进程之间有的关系。如果两个进程独立调用open 去打开同一个物理文件,就会有两个文件表元素被创建,并且它们杜志祥同一个 i 节点表元素。前面博文有说到文件表元素中有自己独立的current file offset 字段,这将导致两个独立进程之间读取同一文件,其偏移量不受影响。
子进程产生之后,父、子进程谁先运行,取决于OS调度策略。
wait 和 waitpid 函数
上面的程序中,fork 克隆出子进程后,为了保证子进程由于父进程运行,我们在父进程中使用了 sleep(2) 的方式让父进程睡眠了2s,但实际上这样做也不能100% 的确定子进程会先于父进程运行,在负荷非常重的系统中,父进程睡眠器件,OS并没有调度到子进程,当父进程睡醒后,首先调度到父进程运行。
这里我们可以调用wait(waitpid)函数来保证父、子进程完全按照程序员的安排来进行同步。
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
//参数:statloc 用于存放进程结束状态
//两个函数返回值:若成功则返回进程ID,出错则返回-1
这两个函数的区别在于:在一个子进程终止前,wait 使其调用者阻塞,而waitpid有一个选项,可是调用者不阻塞。
调用wait 或 waitpid 的进程可能会发生以下情况:
- 如果其所有子进程都还在运行,则阻塞
- 如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
- 如果它没有任何子进程,则立即出错返回。
如果一个子进程已经终止,并且是一个僵死进程,则wait 立即返回并取得该子进程的状态,否则wait 使其调用者阻塞直到一个子进程终止。如调用者阻塞并且它有多个子进程,则在其一个子进程终止时,wait 就立即返回,而不是等其所有的子进程终止后才返回。
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <wait.h>
#define BUFFSIZE 1024
int main(void)
{
int fd;
int status;
int var = 10;
pid_t pid;
char buf[BUFFSIZE];
// char data[] = "sunburn";
fd = open("examplefiles",O_RDWR);
printf("before fork\n");
if((pid = fork()) < 0)
perror("fork error\n");
else if(0 == pid)//child
{
//sleep(1);//
//var++;
printf("son process\n");
}
else//parent
{
//abort();//父进程异常终止
wait(&status);
printf("parent process\n");
}
// printf("pid = %d, ppid = %d, var = %d\n", getpid(), getppid(), var);
return 0;
}
父进程调用wait函数,将会阻塞,直到其中的一个子进程终止,才会执行父进程。调用waitpid 函数则可以等待一个指定的进程终止。
子进程产生之后,父、子进程终止的先后顺序有很大的区别:1.父进程在子进程之前终止。对于父进程已经终止的所有进程,他们的父进程都改变为 init 进程(PID = 1),成为由init 进程领养。其操作过程大致如下:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止进程的子进程,如果是,则将该进程的父进程ID更改为1.这个处理方式保证了每个进程都有一个父进程。2.子进程在父进程之前终止。内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用wait 或 waitpid 时,可以得到这些信息,内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。在UNIX中,我们把一个已经终止、但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它所占用的资源)的进程成为僵死进程,说白了,就是进程死了,没人给它收尸,占用了资源。
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <wait.h>
#define BUFFSIZE 1024
int main(void)
{
int fd;
int status;
int var = 10;
pid_t pid;
fd = open("examplefiles",O_RDWR);
printf("before fork\n");
if((pid = fork()) < 0)
perror("fork error\n");
else if(0 == pid)//child
{
sleep(1);//等待父进程终止,然后子进程被init进程领养
}
else//parent
{
abort();//父进程异常终止
}
printf("pid = %d, ppid = %d, var = %d\n", getpid(), getppid(), var);
return 0;
}
上面程序最后输出 ppid = 1,init 进程成为子进程的新父进程。而init 进程被编写成无论什么时候只要有一个子进程终止,init 进程就会调用一个wait 函数取得其终止状态,这样也就防止了在系统中有很多僵死进程。
参考资料《Unix 环境高级编程》