QA23
fork函数调用一次返回两次,怎么可能呀?
当程序执行到下面的语句: pid=fork(); 的时候会复制父进程的堆栈段,所以两个进程都停留在fork函数中,等待返回。因此fork函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,这两次的返回值是不一样的。通过fork返回的值来判断当前进程是子进程还是父进程
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值。
进程状态有哪几种?进程终止有几种方法?
linux上进程有5种状态:
-
运行(正在运行或在运行队列中等待)
-
中断(休眠中, 受阻, 在等待某个条件的形成或接受到信号)
-
不可中断(收到信号不唤醒和不可运行, 进程必须等待直到有中断发生)
-
僵死(进程已终止, 但进程描述符存在, 直到父进程调用wait4()系统调用后释放)
-
停止(进程收到SIGSTOP, SIGSTP, SIGTIN, SIGTOU信号后停止运行运行)
进程终止有8种
正常终止五种:
1.从main返回。
2.调用exit。
3.调用_exit或_Exit。
4.最后一个线程从其启动例程返回。
5.最后一个线程调用pthread_exit。
三种异常终止:
6.调用abort()。
7.接到一个信号并终止。
8.最后一个线程对取消请求作出响应。
kill—杀死是终止进程吗?kill某进程是怎么实现的?
kill 从字面来看,就是用来杀死进程的命令,但事实上,这个或多或少带有一定的误导性。从本质上讲,kill 命令只是用来向进程发送一个信号,至于这个信号是什么,是用户指定的。
也就是说,kill 命令的执行原理是这样的,kill 命令会向操作系统内核发送一个信号(多是终止信号)和目标进程的 PID,然后系统内核根据收到的信号类型,对指定进程进行相应的操作。
kill 命令的基本格式如下:
[root@localhost ~]# kill [信号] PID
kill 命令是按照 PID 来确定进程的,所以 kill 命令只能识别 PID,而不能识别进程名
kill 命令是按照 PID 来确定进程的,所以 kill 命令只能识别 PID,而不能识别进程名。Linux 定义了几十种不同类型的信号,读者可以使用 kill -l 命令查看所有信号及其编号,这里仅列出几个常用的信号,如下表所示。
信号编号 | 信号名 | 含义 |
---|---|---|
0 | EXIT | 程序退出时收到该信息。 |
1 | HUP | 挂掉电话线或终端连接的挂起信号,这个信号也会造成某些进程在没有终止的情况下重新初始化。 |
2 | INT | 表示结束进程,但并不是强制性的,常用的 “Ctrl+C” 组合键发出就是一个 kill -2 的信号。 |
3 | QUIT | 退出。 |
9 | KILL | 杀死进程,即强制结束进程。 |
11 | SEGV | 段错误。 |
15 | TERM | 正常结束进程,是 kill 命令的默认信号。 |
需要注意的是,表中省略了各个信号名称的前缀 SIG,也就是说,SIGTERM 和 TERM 这两种写法都对,kill 命令都可以理解。
(*)Kill是终止进程并回收进程吗?
kill命令可以加许多参数(如上面的表格),其中-2 -9 -15和不添加参数的kill是不一样的
kill不带参数 即是普通的杀死进程,回收资源,-
2参数是低级别,可以被某些程序忽略,造成无法杀死进程。
-9参数是强制行为,不回收资源,可能造成资源浪费。例如父进程无法被回收
-15就是强制杀死进程,回收资源。
fork的子进程与其父进程有什么相同与区别?
fork之后,操作系统会复制一个与父进程完全相同的子进程,虽说是父子关系,但是在操作系统看来,他们更像兄弟关系,这2个进程共享代码空间,但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同,子进程拥有父进程当前运行到的位置(两进程的程序计数器pc值相同,也就是说,子进程是从fork返回处开始执行的),但有一点不同,如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,如果fork不成功,父进程会返回错误。
fork出来的子进程,基本上除了进程号之外父进程的所有东西都有一份拷贝,基本就意味着不是全部,下面我们要说的是子进程从父进程那里继承了什么东西,什么东西没有继承。还有一点需要注意,子进程得到的只是父进程的拷贝,而不是父进程资源的本身。
由子进程自父进程继承到:
1.进程的资格(真实(real)/有效(effective)/已保存(saved)用户号(UIDs)和组号(GIDs))
2.环境(environment)
3.堆栈
4.内存
5.打开文件的描述符(注意对应的文件的位置由父子进程共享,这会引起含糊情况)
6.执行时关闭(close-on-exec) 标志 (译者注:close-on-exec标志可通过fnctl()对文件描述符设置,POSIX.1要求所有目录流都必须在exec函数调用时关闭。更详细说明,参见《UNIX环境高级编程》 W. R. Stevens, 1993, 尤晋元等译(以下简称《高级编程》), 3.13节和8.9节)
7.信号(signal)控制设定
8.nice值 (译者注:nice值由nice函数设定,该值表示进程的优先级,数值越小,优先级越高)
进程调度类别(scheduler class)(译者注:进程调度类别指进程在系统中被调度时所属的类别,不同类别有不同优先级,根据进程调度类别和nice值,进程调度程序可计算出每个进程的全局优先级(Global process prority),优先级高的进程优先执行)
8.进程组号
9.对话期ID(Session ID) (译者注:译文取自《高级编程》,指:进程所属的对话期(session)ID, 一个对话期包括一个或多个进程组, 更详细说明参见《高级编程》9.5节)
10.当前工作目录
11.根目录 (译者注:根目录不一定是“/”,它可由chroot函数改变)
12.文件方式创建屏蔽字(file mode creation mask (umask))(译者注:译文取自《高级编程》,指:创建新文件的缺省屏蔽字)
13.资源限制
14.控制终端
子进程所独有:
进程号
1.不同的父进程号(译者注:即子进程的父进程号与父进程的父进程号不同, 父进程号可由getppid函数得到)
2.自己的文件描述符和目录流的拷贝(译者注:目录流由opendir函数创建,因其为顺序读取,顾称“目录流”)
3.子进程不继承父进程的进程,正文(text), 数据和其它锁定内存(memory locks)(译者注:锁定内存指被锁定的虚拟内存页,锁定后,4.不允许内核将其在必要时换出(page out),详细说明参见《The GNU C Library Reference Manual》 2.2版, 1999, 3.4.2节)
5.在tms结构中的系统时间(译者注:tms结构可由times函数获得,它保存四个数据用于记录进程使用中央处理器 (CPU:Central Processing Unit)的时间,包括:用户时间,系统时间, 用户各子进程合计时间,系统各子进程合计时间)
6.资源使用(resource utilizations)设定为0
8.阻塞信号集初始化为空集(译者注:原文此处不明确,译文根据fork函数手册页稍做修改)
9.不继承由timer_create函数创建的计时器
10.不继承异步输入和输出
fork的子进程与其父进程同名的全局变量对应同一物理地址?
- fork时子进程获得父进程数据空间、堆和栈的复制,所以变量的地址(当然是虚拟地址)也是一样的。
- 每个进程都有自己的虚拟地址空间,不同进程的相同的虚拟地址显然可以对应不同的物理地址。因此地址相同(虚拟地址)而值不同没什么奇怪。
fork子进程完全复制父进程的栈空间,也复制了页表,但没有复制物理页面,所以这时虚拟地址相同,物理地址也相同,但是会把父子共享的页面标记为“只读”,如果父子进程一直对这个页面是同一个页面,知道其中任何一个进程要对共享的页面“写操作”,这时内核会复制一个物理页面给这个进程使用,同时修改页表。而把原来的只读页面标记为“可写”,留给另外一个进程使用。
假定父进程malloc的指针指向0x12345678, fork 后,子进程中的指针也是指向0x12345678,但是这两个地址都是虚拟内存地址 (virtual memory),经过内存地址转换后所对应的 物理地址是不一样的。所以两个进城中的这两个地址相互之间没有任何关系。
就是说:
子进程继承父进程的全局变量。
子进程创建以后,可以读取原来父进程的全局变量的值。
但是创建以后,子进程修改了变量,或者是父进程修改了变量值,互相都不影响了。
无论是什么类型的变量,fork后父子进程中都是一样的,但两者之间没有关系,任何一个进程修改变量后,在另一个进程中都不能知道,更不能访问另一个进程中的变量,也不会对应相同的物理地址了
但实际上,linux为了提高 fork 的效率,采用了 copy-on-write 技术,fork后,这两个虚拟地址实际上指向相同的物理地址(内存页),只有任何一个进程试图修改这个虚拟地址里的内容前,两个虚拟地址才会指向不同的物理地址(新的物理地址的内容从原物理地址中复制得到)
(*)fork生成副本太浪费空间了,能不能先与父进程共享物理内存?特别是代码段!而且fork后还得execve覆盖掉。
早期的UNIX的fork()实现时,就是原汁原味的复制,和它的表像一样,但是很明显,这种方法效率太低,而且造成了很大的浪费,现在大部分的UNIX实现采用了如下俩种方法来规避这种浪费
(1)首先我们可以确定父子进程的代码段是相同的,所以代码段是没必要复制的,因此内核将代码段标记为只读,这样父子进程就可以安全的共享此代码段了。fork之后在进程创建代码段时,新子进程的进程级页表项都指向和父进程相同的物理页帧
(2)而对于父进程的数据段,堆段,栈段中的各页,由于父子进程要相互独立,所以我们采用写实复制的技术,来最大化的提高内存以及内核的利用率。刚开始,内核做了一些设置,令这些段的页表项指向父进程相同的物理内存页。调用fork之后,内核会捕获所有父进程或子进程针对这些页面的修改企图(说明此时还没修改)并为将要修改的页面创建拷贝。系统将新的页面拷贝分配给被内核捕获的进程,还会对子进程的相应页表项做适当的调整,现在父子进程就可以分别修改各自的上述段,不再互相影响了
fork()后产生的子进程与父进程行为是相互独立的,变量也需要是独立的,所以说物理内存中需要有两份空间,不可以共享全部内存。
(*)fork7去掉while(1),执行后再ps,父与子进程能看到哪一个?
(*)fork8父进程exit为什么ps看不出父进程defunct?
一些知识点:
并发进程是并行执行的,控制流物理上是不相交的。
任务管理器中的进程内是串行、进程间是并行