顺序执行或者通过正常的Call、Ret、JMP等进行跳转的执行流称为正常控制流,除此以外的就称为异常控制流。
对于某个处理器而言,他的某段时间上的控制流称为物理控制流。
对于某个进程而言,某段时间上执行该进程的整个控制流称为逻辑控制流。
例1:缺页
程序需要到内存去取指令,可能发生内存缺页(查页表发现V或者P那位为0),取不到那条指令,这个程序就执行不下去了,就发生了异常。这种情况下用户程序是无能为力的,因为用户程序是不能直接访问磁盘去读那个程序的,只能喊操作系统过来处理去缺页,由操作系统去读磁盘把缺页的那一页取过来装到内存填好页表,然后从缺页处理程序返回,返回后继续执行。所以缺页这种异常是可修复的。
像栈溢出、访问越权(访问了不该访问的内存),访问越级(用户级别的去访问内核),是不可修复的,直接杀死进程。
例2:整除0
内部异常,如整除0,发生这种情况时不知道应该得到什么结果,这就是一种异常事件,只能喊操作系统内核来处理(——杀死这个程序,并给一个message说明发生了整除0异常)。
如溢出,可以编一个硬件,取出OF看是否为1,在溢出时禁止把结果写入到目的寄存器,并终止进程报个溢出错误.
辨3:内部异常与外部中断
外部中断
是发送从CPU外部来的中断请求信号通过总线送到CPU的一个引脚上面,CPU去探测这个引脚,发现这个引脚有效,说明外部有请求信号,CPU被告知外面有请求信号需要停一停,这叫做外部中断。
内部异常
:这个容易理解,是CPU内部发生的意外事件或特殊事件,分为了陷阱,故障和终止。
知4:为什么称外部中断是异步的,内部异常是同步的
1.
同步与异步
两个执行流之间有依赖关系,一方需要等待另一方执行完才能继续执行的,称为同步.
没有依赖关系就称为异步
2.why 外部中断是异步
这是因为中断处理程序和进程没有任何关系,所谓和进程没有任何关系就是指中断处理程序不会依赖进程的任何资源,换句话说,任何一个进程都可能在任何时候被中断而执行中断处理程序,而到底哪个进程被中断,什么时候被中断,具有不确定性,而且中断后,被中断的进程资源不会被中断处理程序所使用。因为中断处理程序和进程没有任何的关系,所以一旦运行起来就会一直运行下去,不会等待某些资源而暂停,所以说中断处理程序时异步运行的。
由于中断处理程序的异步性,其和进程没有任何联系,所以往往中断处理程序,既没有参数也没有返回值。
3.why内部异常是同步
这是因为内部异常是当前进程在执行完某个指令后检测到异常时,就必须终止当前进程的执行流,转而执行异常处理程序之后才能继续执行当前的进程的执行流(也就是设一个断点),或者很严重的问题就只能终止。
程序
是一个静态概念,所有的数据和指令都以二进制的形式保存在exe文件中,这个文件就是一个程序。
进程
是执行中的程序的实例,是一个动态的概念,系统中每个程序都运行在某个进程的上下文context中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
C程序用syscall
函数可以直接调用任何系统调用。然而,实际上几乎没必要这样做,大多数都是C库提供了针对系统调用的一组方便的包装函数。这些包装函数将参数打包到一起,以适当的系统调用指令陷入内核,然后将系统调用的返回状态传递回调用程序。本书称系统调用及其包装函数都称为系统级函数
。
x86-86架构中
,syscall
的参数全部由寄存器传递:
rax
中存放系统调用号
rdi rsi rdx r10 r8 r9
一次存放1~6
参数 <故max(arg count)=6
>
例如
int main()
{
write(1,"hello,world\n",13);
_exit(0);
}
转成汇编代码为
.section .data
string:
.ascii "hello,world\n"
string_end:
.equ len,string_end - string
.section .text
.global main
main:
//First:call write(1,"hello,world\n",13)
movq $1,%rax //write is system call 1
movq $1,%rdi //Arg1:file decripter 1 means stdout标准输出流
movq $string,%rsi //Arg2:hello world string
movq $len,%rdx //Arg3:string length
syscall //Make the system call
//Next:call _exit(0)
movq $60,%rax //_exit is system call 60
movq $0,%rdi //Arg1:exit status is 0
syscall //Make the system call
当Unix系统级函数遇到错误,通常返回-1,并设置全局整数变量errno
来表示出什么错,程序员应该去检查错误。
int pid;
if ( (pid = fork()) <0)
{
fprintf(stderr, "fork error: %s\n",strerror(errno));
exit(0);
}
strerror(errno)
返回一个字符串,描述了errno指定的错误。
可以专门用一个错误报告函数来报告错误。
void unix_error(char *msg)
{
fpintf(stderr,"%s: %s\n",msg,strerror(errno));
exit(0);
}
给定这个函数,我们可以对fork的调用从4行缩减到2行
if ((pid = fork()) < 0 )
unix_error("fork error");
可以进一步使用错误处理包装函数
进一步简化代码。
pid_t Fork()
{
pid_t pid;
if ( (pid = fork() ) < 0)
unix_error("Fork error");
return pid;
}
给定这个包装函数,我们对fork的调用就缩减为1行
pid = Fork();
本书的剩余部分都使用错误处理函数,保持代码简洁且可以有允许忽而略错误检查的假象。
每一个进程都有唯一的正数(非0)进程ID(PID)。getpid
返回该进程的PID,getppid
返回其父进程的PID。
#include
#include
// Linux中在types.h中,pid_t被定义为int
pid_t getpid(void);
pid_t getppid(void);
示例:
#include
int main()
{
printf("%d\n",getpid());
return 0;
}
![在这里插入图片描述](https://img-blog.csdnimg.cn/20201209095710923.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0V0ZXJuaXR5a2M=,si ze_16,color_FFFFFF,t_70)
#include
void exit(int status);
exit讲完了,后面是fork
创建进程。fork是创建相同的副本,创建后两个进程都会继续执行后面的命令,但是子进程先还是父进程先执行,或者交错执行,这都是不确定的。fork()的将0返回给子进程,将子进程的PID返回给父进程。
进程有很多分叉,下面的分叉为父进程,上面的为子进程。父进程和子进程并发执行
,但是是共享文件
的(子进程继承了父进程所有打开文件的),可以看到父进程和子进程都把输出显示在屏幕上,这是因为父进程调用fork时stdout是打开的,并指向屏幕。子进程继承了这个文件,因此它的输出也是指向屏幕的。
#include
#include
pid_t fork(void);
示例:
#include
int main()
{
pid_t pid;
int x=1;
pid = Fork();
if (pid == 0){
printf("child : x=%d\n",++x);
exit(0);
}
printf("parent: x=%d\n",--x);
return 0;
}
当进程终止,内核不是立即把他从系统清除,而是保持在一种已终止的状态,直到被其父进程回收。一个终止了却还未被回收的进程称为僵死进程
(zombie
).
规则:
子进程终止后,不会被回收,变成僵死进程。
父进程终止后,内核会安排pid=1
的init进程
回收它们的僵死子进程。如果父进程终止时子进程没有僵死,那么子进程会依然存在而不会被杀死。
#include
int main()
{
if (fork() == 0) {
/* Child */
printf("Running Child, PID = %d\n",
getpid());
while (1)
; /* Infinite loop */
}
else {
printf("Terminating Parent, PID = %d\n",
getpid());
exit(0);
}
}
wait
和waitpid
有一个重要的性质是,用他们终止的子进程会顺带回收,而不是让他们变成僵死进程。
wait
是在调用的地方暂停,直到子进程终止后才继续,且子进程终止后内核会回收该子进程,回收后才返回到父进程。
<有一个子进程终止就会继续父进程>
<当然如果本来就没有子进程,就继续运行咯>
#include
#include
pid_t wait(int *status);
// 如果成功则返回子进程的PID,若出错返回-1
// 调用后,status指向的整数是表示其子进程终止原因的值
#include
#define N 10
void fork10() {
pid_t pid[N];
int i, child_status;
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0) {
exit(100+i); /* Child */
}
for (i = 0; i < N; i++) { /* Parent */
pid_t wpid = wait(&child_status);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n",wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminate abnormally\n", wpid);
}
}
int main()
{
fork10();
return 0;
}
waitpid
等待特定pid集合的子进程终止,若该集合有一个终止,则父进程继续运行。
<如果pid=-1
,那么等待集合就是由父进程所有的子进程组成的>
<如果pid>0
,那么等待集合为单独的子进程PID=pid>
#include
#include
pid_t waitpid(pid_t,int *statusp,int options);
// 如果成功,则返回子进程的PID
// 如果WNOHANG,则为0
// 如果其他错误,则为-1
#include
#define N 10
void fork11() {
pid_t pid[N];
int i;
int child_status;
for (i = 0; i < N; i++)
if ((pid[i] = fork()) == 0)
exit(100+i); /* Child */
for (i = N-1; i >= 0; i--) {
pid_t wpid = waitpid(pid[i], &child_status, 0);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n",
wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminate abnormally\n", wpid);
}
}
int main()
{
fork11();
return 0;
}
sleep
函数将一个进程挂起一段指定的时间
#include
unsigned int sleep(unsigned int secs);
// 返回还要休眠的秒数
如果请求的时间量到了,sleep返回0
,否则返回还剩下的需要休眠的秒数
。后一种情况是可能的,如果因为sleep函数被一个信号中断而过早的返回。
另一个函数pause
让调用函数休眠,直到该进程收到一个信号。
#include
int pause(void);
// 总是返回-1
execve
函数在当前进程的上下文加载(将可执行文件从磁盘复制到内存<简单先这样理解>)并运行一个新程序。
#include
int execve(const char *filename,
const char *argv[],cosnt char *envp[]);
//如果成功,则不返回
//如果错误,返回-1,返回到调用程序
execve
函数加载并运行可执行目标文件filename
,且带参数列表argv
和环境变量列表envp
。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。
argv[0]=可执行文件的名字
当
execve
加载了filename时,创建了一个内存映像。在程序头部表的引导下,加载器将可执行文件的片复制到代码段和数据段,接下来,加载器跳转到程序的入口点,也就是_start
函数的地址。
_start
函数调用系统启动函数__libc_start_main
,该函数定义在libc.so
中。它初始化执行环境,调用用户层的main
函数,int main(int argc,char *argc[],char *envp[])
。其中argc
是argv[]数组中非空指针的数目,argv
和envp
就是execve的那个参数。
最后处理main函数的返回值,并且在需要的时候把控制返回给内核
习题8.6:编写myecho程序,打印他的命令行参数和环境变量
//myecho.c
#include "csapp.c"
int main(int argc,char *argv[],char *envp[])
{
int i;
printf("Command-line arguments:\n");
for (i=0;argv[i]!=NULL;i++)
printf(" argv[%2d]:%s\n",i,argv[i]);
printf("\n");
printf("Environment variables:\n");
for (i=0;envp[i]!=NULL;i++)
printf(" envp[%2d]:%s\n",i,envp[i]);
exit(0);
}
wow,只要给main以argc,argv,envp参数,整个参数和环境变量都可以都这些参数直接拿来用。而且试验知,如果把argv啥的改名了,也有同样的效果。比如
int main(int a,char *b[],char *c[]);
Question:
那既然可以用main整个参数列表直接得到argv和envp,那为什么下面这个实现简单的shell不用这种方式…
/*
while (1)
1.printf(">");
2.stdin -> char cmdline[];
3.cmdline -> buf (后面都用buf,因为buf会修改,所以cmdline相当于备份);
4.parseline(buf,argv); 将buf解析成argv,return 是否后台执行
5.builtin_command(argv) 根据argv判断是否是内置命令,若是则在函数内部就execve,若不是则在外面execve
:相当于是设立优先级,优先按照内置命令处理,若不是内置命令的,才当作是用户程序尝试execve
6.若前台执行,需要waitpid来回收子进程,若后台执行,则printf子进程的号码以及之前输入的cmdline,然后不管子进程了
*/
/* $begin shellmain */
#include "csapp.c"
#define MAXARGS 128
/* function prototypes */
void eval(char *cmdline);
int parseline(char *buf, char **argv);
int builtin_command(char **argv);
int main()
{
char cmdline[MAXLINE]; /* Command line */
while (1) {
/* Read */
printf("> ");
Fgets(cmdline, MAXLINE, stdin); //这一步就是将在shell中输入的字符记录到cmdline数组中
/*
char *fgets(char *str, int n, FILE *stream);
从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。
当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
*/
if (feof(stdin))//这里好像是正常情况都返回0,只有遇到EOF,即在键盘中敲入Ctrl+D时返回非0
exit(0);
/*
int feof(FILE *stream);
文件结束:返回非0值;文件未结束:返回0值
*/
/* Evaluate */
eval(cmdline);
}
}
/* $end shellmain */
/* $begin eval */
/* eval - Evaluate a command line */
void eval(char *cmdline)
{
char *argv[MAXARGS]; /* Argument list execve() */
char buf[MAXLINE]; /* Holds modified command line 保留修改的命令行*/
int bg; /* Should the job run in bg(后台) or fg(前台)? */
pid_t pid; /* Process id */
strcpy(buf, cmdline);
bg = parseline(buf, argv); //解析命令行,传入buf,传入argv并根据buf来写入argv[0],[1]...,return bg
if (argv[0] == NULL)
return; /* Ignore empty lines */
if (!builtin_command(argv)) {//builtin_command:判断是否是内置的命令,若不是,则尝试去执行这个argv程序
if ((pid = Fork()) == 0) { /* Child runs user job */
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found.\n", argv[0]);
exit(0);
}
}
/* Parent waits for foreground job to terminate */
if (!bg) { //如果前台执行,是要回收子进程的
int status;
if (waitpid(pid, &status, 0) < 0)
unix_error("waitfg: waitpid error");
}
else //如果后台执行,就把子进程号码和命令输出即可
printf("%d %s", pid, cmdline);
}
return;
}
/* If first arg is a builtin command, run it and return true */
int builtin_command(char **argv)
{
if (!strcmp(argv[0], "quit")) /* quit command */
exit(0);
if (!strcmp(argv[0], "&")) /* Ignore singleton & */
return 1;
if (!strcmp(argv[0],"echo"))
{
argv[0] = "/usr/bin/echo";
pid_t pid;
if ((pid = Fork()) == 0) { /* Child runs user job */
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found.\n", argv[0]);
exit(0);
}
}
int status;
if (waitpid(pid, &status, 0) < 0)
unix_error("waitfg: waitpid error");
return 1;
}
return 0; /* Not a builtin command */
}
/* $end eval */
/* $begin parseline */
/* parseline - Parse the command line and build the argv array */
int parseline(char *buf, char **argv)
{
char *delim; /* Points to first space delimiter */
int argc; /* Number of args */
int bg; /* Background job? */
buf[strlen(buf)-1] = ' '; /* Replace trailing '\n' with space */
while (*buf && (*buf == ' ')) /* Ignore leading spaces */
buf++;
/* Build the argv list */
argc = 0;
while ((delim = strchr(buf, ' '))) {
argv[argc++] = buf;
*delim = '\0';
buf = delim + 1;
while (*buf && (*buf == ' ')) /* Ignore spaces */
buf++;
}
argv[argc] = NULL;
if (argc == 0) /* Ignore blank line */
return 1;
/* Should the job run in the background? */
if ((bg = (*argv[argc-1] == '&')) != 0)
argv[--argc] = NULL;
return bg;
}
/* $end parseline */