子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。注意,子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间。
在Linux中最主要的进程标识: 进程号(PID,Process Idenity Number) 父进程号(PPID,parent process ID)。 其中PID惟一地标识一个进程,内核通过这个标识符来识别不同的进程,用户程序通过PID对进程发号施令。 PID和PPID都是是32位的无符号整数。
老进程成为新进程的父进程(parentprocess),而相应的,新进程就是老的进程的子进程(child process)。一个进程除了有一个PID之外,还会有一个PPID(parentPID)来存储的父进程PID。如果我们循着PPID不断向上追溯的话,总会发现其源头是init进程。所以说,所有的进程也构成一个以init为根的树状结构。
写时复制技术:内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟究竟结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。
内核程序通过进程表对进程进行管理,每个进程在进程表中占有一项。在Linux系统中,进程表项是一个task_struct任务结构指针。任务数据结构定义在头文件include/linux/sched.h中。有些书上称其为进程控制块PCB(Process Control Block)或者进程描述符PD(Processor Descriptor)。其中保存着用于控制和管理进程的所有信息。主要包括进程当前运行的状态信息,信号,进程号,父进程号,运行时间累计值,正在使用的文件和本任务的局部描述符以及任务状态段信息。该结构每个字段的含义如下所示。
当一个进程在执行时,CPU的所有寄存器中的值,进程的状态以及堆栈中的内容被称为该进程的上下文。当内核需要切换(switch)至另一个进程时,它就需要保存当前进程的所有状态,也即保存当前进程的上下文,以便在再次执行该进程时,能够恢复到切换时的状态执行下去。在Linux中,当前进程上下文均保存在进程的任务数据结构task_struct中。在发生中断时,内核就在被中断进程的上下文中,在内核状态下执行中断服务例程。但同时会保留所有需要用到的资源,以便中断服务结束时能恢复被中断进程的执行。
getpid返回当前进程标识,getppid返回父进程标识。 用法: #include <sys/types.h> #include <unistd.h>
pid_tgetpid(void); pid_t getppid(void);
1、创建进程的函数原型
pid_t fork( void);
(pid_t是一个宏定义,其实质是int被定义在#include<sys/types.h>中)
返回值:若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;否则,出错返回-1
注意:
一般来说,fork之后父进程先执行还是子进程先执行是不确定的。这取决于内核所使用的调度算法。
pid_t vfork( void);
fork()与vfock()都是创建一个进程,那他们有什么区别呢?总结有以下三点区别: 1. fork ():子进程拷贝父进程的数据段,代码段 vfork ():子进程与父进程共享数据段 2. fork ()父子进程的执行次序不确定 vfork 保证子进程先运行,在调用exec或exit之前与父进程数据是共享的,在它调用exec或exit之后父进程才可能被调度运行。 3. vfork ()保证子进程先运行,在她调用exec或exit之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。
为什么会有vfork,因为以前的fork 很傻, 它创建一个子进程时,将会创建一个新的地址 空间,并且拷贝父进程的资源,而往往在子进程中会执行exec 调用,这样,前面的拷贝工 作就是白费力气了,这种情况下,聪明的人就想出了vfork,它产生的子进程刚开始暂时与 父进程共享地址空间(其实就是线程的概念了),因为这时候子进程在父进程的地址空间中 运行,所以子进程不能进行写操作,并且在儿子 霸占”着老子的房子时候,要委屈老子一 下了,让他在外面歇着(阻塞),一旦儿子执行了exec 或者exit 后,相 于儿子买了自己的 房子了,这时候就相 于分家了。
2、exec函数族
用fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程完全由新程序代换,而新程序则从其 main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。有六种不同的exec函数可供使用,它们常常被统称为exec函数。这些exec函数都是UNIX进程控制原语。用fork可以创建新进程,用exec可以执行新的程序。
exec函数族提供了一种在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段、和堆栈段。在执行完之后,原调用进程的内容除了进程号外,其他全部都被替换了。可执行文件既可以是二进制文件,也可以是任何Linux下可执行的脚本文件。
3、wait和waitpid
wait函数:调用该函数使进程阻塞,直到任一个子进程结束或者是该进程接收到一个信号为止。如果该进程没有子进程或者其子进程已经结束,wait函数会立即返回。
waitpid函数:功能和wait函数类似。可以指定等待某个子进程结束以及等待的方式(阻塞或非阻塞)。
wait函数
pid_t wait(int *status);
函数参数:
status是一个整型指针,指向的对象用来保存子进程退出时的状态。
A.status若为空,表示忽略子进程退出时的状态
B.status若不为空,表示保存子进程退出时的状态
子进程的结束状态可由Linux中一些特定的宏来测定。
pid_t waitpid(pid_t pid,int*status,int options);
4、exit
exit是一个库函数,exit(1)表示发生错误后退出程序,exit(0)表示正常退出。在stdlib.h中exit函数是这样子定义的:void exit(int status)。这个系统调用是用来终止一个进程的,无论在程序中的什么位置,只要执行exit,进程就会从终止进程的运行。讲到exit这个系统调用,就要提及另外一个系统调用,_exit,_exit()函数位于unistd.h中,相比于exit(),_exit()函数的功能最为简单,直接终止进程的运行,释放其所使用的内存空间,并销毁在内存中的数据结构,而exit()在于在进程退出之前要检查文件的状态,将文件缓冲区中的内容写回文件。
exit函数和_exit函数有如下区别:
1> exit函数在ANSIC中说明的,而_exit函数是在POSIX标准中说明
2> exit函数将终止调用进程,在退出程序之前所有文件被关闭,标准输入输出的缓冲区被清空,并执行在atexit注册的回调函数;_exit函数终止调用进程,但不关闭文件,不清除标准输入输出的缓冲区,也不调用在atexit注册的回调函数。
首先是最简单的system函数,它需要启动新的shell并在新的shell是执行子进程,所以对环境的依赖较大,而且效率也不高。同时system函数要等待子进程的返回才能执行下面的语句。
exec系统函数是用新的进程来替换原先的进程,效率较高,但是它不会返回到原先的进程,也就是说在exec函数后面的所以代码都不会被执行,除非exec调用失败。然而exec启动的新进程继承了原进程的许多特性,在原进程中已打开的文件描述符在新进程中仍将保持打开,但需要注意,任何在原进程中已打开的目录流都将在新进程中被关闭。
fork则是用当前的进程来复制出一个新的进程,新进程与原进程一模一样,执行的代码也完全相同,但新进程有自己的数据空间、环境变量和文件描述符,我们通常根据fork函数的返回值来确定当前的进程是子进程还是父进程,即它并不像exec那样并不返回,而是返回一个pid_t的值用于判断,我们还可以继续执行fork后面的代码。感觉用fork与exec系列函数就能创建很多需的进程。#include <unistd.h> #include <syspes.h> #include <sys/wait.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <math.h> int main(void) { pid_t child; /* 创建子进程 */ if((child=fork())==-1) { printf("Fork Error : %s\n", strerror(errno)); exit(1); } else { if(child==0) // 子进程 { printf("the child process is run\n"); sleep(1); //子进程睡眠一秒,但并没有去运行父进程 printf("I am the child: %d\n", getpid()); exit(0); } else //父进程 { wait(NULL); //等到子进程退出,父进程才会运行 printf("the father process is run\n"); printf("I am the father:%d\n",getpid()); return 0; } } }
