【学习时间:8小时】 【学习内容:《深入理解计算机系统》第八章 】
从处理器加点开始,直到断点为止,PC假设一个值的序列 a0,a1,a2……,a(n-1)(其中,每个ak是某个相应的指令Ik的地址)。每次从ak到a(k+1)的过渡称为控制转移。这样的控制转移序列称为控制流。 【理解:本质上是PC一连串的操作,“流”是一种形象化的说法,意味着处理器的顺畅执行】
【执行异常的时候,压入栈中的是什么?】 异常虽然类似于过程调用,但在压入栈的数据方面有不同。它会把一些额外的处理器状态压入栈中;并且如果是转移到内核的程序,压入的是内核栈中。 一旦触发异常,剩下的工作就由异常处理程序在软件中完成。在处理程序处理结束之后,它通过执行一条特殊的“从中断返回”指令,可选择地返回到被中断的程序。
【理解:什么叫异步?什么叫同步?我认为,同步或者异步都是相对于指令的执行而言的,与指令执行同时发生的,也就是执行指令才会产生的就叫做同步;而不一定与指令开始执行同时的,也就是与指令无关的,就叫做异步】
举例说明 【以hello程序为例】
int main()
{
write(1,"hello world\n",13);//用系统调用来写write函数
exir(0);
}
write函数第一个参数是将输出发送到stdout,第二个是要写的字节序,第三个是字节序列长度(算上,和换行符是12个;然而字符串结尾的时候还会有结束符\0)。
【针对write函数的系统调用】
movl $4,%eax//write函数的编号
movl $1,%ebx//以下是设置参数
movl $string,%ecx
movl $len,%edx
int $0x80//使用int来执行系统调用(系统调用的异常号是128)
【解释:系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需要的状态组成的。这个状态包括放在存储器中的程序的代码和数据等】 【每次用户通过向外壳输入一个可执行目标文件的名字,并运行一个程序的时候外壳就会创建一个新的进程;然后在这个新进程的上下文中运行这个可执行目标文件】 【应用程序也能够创建新的进程,然后再这个新进程的上下文中运行自己的代码或者其他应用程序】
【为什么会有好像程序独占处理器的假象?】 首先,进程计数器(PC)中的每一个值都唯一地对应于包含在程序的可执行目标文件中的指令,或者是包含在运行时动态链接的到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流。 其次,进程是轮流使用处理器的;每个进程执行它的流的一部分然后被挂起,其他进程执行。 然后,对于一个运行在其中一个进程上下文中的程序而言,它看上去就像是唯一地占用了处理器(只不过如果精确测量的话,会发现对于一个进程来说,它在执行期间好像被停顿了若干个很短的时间)。
过程:父进程通过调用fork函数来创建一个新的运行子进程
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);//子进程返回0;父进程返回子进程的PID;如果出错,则为-1
举例:
#include "csapp.h"
int main()
{
pid_t pid;
int x;
pid = fork();
if(pid ==0)
{
printf("child:x = %d\n",++x);
exit(0);
}
printf("parent : x=%d\n",--x);
exit(0);
}
程序运行结果为:
parent:x=0
child:x=2
【注释:子进程继承了父进程所有打开的文件,包括stdout文件;当父进程调用子进程的时候,后者的输出也是指向屏幕的】
引入:一个进程可以通过调用waitpid函数来等待它的子进程终止或者停止
#include<sys/types.h>
#incldue<sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);//如果成功,返回子进程的PID,如果为WNOHANG,则为0,其他错误则为-1
说明:默认地,即当options=0的时候,waitpid挂起调用进程的执行,直到它的等待集合中的一个子进程终止。如果等待集合中的一个进程在刚调用的时候就已经终止了,那么waitpid就立即返回。以上两种情况都会使得waitpid函数返回已经终止的子进程的PID,并且去除该进程。
应用举例
#include "csapp.h"
#define N 2
int main()
{
int status,i;
pid_t pid;
for(i =0;i<N;i++)
if((pid = fork())==0)
exit(100+i);
while((pid = waitpid(-1,&status,0))>0)
{
if(WIFEXITED(status))
printf("child %d terminated normally with exit status = %d\n",pid,WEXITSTATUS(status));
else
printf("child %d terminated abnormally\n",pid);
}
if(errno != ECHILD)
unix_error("waitpid error");
exit(0);
}
函数:
unsigned int sleep(unsigned int secs);//返回还要休眠的秒数
【如果请求的时间量已经到了,则返回0】
int pause(void);//该函数让调用函数休眠
函数:
#include<unistd.h>
int execve(const char *filename,const char *argv[],const char *envp[]);//加载并运行可执行目标文件,如果成功则无返回值,如果不成功则返回-1
运行 在上面的函数加载了filename之后,它调用了启动代码。启动代码设置栈,将控制传给新程序的主函数
int main(int argc,char **argv,char **envp);//这里argc描述argv数组中非空元素的个数
对于32位的linux系统,执行的时候栈的情况如下:
函数:setjmp,longjmp
int setjmp(imp_buf env);返回0
int sigsetjmp(sigjmp_buf env,int savesigs);
void longjmp(jmp_buf env,int retval);
void siglongjmp(sigjmp_buf env,int retval);
A(进程) 0(起始时间) 2(结束时间)
B(进程) 1(起始时间) 4(结束时间)
C(进程) 3(起始时间) 5(结束时间)
问:进程两两之间是否属于并发?
【A&C:不属于;B&C:属于;A&B:属于】
#include "csapp.h"
int main()
{
int x =1;
if(fork()==0)
printf("printf1:x=%d\n",++x);
printf("printf2:x =%d\n",--x);
exit(0);
}
问:子进程的输出是什么?父进程的输出是什么? A:子进程输出是 printf1:x=2
B:父进程输出是 printf2:x=0
int main()
{
if(Fork() == 0)
{
printf("a");
fflush(stdout);
}
else
{
printf("b");
fflush(stdout);
waitpid(-1,NULL,0);
}
printf("c");
fflush(stdout);
exit(0);
}
【子进程会输出“ac”,父进程会输出“bc”;而父子进程执行先后不是确定的,所以输出序列可能是:acbc,abcc,bacc,bcac】
unsigned int snooze(unsigned int secs);//除了它会打印一条语句来描述休眠了多少秒之外,和sleep函数一样
解答:
unsigned int snooze(unsigned int secs)
{
unsigned int rc = sleep(secs);
printf("Slept for %u of %u seconds\n",secs-rc,secs);
return rc;
}
unix> ./myecho arg1,arg2
Command line arguments:
argv[ 0]:myecho
……
Envirionment variables:
envp[ 0]:PWD+/user0/droh/ics/code/ecf
……
解答: 补充函数
char *getenv(const char *name);//若存在,则返回指向name的指针,若无,则为null
该函数在环境数组中搜索字符串“NAME=VALUE”,如果找到则返回指向value的指针。 另外,根据8.4节的内容,main函数就可以很好地结合argv和envp两个数组,所以主体应是main函数;这样的话argv和envp数组的元素可以直接输出
#include "csapp.h‘
int main(int argc,char *argv[],char *envp[])
{
int i;
printf("Command line arguments:\n");
for(i=0;i=argv[i]!=NULL,i++)
printf(" argv[%2d]:%s\n",i,argv[i]);
printf("\n");
printf("Envirionment variables:\n");
for(i=0;envp[i]!=NULL;i++)
printf(" envp[%2d]:%s\n",i,envp[i]);
exit(0);
}
P495
pidt waitpid(pidt pid,int *status,int options);//如果成功,返回子进程的PID,如果为WNOHANG,则为0,其他错误则为-1
P496
(修改默认行为部分)WUNTRACED:挂起调用进程的执行,直到等待集合中的一个变成已经终止的或者被停止,然后返回导致返回的子进程的PID
【疑问:无论options是默认的0还是WUNTRACED,最后的函数返回情况都是一样的吗?(书上的解释为:默认的options值会使得函数返回已经终止的子进程;而将options的值修改为WUNTRACED之后,返回的是导致返回的已终止及被停止的进程PID——这两者有什么不同?)】
本章的内容较多,但是都是针对之前章节学习中有所涉及而不能解答的地方展开,比如:处理器怎样执行程序、怎样处理错误、怎样控制进程甚至怎样灵活地跳转等等;同时,也和其他课程有序衔接了起来(比如和操作系统中的进程控制部分就有相似之处)。所以读起来不时会有豁然开朗的感觉;越来越觉得原来处理器看似复杂曲折的“大脑”也有着简单、人性化的基础。