一、进程创建
Unix的进程创建很特别。许多其他的操作系统都提供了产生(spawn) 进程的机制,首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。Unix 采用了与众不同的实现方式,它把上述步骤分解到两个单独的函数中去执行: fork()和exec系列函数。首先,fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID (每个进程唯一)、 PPID (父进程的进程号,子进程将其设置为被拷贝进程的PID)和某些资源和统计量(例如,挂起的信号,它没有必要被继承)。exec系列函数( 函数负责读取可执行文件并将其载入地址空间开始运行。把这两个函数组合起来使用的效果跟其他系统使用的单-一函数的效果相似。
在Linux系统中,除了系统启动之后的第一个进程由系统来创建,其余的进程都必须由已存在的进程来创建,新创建的进程叫做子进程,而创建子进程的进程叫做父进程。那个在系统启动及完成初始化之后,Linux自动创建的进程叫做根进程。根进程是Linux中所有进程的祖宗,其余进程都是根进程的子孙。具有同一个父进程的进程叫做兄弟进程。这样的模型就是树状结构。
Linux进程创建的如下所示:
fork()函数
fork()函数将运行着的程序分成2个(几乎)完全一样的进程, 每个进程都启动一个从代码的同一位置开始执行的线程。这两个进程中的线程继续执行,就像是两个用户同时启动了该应用程序的两个副本。一个进程,包括代码、数据和分配给进程的资源。fork() 函数通过系统调用创建一个与原来进程几乎完全相同的进程, 也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变不同,两个进程也可以做不同的事。
fork函数原型
功能:创建子进程
函数原型:pid_t fork(void);
返回值:成功:>=0 ; ==0 返回给子进程 >0 返回给父进程
失败:== -1
函数说明
由fork创建的新进程被称为子进程(child process)。 fork函数被调用一-次,但返回两次。两次返回的唯一区别是子进程的返回值是0,而父进程的返回值则是新子进程的进程ID。
将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程ID。
fork使子进程得到返回值0的理由是:一个进程只会有一个父进程, 所以子进程总是可以调用getppid以获得其父进程的进程ID (进程I 0总是由内核交换进程使用,所以一个子进程的进程ID不可能为0)。
子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父、子进程并不共享这些存储空间部分。
问题: 1.子进程到底是从fork这行代码开始运行,还是从fork之后的代码开始运行?为什么
从fork之后的代码开始运行
原因:父进程在执行完毕fork指令后,程序计数器保存的是fork之后的指令。子进程拷贝父进程的pcb,也将程序计数器拷贝了。
2.创建出来子进程之后,父子进程谁先运行?
不确定,是抢占式执行的。
代码验证一下fork相关结论
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
int main()
{
int n;
char *s = NULL;
pid_t res = fork();
if (res == -1)
{
printf("Creat process failed!\n");
return -1;
}
else if (res == 0)
{
n = 0;
s = "child";
}
else
{
n = 1;
s = "father";
}
printf("I'm %s process,PID = %d,PPID = %d,&n = %p,n = %d\n", s, getpid(), getppid(), &n, n);
sleep(1);
exit(0);
}
二、僵尸进程
什么是僵尸进程
一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程(也就是进程为中止状态,僵死状态)
但是如果该进程的父进程已经结束,那该进程就不会变成僵尸进程,因为每个进程结束的时候,系统会扫描所有运行中的进程,看看有没有哪个进程是刚刚结束的进程的子进程,如果有就由Init来接管他,成为他的父进程。
危害
UNIX提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息,就可以得到。 这种机制就是:在每个进程退出的时候内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一 定的信息(包括进程 号the process ID,退出状态the
termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait/waitpid来取时才释放。但这样就导致了问题,如果进程不调用wait/waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用, 但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。此即为僵尸进程的危害,应当避免。
模拟僵尸进程
子进程先于父进程退出,且父进程没有对子进程资源进行回收
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
int main()
{
pid_t res = fork();
if (res == -1)
{
printf("Creat process failed!\n");
return -1;
}
else if (res == 0)
{
printf("I'm child process,PID = %d,PPID = %d\n", getpid(), getppid());
}
else
{
printf("I'm father process,PID = %d,PPID = %d\n", getpid(), getppid());
while (1)
{
sleep(1);
}
}
exit(0);
}
解决僵尸进程问题
- 重启操作系统 (不推荐)
- 杀掉父进程(不推荐)
- 进程等待-进程控制;
- 父进程如果结束, init会成为子进程的父进程,然后init调用wait获取退出码,子进程便不会变成僵尸进程
解决一、子进程后于父进程退出,变为孤儿进程,有init进程接管子进程,释放其资源
代码如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
int main()
{
pid_t res = fork();
if (res == -1)
{
printf("Creat process failed!\n");
return -1;
}
else if (res == 0)
{
printf("I'm child process,PID = %d,PPID = %d\n", getpid(), getppid());
while (1)
{
sleep(1);
}
}
else
{
printf("I'm father process,PID = %d,PPID = %d\n", getpid(), getppid());
}
exit(0);
}
解决二、使用wait函数
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int status;
pid_t res = fork();
if (res == -1)
{
printf("Creat process failed!\n");
return -1;
}
else if (res == 0)
{
printf("I'm child process,PID = %d,PPID = %d\n", getpid(), getppid());
}
else
{
pid_t id = wait(&status);
if (WIFEXITED(status))
{
printf("exit code = %d\n", WEXITSTATUS(status));
}
printf("I'm father process,PID = %d,PPID = %d\n", getpid(), getppid());
}
sleep(1);
exit(5);
}
wait函数原型:
进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸进程的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个这样的进程出现为止。
参数status用来保存被回收进程退出时的一些状态,如果我们不想知道这个子进程是如何死掉的,只想把它消灭掉的话,那么我们可以设定这个参数为NULL,就像下面这样:
pid = wait(NULL);
如果成功,wait会返回被回收子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。
1、WIFEXITED(status) 这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。(请注意,虽然名字一样,这里的参数status并不同于wait唯一的参数指向整数的指针status,而是那个指针所指向的整数)
2、WEXITSTATUS(status) 当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status)就会返回5;如果子进程调用exit(2), WEXITSTATUS(status)就会返回2。
请注意,如果进程不是正常退出的,也就是说,WIFEXITED返回0, 这个值就毫无意义。
三、孤儿进程
父进程先于子进程退出,子进程会被1号进程所领养,子进程在退出的时候,就会由1号进程回收子进程的退出资源,而子进程就不会变成僵尸进程,这样的进程称为孤儿进程。
1号进程是操作系统开机启动的第一个进程。没有孤儿状态,只有孤儿进程。