最近在努力地看《unix网络编程》这本书,也模仿地完成了一个FTP 客户端。但是对并发服务器的章节特别模糊,特别是其中关于进程和信号的概念,所以特意去看了《深入理解计算机系统》的相关章节,写下一些思考,以便以后回顾。
程序与进程
程序:当你对一个Cpp程序的编译和运行有所了解,你就会明白程序其实就是一堆代码和数据,程序可以作为目标模块存在于磁盘上,或者作为段存在于地址空间中,我肤浅地认为,程序是静态的。
而进程是执行中程序的一个具体的实例,程序总是运行在某个进程的上下文中(context),可以说,进程是动态的。进程为每个程序提供假象,好像它在独占地使用处理器。
用fork()和execve()函数举一个例子(下面要详细描述这两个函数)。
(1)fork函数在新的子进程中运行相同的程序,新的子进程是父进程的一个复制品;
(2)execve函数在当前进程的上下文中加载并运行一个新的程序,它会覆盖当前进程的地址空间,但并没有创建一个新进程。新的程序仍然有相同的PID,而且继承了调用execve函数时已打开的所有文件描述符。
上下文(context)和私有地址空间
在一个操作系统中,每个程序都是运行在某个进程的context中的。上下文就是由程序正确运行所需的状态组成的。这个状态包括存放在存储器中的程序的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。
进程也为每个程序提供了一种假象,好像它独占地使用系统地址空间,每个进程地址空间都有相同的地址结构,用于存放进程的上下文。
对于32位进程来说,代码段从地址0x08408000开始;对于64位进程来说,代码代码段是从地址0x00400000开始的。
进程控制
(1)获取进程ID
每个进程都有一个唯一的正数(非零)进程ID(PID)。getpid函数返回调用进程的PID。getppid函数返回它的父进程的PID(创建调用进程的进程)。
#<sys/types.h>
#<unistd.h>
pid_t getpid(void);
pid_t getppid(void);
(2)创建和终止进程
从程序员的角度,我们可以认为进程总是处于下面三种状态之一:
运行:进程要么在CPU上执行,要么等待被执行且最终会被内核调度;
停止:进程的执行被挂起(suspend),且不会被调度。当收到SIGSTOP,SIGTSTP,SIDTTIN或者SIGTTOU信号时,进程就停止,并且保持停止直到它收到一个SIGCONT信号,在这个时刻,进程再次开始运行(信号是软件中断的一种形式)。
终止:进程永远地停止了,进程停止会因为这三种原因:收到一个信号,该信号的默认行为是终止进程;从程序返回;调用exit函数。
tips:exit以status退出状态来终止进程(另一种设置退出状态的方法是从主程序中返回一个整数值,e.g. return 0)。
下面该进入这一章节的重点了,那就是fork函数。
父进程通过调用fork函数创建一个新的运行的子进程:
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);
新创建的子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟地址空间相同的(但是单独的一份拷贝),包括文本,数据和bss段,堆以及用户栈。子进程还获得与父进程任何打开文件描述符相同的拷贝,这就意味着当父进程调用fork时,子进程可以读写父进程中的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
#include "caspp.h"
int main()
{
pid_t pid;
int x=1;
pid=Fork();
if(pid==0){
printf("child: x=%d\n",++x);/*child*/
exit(0);/*exit(status)*/
}
prinf("parent:x=%d\n",--x);
exit(0);
}
关于fork,这里有几点说明:
(1)调用一次,返回两次:这是fork() 最特别的一个地方。一次是返回到父进程,一个是返回到新创建的子进程。
(2)并发执行:父进程和子进程是并发执行的独立进程。内核能够以任何方式交替执行它们的逻辑控制流中的指令。一个逻辑流的执行在时间上与另一个流重叠,称为并发流。一个父进程先完成它的prinf语句,然后是子进程。然而,在另一个系统上可能正好相反。
(3)相同的但是独立的地址空间:我们可以看到,父进程和子进程对x所做的任何改变都是独立的,不会反映在另一个进程的存储器中。这就是为什么父进程和子进程调用它们各自的printf语句时,它们中的变量x会有不同的值的原因。
(4)共享文件:父进程和子进程都吧它们的输出显示在屏幕上。原因是子进程继承了父进程所有的打开文件,当父进程调用fork时,stdout文件是被打开的,并指向屏幕,子进程继承了这个文件,因此它的输出也是指向屏幕的。
下面的一段代码,可以帮组我们深入了解fork函数:
#include "csapp.h"
int main(){
Fork();
Fork();
Fork();
printf("hello\n");
exit(0);
}
在这段代码里面,hello被执行了八次。我们可以用进程图帮助我们理解其中的过程,在进程图中,我们可以把主轴想象成父进程,分轴想象为子进程,就像一个二叉树,有三层,所以一共执行了8次hello。