20145236 《信息安全系统设计基础》第11周学习总结
第八章 异常控制流
8.1异常
- 异常是异常控制流的一种形式,它一部分是由硬件实现的,一部分是有操作系统实现的。
- 异常:控制流中的突变,用来响应处理器状态中的某些变化。
- 在处理器中,状态被编码为不同的位和信号。状态变化成为事件。
- 异常表:当处理器监测到有时间发生时,通过一张叫做异常表的跳转表,进行一个间接过程调用,到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序)。
- 当异常处理程序完成处理后,根据引起异常的事件的类型,会发生以下三种情况的一种:
- 处理程序将控制返回给当前指令Icurr,即当事件发生时正在执行的指令。
- 处理程序将控制返回给Inext,即如果没有发生异常将会执行的下一条指令。
- 处理程序终止被中断的程序。
8.1.1 异常处理
- 系统中可能的每种类型的异常都分配了一个唯一的非负整数的异常号。
- 异常号的分配:
- 处理器的设计者:被除零、缺页、存储器访问违例、断点以及算数溢出。
- 操作系统内核的设计者分配的:系统调用和来自意外不I/O设备的信号。
- 异常号:到异常表中的索引
异常表基址寄存器:异常表的起始地址存放的位置。 - 异常与过程调用的异同:
- 过程调用时,在跳转到处理器之前,处理器将返回地址压入栈中。然而,根据异常的类型,返回地址要么是当前指令,要么是下一条指令。
- 处理器把一些额外的处理器状态压入栈里,在处理程序返回时,重新开始被中断的程序会需要这些状态。
- 如果控制从一个用户程序转移到内核,那么所有这些项目都被压到内核栈中,而不是压到用户栈中。
- 异常处理程序运行在内核模式下,意味着它们对所有的系统资源都有完全的访问权限。
8.1.2 异常的类别
- 异常的分类:中断、陷阱、故障和终止。
- 中断:异步发生,是来自处理器外部的I/O设备的信号的结果。 硬件异常中断处理程序通常称为中断处理程序。
- 异步异常是有处理器外部的I/O设备中的时间产生的,同步异常是执行一条指令的直接产物。
- 陷阱、故障、终止时同步发生的,是执行当前指令的结果,我们把这类指令叫做故障指令。
- 陷阱和系统调用
- 陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
- 普通的函数运行在用户模式中,用户模式限制了函数可以执行的指令的类型,而且它们只能访问与调用函数相同的栈。系统调用运行在内核模式中,内核模式允许系统调用执行指令,并访问定义在内核中的栈。
- 故障:是由错误情况引起的。
例如:abort例程会终止引起故障的应用程序。 根据故障是否能够被修复,故障处理程序要么重新执行引起故障的指令,要么终止。例如:缺页故障。 - 终止:是不可恢复的致命错误造成的结果,通常是一些硬件错误。终止处理程序从不将控制返回给应用程序。
8.1.3 linux/IA32系统中的异常
- 031号:由intel架构师定义的异常。32255号:操作系统定义的中断和陷阱。
- linux/IA32故障和终止:
- 除法错误(linux中称为浮点异常)
- 一般保护故障(linux中称为段故障)
- 缺页
- 机器检查
- linux/IA32系统调用
- 每一个系统调用都有一个唯一的整数号,对应于一个到内核中跳转表的偏移量。
- C程序用syscall函数可以直接调用任何系统调用。
- 系统级函数:系统调用和它们相关联的包装函数。
- linux系统调用的参数都是通过吉春器而不是栈传递的,寄存器%eax包含系统调用号,栈指针%esp不能使用,因为当进入内核调用时,内核会覆盖它。
8.2 进程
- 异常是允许操作系统提供进程的概念所需要的基本构造块。
- 进程:一个执行中的程序的实例。
- 上下文是由程序正确运行所需要的状态组成的,这个状态包括存放在存储器中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
- 进程提供给应用程序的关键抽象:
- 一个独立的逻辑控制流,独占地使用处理器;
- 一个私有的地址空间,独占地使用存储器系统。
8.2.1 逻辑控制流
程序计数器:唯一的对应于包含在程序的可执行目标文件中的指令,或者是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,简称逻辑流。
8.2.2 并非流
- 并发流:一个逻辑流的执行在时间上与另一个流重叠。
- 并发:多个流并发地执行的一般现象。
- 多任务:一个进程和其他进程轮流运行的概念。
- 时间片:一个进程执行它的控制流的一部分的每一时间段。
- 多任务也叫时间分片。
- 并行流:如果两个流并发的运行在不同的处理器核或者计算机上
8.2.3 私有地址空间
和这个空间中某个地址相关联的那个存储器字节是不能被其他进程读或者写的。所以这个空间地址是私有的。
8.2.4 用户模式和内核模式
- 模式位:用某个控制寄存器中的一个位模式,限制一个应用可以执行的指令以及它可以访问的地址空间范围。
- 当设置了位模式,进程就运行在内核模式中,一个运行在内核模式中的进程可以中兴指令集中的任何指令,而且可以访问系统中任何存储器位置。
- 没有设置位模式时,进程就运行在用户模式中,不允许执行特权指令,例如停止处理器、改变位模式,或者发起一个I/O操作。
- 用户程序必须通过系统调用接口间接的当问内核代码和数据。
- 进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障、或者陷入系统调用这样的异常。
8.2.5 上下文切换
-
上下文就是内核重新启动一个被抢占的进程所需的状态。
-
调度:内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。有内核中称为调度器的代码处理的。
-
上下文切换机制:
- 保存当前进程的上下文
- 恢复某个先前被抢占的进程被保存的上下文
- 将控制传递给这个新恢复的进程
-
引起上下文切换的情况
- 当内核代表用户执行系统调用时
- 中断时
8.3 系统调用错误处理
错误处理包装函数:包装函数调用基本函数,检查错误,如果有任何问题就终止。
8.4 进程控制
8.4.1 获取进程ID
- 每个进程都有一个唯一的正数的进程ID。
- getpid函数返回调用进程的PID,getppid函数返回它的父进程的PID。上面两个函数返回一个同类型为pid_t的整数值,在linux系统中,它在types.h中被定义为int。
8.4.2 创建和终止进程
-
进程总处于三种状态
- 运行:进程要么在CPU上执行,要么在等待被执行且最终会被内核调度。
- 停止:程序的执行被挂起,,且不会被调度。
- 终止:进程用永远停止了。终止原因:(1)收到一个信号,默认行为是终止进程;(2)从主进程返回(3)调用exit函数。
-
父进程通过调用fork函数创建一个新的运行的子进程。
-
子进程和父进程的异同:
- 异:有不同的PID
- 同:用户级虚拟地址空间,包括:文本、数据和bss段、堆以及用户栈。任何打开文件描述符,子进程可以读写父进程中打开的任何文件。
-
fork函数: 因为父进程的PID总是非零的,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行。
fork函数的特点:- 调用一次,返回两次
- 并发执行
- 相同的但是独立的地址空间
- 共享文件
8.4.3 回收子进程
- 当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程。
一个终止了但还未被回收的进程称为僵死进程。 - 一个进程可以通过调用waitpid函数来等待它的子进程终止或者停止。
#include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid,int *status,int options); //返回:若成功,返回子进程的PID;若WNOHANG,返回0;若其他错误,返回-1。
默认地,当option=0时,waitpid挂起调用进程的执行,直到它的等待集合中的一个子进程终止。
3. 判定等待集合的成员
有参数pid来确定的:
1. pid>0:等待集合是一个单独的子进程,进程ID等于pid。
2. pid=-1:等待结合就是由父进程所有的子进程组成的。
4. 修改默认行为
通过options设置:
1. WNOHANG:默认行为是挂起调用进程。
2. WUNTRACED:默认行为是只返回已终止的子进程。
3. WNOHANG|WUNTRACED:立即返回,如果等待集合中没有任何子进程被停止或者已终止,那么返回值为0,或者返回值等于那个被停止或者已经终止的子进程的PID。
5. 检查已回收子进程的退出状态
wait.h头文件定义了解释status参数的几个宏:
1. WIFEXITED:如果子进程通过调用exit或者一个返回正常终止,就返回真;
2. WEXITSTATUS:返回一个正常终止的子进程的退出状态。只有在WIFEXITED返回真时,才会定义这个状态。
6. 错误条件
1. 若调用进程没有子进程,那么waitpid返回-1,并且设置errno为ECHILD;
2. 若waitpid函数被一个信号中断,那么返回-1,并设置errno为EINTR
7.wait函数
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status); //返回:若成功,返回子进程的PID;若错误,返回-1。
调用wait(&status)等价于调用waitpid(-1.&status,0)
8.4.4 让进程休眠
- sleep函数:将进程挂起一段指定的时间
#include <unistd.h> unsigned int sleep(unsigned int secs); //返回:还要休眠的秒数
如果请求的时间量已经到了,返回0,否则返回还剩下的要休眠的秒数。
2. pause函数:让调用函数休眠,直到该进程收到一个信号。
#include <unistd.h> int pause(void); //返回:总是-1
8.4.5 加载并运行程序
- execve函数:在当前进程的上下文中加载并运行一个新程序。
#include <unistd.h> int execve(const char *filename,const char *argv[],const char *envp[]); //返回:若成功,则不返回,若错误,返回-1
filename:可执行目标文件
argv:带参数列表
envp:环境变量列表
特点:execve调用一次从不返回
2. getenv函数:在环境数组中搜素字符串“name =VALUE”,若找到了,就返回一个指向value的指针,否则它就返回NULL。
#include <stdlib.h>> char *getenv(const char *name); //返回:存在,返回指向name的指针,若无匹配的,为NULL
- 注意:
execve函数在当前进程的上下文中加载并运行一个新的进程。它会覆盖当前进程的地址空间,并没有创建一个新的进程,新的进程仍然有相同的PID,并且继承了调用execve函数时已打开的所有文件描述符。
8.4.6 利用fork和execve运行程序
- 外壳是一个交互型的应用级程序,它代表用户运行其他程序。
- 外壳执行一系统的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行,求值步骤解释命令行,并代表用户运行程序。
- eval函数:对外壳命令行求值
- parseline函数:解析外壳的一个输入
8.5 信号
底层的硬件异常是由内核异常处理程序处理的,正常情况下,对用户进程而言是不可见的。
其他信号对应于内核或者其他用户进程中较高层的软件事件。
8.5.1 信号术语
- 发送信号的两个不同步骤:
- 发送信号:内核通过更新目的进程上下文中的某个状态,发送(递送)一个信号给目的进程。
- 发送信号的两个原因:
- 内核监测到一个系统事件,比如被零除错误或者子进程终止。
- 一个进程调用了kill函数,显式地要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。
- 发送信号的两个原因:
- 接收信号:信号处理程序捕获信号的基本思想。
- 发送信号:内核通过更新目的进程上下文中的某个状态,发送(递送)一个信号给目的进程。
- 待处理信号:一个只发出而没有被接收的信号
一个进程可以有选择性地阻塞接收某种信号。
待处理信号不会被接收,直到进程取消对这种信号的阻塞。 - 一个待处理信号最多只能被接受一次,pending位向量:维护着待处理信号集合,blocked向量:维护着被阻塞的信号集合。
8.5.2 发送信号
- 进程组
每个进程都只属于一个进程组,进程组是由一个正整数进程组ID来标识的。getpgrp函数返回当前进程的进程组ID:默认地,一个子进程和它的父进程同属于一个进程组。 - 用/bin/kill/程序发送信号 一个为负的PID会导致信号被发送到进程组PID中的每个进程。
- 从键盘发送信号
作业:表示对一个命令行求值而创建的进程。外壳为每个作业创建一个独立的进程组。 - 用kill函数发送信号
进程通过调用kill函数发送信号给其他的进程。父进程用kill函数发送SIGKILL信号给它的子进程。 - 用alarm函数发送信号
在任何情况下,对alarm的调用都将取消任何待处理的闹钟,并且返回任何待处理的闹钟在被发送前还剩下的秒数。
8.5.3 接收信号
- 当内核从一个异常处理程序返回,准备将控制传递该进程p时,它会检查进程p的未被阻塞的待处理信号的集合。如果这个集合是非空的,那么内核选择集合中的某个信号k,并且强制p接收信号k。
- 进程可以通过使用signal函数修改和信号相关联的默认行为。 唯一例外是SIGSTOP和SIGKILL,它们的默认行为是不能被修改的。
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum,sighandler_t handler); //返回:若成功,返回指向前次处理程序的指针;若出错,为SIG_ERR
- signal函数改变和信号signum相关联的行为的三种方法:
- handler是SIG_ IGN,忽略类型为signum的信号;
- handler是SIG_ DFL,类型为signum的信号行为恢复为默认行为。
- 否则,handler就是用户定义的函数地址。这个函数称为信号处理程序。
- 设置信号处理程序:通过把处理程序的地址传递到signal函数从而改变默认行为。
- 捕获信号:调用信号处理程序。
- 处理信号:执行信号处理程序。
- 因为信号处理程序的逻辑控制流与主函数的逻辑控制流重叠,信号处理程序和主函数并发执行。
8.5.4 信号处理问题
- 待处理信号被阻塞:
- 待处理信号不会排队等待;
- 系统调用可以被中断:像read、wait、accept这样的系统调用潜在地会阻塞进程一段较长的时间,称为慢速系统调用。
注意:不可以用信号来对其他进程中发生的事件计数。
8.5.5 可移植的信号处理
信号处理语义的差异,是UNIX信号处理的一个缺陷。
8.5.6 显式地阻塞和取消阻塞信号
sigprocmask函数改变当前已阻塞信号的信号。
how的值:
1. SIG_ BLOCK :添加set中的信号到blocked中
2. SIG_ UNBLOCK:从blocked中删除set中的信号
3. SIG_ SETMASK:blocked = set
8.6 同步流以避免讨厌的并发错误
基本的问题是以某种方式同步并发流,从而得到最大的可行的交错的集合,每个可行的交错都能得到正确的结果。
非本地跳转
- 非本地跳转:不需要经过正常的调——返回序列。非本地跳转是通过setjmp和longjmp函数来提供的。
- setjmp函数在env缓冲区中保存当前调用环境,以供后面longjmp使用,并返回0,调用环境包括程序计数器、栈指针和通用目的寄存器。
- setjmp函数和longjmp函数的区别:
- setjmp函数只被调用一次,但返回多次;
- 当第一次调用setjmp,而调用环境保存在缓冲区env中时;
- 一次是为每个相应的longjmp调用 ;
- 另一方面,longjmp函数被调用一次,但从不返回。
- 非本地跳转的应用:
- 允许从一个深层嵌套的函数调用中立即返回,通常是由检测到某个错误情况引起的
- 一个信号信息处理程序分支到一个特殊的代码位置,而不是返回到被信号到达中断了的指令的位置。
对sigsetjmp函数的初始调用保存调用环境和信号的上下文。
8.7 操作进程的工具
- 异常控制流发生在计算机系统的各个层次,是计算机系统中提供并发的基本机制。
在硬件层,异常是由处理器中的事件触发的控制流中的突变。
返回到故障指令后面的那条指令。一条指令的执行可能导致故障和终止同时发生。
在操作系统层,内核用ECF提供进程的基本概念。
在应用层,C程序可以使用非本地跳转来规避正常的调用/返回栈规则,并且直接从一个函数分支到另一个函数。
学习中遇到的问题
- 课本P504的代码的第14行中的strchr是什么函数?
答:
char strchr(const char _Str,int _Val)
char strchr(char _Str,int _Ch)
头文件:#include <string.h>
功能:查找字符串s中首次出现字符c的位置
说明:返回首次出现c的位置的指针,返回的地址是被查找字符串指针开始的第一个与Val相同字符的指针,如果s中不存在c则返回NULL。
返回值:成功则返回要查找字符第一次出现的位置,失败返回NULL
- 不是很理解signal1代码的两个改进版signal2和signal3函数,为什么改完以后可以解决可移植性的问题,不是很懂,希望老师上课可以点拨一下。
代码实践部分
1.argv(文件夹):argtest.c和freemakeargv.c及makeargv.c
代码分析:
- 为什么是
* *myargv
?
经过查阅得知,比较准确的说法是: **
相当于二级指针,char **
就是指向字符型指针的指针。最常使用的地方就是 int main(int argc,char **argv)
,相当于int main(int argc,char *argv[])
。也就是说,可以看作是指向了字符串数组
- 为什么是
int makeargv(const char *s, const char *delimiters, char * **argvp)
把最后一个参数理解为向字符串数组取地址(从左到右,第一个代表取地址,后两个 **
代表上文中说过的字符串数组)
- 关于
strtok
函数?
strtok
函数用来将字符串分割成一个个片段,它的原型是char *strtok(charr s[],const char *delim)
。只要在s中遇到delim中包含的字符(不一定是delim),就把这个字符改成\0
。每次调用成功后返回的都是被分割出的片段的指针。
errno
与error
?
前者是记录系统最后一次错误的函数;后者是系统错误。
2. env文件夹-environ.c
代码分析:
setenv
函数的作用?
setenv
用来在本次函数运行的过程中增加或者修改环境变量。当最后一个参数不为0的时候,原来的内容会被修改为第二个参数所指的内容。
如图所示:先打印了一开始的初始环境变量,接着重新设置环境变量,并打印输出。
3.env文件夹-environvar.c
代码分析:
environ
变量是什么?
该变量指向一个叫environment
的字符串数组。包括USER(登录用户的名字),LOGNAME(与user类似),HOME(用户登录目录),LANG(地域名),PATH等
分析:将外部变量environ的内容打印出来,也就是把系统相关宏值,打印出来。
4.env文件夹-producer
代码运行结果:
5.env文件夹-testmf.c
代码运行结果:
结果如上图,说明创建失败了。
6.pipe文件夹-consumer.c
功能:判断是否打开文件流,并判断是否正常打开文件。
代码分析:
PIPE_BUF
的值是多少?
4096字节
memset
函数用法?
原型:memset(void *s,int ch,size_t n);
将s中前n个字节用ch替换并返回s
open
函数用法?
open(const char *pathname,int flags);第一个参数是欲打开的文件路径字符串,第二个参数是打开方式
FIFONAME
是什么?
这里需要补充一下fifo的含义,它是一种文件类型,可以通过查看文件stat结构中的stmode成员的值来判断文件是否是FIFO文件。fifo是用来在进程中使用文件来传输数据的,也具有管道特性,可以在数据读出的时候清除数据。
代码运行结果:
输出打开文件流的进程号,以及打开文件进程号,并返回打开文件的结果。并且可以输入消息。
7.pipe文件夹-pipe.c
代码运行结果:
相当于管道的作用
8.pipe文件夹-stdinredir1.c
代码运行结果:
如上图所示:先从标准输入输入3行信息,接着分别打印这三行信息,执行打开文件语句,若打开正常,则从文件中读取前三行信息。
stdinredir2.c
上图结果表示打开文件失败的情况。
9.pipe文件夹-testtty.c
代码分析:
write
函数
write(int handle,void *buf,int nbyte);
第一个参数是文件描述符,第二个参数是指向一端内存单元的指针,第三个参数是要写入指定文件的字节个数;成功时返回字节个数,否则返回-1。
代码运行结果:
将缓冲区中的内容打印出来
10.signal文件夹-sigactdemo1.c
代码分析:
sigaction
结构体,用来查询或设置信号处理方式。比如它指定了对特定信号的处理,信号所传递的信息,信号处理函数执行过程中应该屏蔽掉哪些函数等。
代码运行结果:
从上图的结果中我们可以看到,该代码的功能是,将标准输入的信息打印到标准输出上,需要强制退出结束进程。
11.signal文件夹-sigdemo1.c
代码运行结果:
signal文件夹-sigdemo2.c
每隔1s打印一次"haha"。
12.signal文件夹-sigdemo3.c
每隔2s打印一次hello。
13.exec1.c
代码分析:
execvp
函数:从PATH 环境变量所指的目录中查找符合参数file 的文件名,找到后便执行该文件,然后将第二个参数argv传给该欲执行的文件。
如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno中。
exevp函数调用成功没有返回,所以没有打印出“* * * ls is done. bye”这句话。
14.exec2.c
代码分析:
它与exec1的区别就在于exevp函数的第一个参数,exec1传的是ls,exec2直接用的arglist[0],由定义可得二者等价,运行结果相同。
14.exec3.c
代码分析:
execlp
函数?
从PATH环境变量中查找文件并执行。原型:int execlp(const char *file,const char *arg,……);
从PATH环境变量所指的目录中查找符号参数file的文件名,然后将第二个及以后的参数当作该文件的argv[0],argv[1],……,最后一个参数必须用NULL结束。
execv
函数?
原型:int execv(const char *pathname,char *const argv[]);
装入并运行其他程序 对比:execvp
函数原型: int execvp(const char *file,char *const argv[]);
注意:
exce族的区别
1. :这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。
2. 规律:
* 不带字母p(表示 path)的exec函数第一个参数必须是程序的相对路径或绝对路径,例如“/bin/ls”或“./a.out”,而不能是“ls”或“a.out”。
* 对于带字母p的函数: 如果参数中包含/,则将其视为路径名。否则视为不带路径的程序名,在PATH环境变量的目录列表中搜索这个程序。
* 带有字母l(表示list)的exec函数要求将新程序的每个命令行参数都当作一个参数传给它,命令行参数的个数是可变的,因此函数原型中有…,…中的最后一个可变参数应该是 NULL,起sentinel的作用。
* 对于带有字母v(表示vector)的函数,则应该先构造一个指向各参数的指针数组,然后将该数组的首地址当作参数传给它,数组中的最后一个指针也应该是NULL,就像main函数的argv参数或者环境变量表一样。
3. 对于以e(表示environment)结尾的exec函数,可以把一份新的环境变量表传给它,其他exec函数仍使用当前的环境变量表执行新程序。
4. 事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve,所以execve在man手册第2节,其它函数在man手册第3节。
15. forkdemo1.c
代码分析:
fork函数: :将运行着的程序分成2个(几乎)完全一样的进程,每个进程都启动一个从代码的同一位置开始执行的线程。若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;否则,出错返回-1。
从图中可以看出,After打印语句打印了两次,第一次打印的After语句是父进程执行的,因为fork函数的返回值不是0,说明是父进程在执行,第二次打印的After语句是子进程执行的,因为fork函数的返回值是0,而我的id是子进程id。
16.forkdemo2.c
代码分析:
因为执行了两次fork函数,执行第一次,分出2个线程,执行第二次,之前的两个线程分别分出2个线程,所以一共是四个线程,最终出现4次After语句。
17.forkdemo3.c
代码分析:
fork函数会将一个进程分成两个进程,并且会返回两次,所以如上图所示,我们可以看到,出现了一次“I am the parent. my child is 4954”,又出现了一次“I am the parent. my child is 4954”。这个c文件,还包括了错误处理,提高了代码的健壮性。
18.forkdemo4.c
代码分析:
先打印进程pid,然后fork创建子进程,父进程返回子进程pid,所以输出parent一句,休眠十秒;子进程返回0,所以输出child与之后一句。
19.forkgdb.c
代码分析:
父进程打印是先打印两句,然后休眠一秒,然后打印一句,子进程先打印一句,然后休眠一秒,然后打印两句。并且这两个线程是并发的,所以可以看到在一个线程休眠的那一秒,另一个线程在执行,并且线程之间相互独立互不干扰。
20.psh1.c
代码分析:
首先while循环输入命令,并将输入的值转换为字符串型,直到输入回车换行时,调用execute函数,将存储命令的数组作为参数传入,实现执行指令的功能。
运行结果中失败的情况,分别显示不同错误的处理方式不同。
21.psh2.c
代码分析:
与psh1对比,多了循环判断,不退出的话就会一直要你输入指令,并且对于子程序存在的状态条件
22.testbuf1.c
功能:打印hello,但没有结束进程,若此时向标准输入设备中输入数据,屏幕上会显示
代码分析:
打印hello,但没有结束进程,若此时向标准输入设备中输入数据,屏幕上会显示出来。必须用户强制退出,才能退出程序。
23.testbuf2.c
功能:同testbuf1.c一样
24.testbuf3.c
功能:比较打印的差别
25.testpid.c
功能:显示进程的id
26.testsystem.c
system函数:执行shell命令,也就是向dos发送一条指令。这里是后面可以跟两个参数,然后向dos发送这两个命令,分别执行。
27.waitdemo1.c
功能:验证父子进程的调用顺序,测试函数sleep、wait在进程调用中的作用。
代码分析:
我们可以看到hildcode函数里,调用了sleep函数,这表示执行完printf("child %d here. will sleep for %d seconds\n", getpid(), delay);语句后,系统休眠4s继续进行。
为什么这里不会让父进程继续进行?本来父子进程时并发执行的,按理说应该子进程休眠,父进程正常执行的,是因为parentcode函数里的wait_rv = wait(NULL);代码,说明要子程序执行完毕,父进程才能继续往下进行。
28.waitdemo2.c
功能:在waitdemo1.c的基础上,设置了状态位。
代码分析:
waitdemo2.c和waitdemo1.c最大的不同就是:设置了子进程结束后父进程的状态位。
代码实践中遇到的问题
-
不是说fork函数,产生的两个线程的执行顺序是不确定的,为什么我多次执行,都是先显示父进程的,如图所示:
-
argv文件夹下的c文件都不能正常编译运行,例如,argtest.c编译出现错误。
代码托管
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第一周 | 200/200 | 2/2 | 20/20 | |
第二周 | 200/400 | 2/4 | 18/38 | |
第三周 | 100/500 | 1/5 | 10/48 | |
第四周 | 250/750 | 1/6 | 10/58 | |
第五周 | 100/850 | 1/7 | 10/68 | |
第六周 | 100/950 | 1/8 | 12/80 | |
第七周 | 200/1150 | 1/9 | 12/92 | |
第八周 | 124/1274 | 2/11 | 10/102 | |
第九周 | 205/1479 | 2/13 | 5/107 | |
第十周 | 333/1712 | 2/15 | 10/117 | |
第十一周 | 758/2470 | 3/18 | 12/129 |