每个进程都有一个非负整型标识唯一的进程ID。因为进程ID标识符总是唯一的,常将其用作其他标识符的一部分以保证其唯一性。
虽然是唯一的,但是进程ID是可复用的,当一个进程终止后,其进程ID就称为复用的候选者,大多数UNIX系统实现延迟复用算法,使得赋予新建进程的ID不同于最近终止进程所使用的ID,这防止了将新进程误认为是使用同一ID的某一个已终止的进程。
系统中有一些专用进程。
#include .h>
pid_t getpid();
pid_t getppid();
uid_t getuid();
uid_t geteuid();
gid_t getgid();
gid_t getegid();
一个现有的进程可以调用fork函数创建一个新进程
#include
pid_t fork();
由fork创建的新进程被称为子进程。
fork函数被调用一次,但返回两次。
两次返回的而区别是子进程的返回值是0,父进程的返回值则是新建子进程的ID。
将新建子进程ID返回给父进程的理由是:
因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程ID。
fork使子进程得到返回值0的理由是:
一个进程孩子会有一个父进程,所以子进程总是可以调用getppid函数获得其父进程的ID。
子进程和父进程继续执行fork调用之后的指令,
子进程是父进程的副本。
例如:子进程获得父进程的数据空间,堆和栈的副本,注意,这是子进程所拥有的副本,父进程和子进程并不共享这些存储空间部分。
父进程和子进程共享正文段。
PS:正文段是由CPU执行的机器指令部分。正文段通常是共享的,另外,正文段也常常是可读的。
由于在fork后经常跟着exec,所以现在的很多的实现并不执行一个父进程数据段,栈和堆的完全副本。作为替代,使用了写时拷贝技术。
这些区域由父进程和子进程共享,并且内核将他们的访问权限改变为只读。如果父进程和子进程任意一个试图修改这些区域,则内核值为修改区域的那一块内存制作一个副本,通常是虚拟存储系统中的一页。
在上一篇Linux下的进程1——进程概念,进程切换,上下文切换,虚拟地址空间中,在后面我讲述了写时拷贝技术。此处不再赘述。
在我们知道了虚拟存储器和存储器映射后,我们可以清晰的知道fork函数是如何创建一个带有自己独立虚拟地址空间的新进程的。
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给他一个唯一的PID。
为了给这个新进程创建虚拟存储器,他创建了当前进程的mm_struct、区域结构和页表的原样拷贝。
它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时拷贝。
当fork在新进程返回时,新进程现在的虚拟存储器刚好和调用fork时存在的虚拟存储器相同。当这两个进程中的人一个后来进行写操作时,写时拷贝机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
一般来说,在fork之后是父进程先执行还是子进程先执行是不确定的,这取决于内核所使用的调度算法。
如果要求父进程和子进程之间相互同步,则要求某种形式的进程间通信。
如果我们在fork前在缓冲区中写入数据,那么fork后,子进程会继承父进程的缓冲区,在输出缓冲区内容时,fork前在缓冲区中的内容也会被写入子进程输出。
fork的一个特性是父进程的所有打开文件描述符都被复制到子进程中。
我们说的赋值是因为对每个文件描述符来说,就好像执行了dup函数,父进程和子进程每个相同的打开描述符共享一个文件表项。
现在来说一下什么叫做文件共享:
UNIX系统支持在不同进程间共享打开文件。
内核使用三种数据结构表示打开文件,他们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
1.每个进程在进程表中都有一个记录项,记录项包含一张打开的文件描述符表,每个描述符占用一项。
与每个文件描述符相关联的是:
2.指向一个文件表项的指针
2.内核为所有打开文件维持一张文件表,每个文件表项包含:
1.文件状态标志
3.每个打开文件都有一个v节点结构。
v节点包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点,这些信息是在打开文件时从磁盘上读入内存的,所以文件的所有相关信息都是随时可用的。
如果两个独立进程各自打开了同一文件,则有:
考虑以下情况:一个进程具有三个不同的打开文件,他们是标准输入,标准输出和标准错误。
再从fork返回时,就会有:
重要的一点是:父进程和子进程共享同一个文件偏移量。
考虑以下情况:一个进程fork了一个子进程,然后等待子进程终止。
假定,父进程和子进程都向标准输出进行写操作,如果父进程的标准输出已重定向,那么子进程写到该标准输出时,它将更新与父进程共享的该文件的偏移量。
在上图中,当父进程等待子进程时,子进程写到标准输出,而在子进程终止后,父进程也写到标准输出上,并且知道其输出会追加在子进程所写数据之后。
如果父进程和子进程不共享同一文件偏移量,要实现这种形式的交互就要困难很多。
如果父进程和子进程写同一描述符指向的文件,但又没有任何形式的同步,那么他们的输出就会互相混合。
fork之后处理文件描述符有以下两种常见情况:
除了打开的文件,父进程的很多属性也由子进程继承:
1.实际用户ID,实际组ID,有效用户ID,有效组ID
2.附属组ID
3.进程组ID
4.会话ID
5.控制终端
6.设置用户ID标志和设置组ID标志
7.当前工作目录
8.根目录
9.文件模式创建屏蔽字
10.信号屏蔽和安排
11.对任意打开文件描述符的执行时关闭标志
12.环境
13.连接的共享存储段
14.存储映像
15.资源限制
上面这么多都是子进程继承父进程的,那么父进程和子进程的区别是什么呢?
1.fork返回值不同
2.进程ID不同
3.这两个进程的父进程ID不同
4.子进程的tms_ utime , tms_ stime, tms_ cutime,tms_ ustime的值设置为0
5.子进程不继承父进程设置的文件锁
6.子进程的未处理闹钟被清除
7.子进程的未处理信号集设为空
使fork失败主要有两个原因:
1.系统中已经有了太多进程
2.该实际用户ID的进程总数超过了系统限制
fork有两个用法:
1.一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。
这在网络服务进程中是常见的,父进程等待客户端的服务请求,当这种请求到达时,父进程调用fork产生子进程处理,父进程则继续等待下一个服务请求。
2.一个进程要执行不同的程序。
这在shell是常见的,在这种情况下,子进程从fork返回后立即调用exec。
vfork函数的调用序列和返回值与fork相同,但两者的语义不同。
vfork和fork都会创建一个子进程,但是他并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec或exit,于是也就不会引用该地址空间。
不过在子进程调用exec或exit之前,他在父进程的空间中运行。
vfork和fork的另一个区别是:
vfork保证子进程先运行.
在他调用exec或exit之后父进程才可能被调度运行,当子进程调用这两个函数中的任意一个时,父进程会恢复运行。
在vfork调用后,一般子进程都会调用exec函数,那么他究竟是干什么的呢?
当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从main函数开始执行。
因为调用exec并不创建新进程,所以前后的进程ID并未改变,exec只是用磁盘上的一个新程序替换了当前进程的正文段,数据段,堆段和栈段。
有其中不同exec函数可供使用,他们常常被统称为exec函数。
#include
int execl(const char *path, const char *arg, ...)
int execv(const char *path, char *const argv[])
int execle(const char *path, const char *arg, ..., char *const envp[])
int execve(const char *path, char *const argv[], char *const envp[])
int execlp(const char *file, const char *arg, ...)
int execvp(const char *file, char *const argv[])
看起来exec函数族很多,但是掌握了规律就很好记了。
1.p表示path
对于不带字母p的exec函数,第一个参数必须是程序的相对路径或者绝对路径,例如”/bin/ls“而不是”ls”。
对于带字母p的函数:如果参数中包含/,则将其视为路径名,否则视为不带路径的程序名,在PATH环境变量的目录列表中搜索这个程序。
2.l表示list
带有字母l的exec函数要求将新程序的每个命令行参数都当做一个参数传给他,命令行参数的个数是可变的,因此函数原型中有…,…中的最后一个可变参数应该是NULL。
3.v表示vector
带有字母v的函数,应该先构造一个指向各参数的指针数组,然后将该数组的首地址当做参数传给他,数组中的最后一个指针也应该是NULL,就像main函数的argv参数或者环境变量表一样。
4.e表示environment
对于以e结尾的exec函数,可以把一份新的环境变量表传给它, 其他exec函数仍使用当前的环境变量表执行新程序。
事实上,只有execve是真正的系统调用,其他的五个函数最终都调用execve。
前面也说过,在执行exec后,进程ID没有改变。
但新程序从调用进程继承的下列属性:
1.实际用户ID,实际组ID,有效用户ID,有效组ID
2.附属组ID
3.进程组ID
4.会话ID
5.控制终端
6.闹钟尚余留的时间
7.当前工作目录
8.根目录
9.文件模式创建屏蔽字
10.文件锁
11.进程信号拼壁
12.未处理信号
13.nice值
14.tms_ utime , tms_ stime, tms_ cutime,tms_ ustime的值
15.资源限制