20145305 《信息安全系统设计基础》第11周学习总结
教材学习内容总结
第八章 异常控制流
第一节 异常
1.产生:处理器中的变化(事件)触发从应用程序到异常处理程序的突发的控制转移,也就是异常
2.类型:被零除,缺页,存储器访问违例,断点,算术溢出;系统调用,来着外部I/O设备的信号
3.处理:在任何情况下,当处理器检测到有事件发生时,它就会通过一张叫做异常表的跳转表进行一个间接过程调用,到一个专门处理这类时间的操作系统子程序(异常处理程序)
4.结果:当 exception handler处理结束之后,会有三种结果:
处理程序将控制返回给事件发生的时候正在执行的指令
处理程序将控制返回给如果没有发生异常将会执行的下一条指令
处理程序终止被终端的程序
(1)中断
(2)陷阱
(3)故障
(4)终止
5.Linux/IA32系统中的异常一共有256种
(1)Linux/IA32故障和终止
(2)Linux/IA32系统调用
系统调用的实现方法:
在IA32中,系统调用通过一条陷阱指令提供:
所有的到Linux系统调用的参数都是通过寄存器传递的。惯例如下:
%eax:包含系统调用号
%ebx,%ecx,%edx,%esi,%edi,%ebp:包含最多六个任意参数
%esp:栈指针,不能用
第二节 进程
进程定义:一个执行中的程序的实例
系统中的每个程序都是运行在某个进程的上下文中的
※上下文:由程序正确运行所需的状态组成的
进程提供给应用程序的关键抽象:
一个独立的逻辑控制流:独占的使用处理器
一个私有的地址空间:独占的使用存储器系统
1.逻辑控制流
(1)进程计数器(PC)中的每一个值都唯一地对应于包含在程序的可执行目标文件中的指令,或者是包含在运行时动态链接的到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流。
(2)进程是轮流使用处理器的;每个进程执行它的流的一部分然后被挂起,其他进程执行。
(3)对于一个运行在其中一个进程上下文中的程序而言,它看上去就像是唯一地占用了处理器
2.并发流
(1)含义
一个逻辑流的执行在时间上与另一个流重叠。
这两个流并发的运行。
(2)几个概念:并发、多任务、时间片
(3)并行
两个流并发的运行在不同的处理机核或者计算机上。
并行流并行的运行,并行的执行。
3.私有地址空间
进程为程序提供的假象,好像它独占的使用系统地址空间。一般而言,和这个空间中某个地址相关联的那个存储器字节是不能被其他进程读写的。
4.用户模式和内核模式
区别:有无模式位,有的话就是内核模式,可以执行指令集中的所有指令,访问系统中任何存储器位置;没有就是用户模式。
5.上下文切换
操作系统内核使用上下文切换这种较高层形式的异常控制流来实现多任务。上下文切换机制建立在较底层异常机制之上。
(1)上下文:内核重新启动一个被抢占的进程所需的状态。
由一些对象的值组成:、通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈
内核数据结构:页表、进程表、文件表
(2)调度和调度器
(3)上下文切换机制
(4)可能发生上下文切换的原因
第三节 系统调用错误处理
所有到linux的系统调用的参数都是通过通用寄存器而非栈来传递。按照惯例,寄存器%eax包含系统调用号,寄存器%ebx,%ecx,%edx,%esi和%ebp包含最多六个参数。%esp不适用,因为当进入内核模式的时候,内核会覆盖它。
举例说明 【以hello程序为例】
int main()
{
write(1,"hello world\n",13);//用系统调用来写write函数
exir(0);
}
第四节 进程控制
一、获取进程ID
每个进程都有一个唯一的正数进程ID(PID)
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); 返回调用进程的PID
pid_t getppid(void); 返回父进程的PID(创建调用进程的进程)
二、创建和终止进程
1.进程三种状态:运行、停止、终止
2.创建进程
父进程通过调用fork函数来创建一个新的运行子进程。fork函数定义如下:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
fork函数只被调用一次,但是会返回两次:父进程返回子进程的PID,子进程返回0.如果失败返回-1.
图8-15的代码为:
/* $begin fork */
#include "csapp.h"
int main()
{
pid_t pid;
int x = 1;
pid = Fork(); //line:ecf:forkreturn
if (pid == 0) { /* Child */
printf("child : x=%d\n", ++x); //line:ecf:childprint
exit(0);
}
/* Parent */
printf("parent: x=%d\n", --x); //line:ecf:parentprint
exit(0);
}
/* $end fork */
3.终止进程
用exit函数。
#include <stdlib.h>
void exit(int status);
exit函数以status退出状态来终止进程。
三、回收子进程
含义:当一个进程由于某种原因终止的时候,内核并不是把它从系统中清除,而是保持在已经终止的状态中,直到被它的父进程回收。这时,内核将子进程的退出状态传递给父进程,然后抛弃已经终止的进程。这之后,该进程才可以说是“不存在”了。
补充:已经终止但是尚未被回收的进程叫做僵死进程。
特例:如果父进程没有回收它的子进程就终止了,那么内核就会安排init函数来回收它们,init函数的返回值是1。waitpid函数的定义如下:
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
成功返回子进程PID,如果WNOHANG,返回0,其他错误返回-1.
1.判断等待集合的成员——pid
2.修改默认行为——options
3.检查已回收子进程的退出状态——status
4.错误条件
5.wait函数
wait函数是waitpid函数的简单版本,wait(&status)等价于waitpid(-1,&status,0).
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
成功返回子进程pid,出错返回-1
四、让进程休眠
1.sleep函数
sleep函数使一个进程挂起一段指定的时间。
2.pause函数
让调用函数休眠,直到该进程收到一个信号。
代码实践内容总结
argv
argv文件夹中的主要程序是argtest.c
采取了两种运行方法:
第一种是用-c将所有.c的文件编译成后缀为.o的文件,然后一起编译成可执行文件
第二种方法我尝试直接将这些所有.c的文件编译成可执行文件,运行结果是一样的
如图是Usage: 命令字符 string
代码分析
argtest.c
#include <stdio.h>
#include <stdlib.h>
#include "argv.h"//该函数库中包括freemakeargv.c及makeargv.c函数的调用
int main(int argc, char *argv[])
{
char delim[] = " \t";//制表符
int i;
char **myargv;//见下方解释
int numtokens;
if (argc != 2)//如果输入的命令字符个数不等于2,就输出标准错误
{
fprintf(stderr, "Usage: %s string\n", argv[0]);
return 1;
}
if ((numtokens = makeargv(argv[1], delim, &myargv)) == -1)
{
fprintf(stderr, "Failed to construct an argument array for %s\n", argv[1]);//翻译过来就是无法构造一个参数数组
return 1;
}
printf("The argument array contains:\n");
for (i = 0; i < numtokens; i++)
printf("%d:%s\n", i, myargv[i]);
execvp(myargv[0], myargv);
return 0;
}
makeargv.c
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include "argv.h"
int makeargv(const char *s, const char *delimiters, char ***argvp)//见下方解释
{
int error;
int i;
int numtokens;
const char *snew;
char *t;
if ((s == NULL) || (delimiters == NULL) || (argvp == NULL))
{
errno = EINVAL;
return -1;
}
*argvp = NULL;//把字符串数组置为空
snew = s + strspn(s, delimiters);//返回字符串s开头连续包含字符串delimiters内的字符数目
if ((t = malloc(strlen(snew) + 1)) == NULL)
return -1;
strcpy(t, snew);
numtokens = 0;
if (strtok(t, delimiters) != NULL)//关于strtok函数的用法见下方
for (numtokens = 1; strtok(NULL, delimiters) != NULL; numtokens++) ;
if ((*argvp = malloc((numtokens + 1)*sizeof(char *))) == NULL)
{//malloc函数请求分配了numtokens+1个字节的空间;如果是返回值是NULL说明分配不成功
error = errno;
free(t);//释放malloc函数给指针变量分配的内存空间的函数。使用后该指针变量一定要重新指向NULL。
errno = error;
return -1;
}
if (numtokens == 0)
free(t);
else
{
strcpy(t, snew);
**argvp = strtok(t, delimiters);
for (i = 1; i < numtokens; i++)
*((*argvp) + i) = strtok(NULL, delimiters);
}
*((*argvp) + numtokens) = NULL;
return numtokens;
}
1.为什么是* *myargv?
**相相当于二级指针,char **就是指向字符型指针的指针。最常使用的地方就是 int main(int argc,char **argv),相当于int main(int argc,char *argv[])。也就是说,可以看作是指向了字符串数组
2.为什么是 int makeargv(const char *s, const char *delimiters, char * **argvp)?
把最后一个参数理解为向字符串数组取地址(从左到右,第一个代表取地址,后两个 **代表上文中说过的字符串数组)
3.关于strtok函数?
strtok函数用来将字符串分割成一个个片段,它的原型是char *strtok(charr s[],const char *delim)。只要在s中遇到delim中包含的字符(不一定是delim),就把这个字符改成\0。每次调用成功后返回的都是被分割出的片段的指针。
4.errno与error?
前者是记录系统最后一次错误的函数;后者是系统错误
env
environ.c
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
printf("PATH=%s\n", getenv("PATH"));//getenv函数用来取得参数PATH环境变量的值,执行成功则返回该内容的指针
setenv("PATH", "hello", 1);//见下方解释
printf("PATH=%s\n", getenv("PATH"));
#if 0
printf("PATH=%s\n", getenv("PATH"));
setenv("PATH", "hellohello", 0);
printf("PATH=%s\n", getenv("PATH"));
printf("MY_VER=%s\n", getenv("MY_VER"));//版本
setenv("MY_VER", "1.1", 0);
printf("MY_VER=%s\n", getenv("MY_VER"));
#endif
return 0;
}
getenv函数
1.获得环境变量值的函数
2.参数是环境变量名name,例如”HOME”或者”PATH”。如果环境变量存在,那么getenv函数会返回环境变量值,即value的首地址;如果环境变量不存在,那么getenv函数返回NULL
setenv函数
1.修改或添加环境变量的函数
2.将name设置成value
1.如果name在环境中不存在,那么很好办,在环境中添加这个新的变量就OK。
setenv函数必须在environment list中增加一个新的entry,然后动态申请存储空间来存储name=value,并且使entry指向该空间。
2.如果在环境中name已经存在,那么
(a)若overwrite非0,那么更新name的value(实质是更新环境表,指向新的value)
(b)若overwrite为0,则环境变量name不变,并且也不出错
setenv函数不必在environment list中增加一个新的entry。当overwrite为0, 则不必改动entry的指向;当overwrite非0, 则直接使该entry指向name=value,当然该name=value也是存储在动态申请的内存里。
environvar.c
简单打印环境变量表
extern char **environ;
每个程序都有一个环境表,它是一个字符指针数组,其中每个指针包含一个以NULL结尾的C字符串的地址。全局变量environ则包含了该指针数组的地址
fifo
1.FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。
2.FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。
3.FIFO往往都是多个写进程,一个读进程。
consumer.c管道写端
1.PIPE_BUF的值是多少?
4096字节
2.memset函数用法?
原型:memset(void *s,int ch,size_t n);将s中前n个字节用ch替换并返回s
3.open函数用法?
open(const char *pathname,int flags);第一个参数是欲打开的文件路径字符串,第二个参数是打开方式
4.FIFONAME是什么?
这里需要补充一下fifo的含义,它是一种文件类型,可以通过查看文件stat结构中的stmode成员的值来判断文件是否是FIFO文件。fifo是用来在进程中使用文件来传输数据的,也具有管道特性,可以在数据读出的时候清除数据。
producer.c管道读端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FIFO_NAME "/tmp/myfifo"
#define BUFFER_SIZE PIPE_BUF
#define TEN_MEG (1024 * 1024 * 10)
int main()
{
int pipe_fd;
int res;
int open_mode = O_WRONLY;
int bytes = 0;
char buffer[BUFFER_SIZE + 1];
if (access(FIFO_NAME, F_OK) == -1) {//检查文件是否有相应的权限
res = mkfifo(FIFO_NAME, 0777);//依据FIFO_NAME创建fifo文件,0777依次是相应权限
if (res != 0) {
fprintf(stderr, "Could not create fifo %s \n",
FIFO_NAME);
exit(EXIT_FAILURE);
}
}
printf("Process %d opening FIFO O_WRONLY\n", getpid());
pipe_fd = open(FIFO_NAME, open_mode);
printf("Process %d result %d\n", getpid(), pipe_fd);
if (pipe_fd != -1) {
while (bytes < TEN_MEG) {
res = write(pipe_fd, buffer, BUFFER_SIZE);
if (res == -1) {
fprintf(stderr, "Write error on pipe\n");
exit(EXIT_FAILURE);
}
bytes += res;
}
close(pipe_fd);
} else {
exit(EXIT_FAILURE);
}
printf("Process %d finish\n", getpid());
exit(EXIT_SUCCESS);
}
testmf.c 演示了mkfifo函数的用法
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()//就是创建fifo文件
{
int res = mkfifo("/tmp/myfifo", 0777);
if (res == 0) {
printf("FIFO created \n");
}
exit(EXIT_SUCCESS);
}
mkfifo()
会依参数pathname建立特殊的FIFO文件,该文件必须不存在,而参数mode为该文件的权限(mode%~umask),因此 umask值也会影响到FIFO文件的权限。Mkfifo()建立的FIFO文件其他进程都可以用读写一般文件的方式存取。当使用open()来打开 FIFO文件时,O_NONBLOCK旗标会有影响
1.当使用O_NONBLOCK 旗标时,打开FIFO 文件来读取的操作会立刻返回,但是若还没有其他进程打开FIFO 文件来读取,则写入的操作会返回ENXIO 错误代码。
2.没有使用O_NONBLOCK 旗标时,打开FIFO 来读取的操作会等到其他进程打开FIFO文件来写入才正常返回。同样地,打开FIFO文件来写入的操作会等到其他进程打开FIFO 文件来读取后才正常返回。
pipe
调用pipe来创建管道并将其两端连接到两个文件描述符,array[0]为读数据端的文件描述符,而array[1]则为写数据端的文件描述符,内部则隐藏在内核中,进程只能看到两个文件描述符。
listargs.c 证明了shell并不将重定向标记和文件名传递给程序
pipe.c
#include <stdio.h>
#include<stdlib.h>
#include <unistd.h>
#define oops(m,x) //当linux系统执行代码遇到问题时,就会报告oops
{ perror(m); exit(x); }
int main(int ac, char **av)
{
int thepipe[2], newfd,pid;
if ( ac != 3 ){//输入的命令长度不等于3
fprintf(stderr, "usage: pipe cmd1 cmd2\n");
exit(1);
}
if ( pipe( thepipe ) == -1 ) //以下是各种错误
oops("Cannot get a pipe", 1);
if ( (pid = fork()) == -1 )
oops("Cannot fork", 2);
if ( pid > 0 ){
close(thepipe[1]);
if ( dup2(thepipe[0], 0) == -1 )
oops("could not redirect stdin",3);
close(thepipe[0]);
execlp( av[2], av[2], NULL);
oops(av[2], 4);
}
close(thepipe[0]);
if ( dup2(thepipe[1], 1) == -1 )
oops("could not redirect stdout", 4);
close(thepipe[1]);
execlp( av[1], av[1], NULL);
oops(av[1], 5);
}
pipedemo.c 管道
展示了如何创建管道并使用管道来向自己发送数据
#include <stdio.h>
#include<stdlib.h>
#include<string.h>
#include <unistd.h>
int main()
{
int len, i, apipe[2]; //apipe数组中存储两个文件的描述符
char buf[BUFSIZ];
if ( pipe ( apipe ) == -1 ){
perror("could not make pipe");
exit(1);
}
printf("Got a pipe! It is file descriptors: { %d %d }\n",
apipe[0], apipe[1]);
while ( fgets(buf, BUFSIZ, stdin) ){ //从标准输入读入数据,放到缓冲区
len = strlen( buf );
if ( write( apipe[1], buf, len) != len ){
//向apipe[1](即管道写端)写入数据
perror("writing to pipe");
break;
}
for ( i = 0 ; i<len ; i++ ) //清理缓冲区
buf[i] = 'X' ;
len = read( apipe[0], buf, BUFSIZ ) ; //从apipe[0](即管道读端)读数据
if ( len == -1 ){
perror("reading from pipe");
break;
}
if ( write( 1 , buf, len ) != len ){ //把从管道读出的数据再写到标准输出
perror("writing to stdout");
break;
}
}
}
想想who|sort是怎么实现的。who把输出送给stdout,sort从stdin中读入数据,那也就是说who的stdout和sort的stdin连成了一个。
result=pipe(int array[2]);array[0]是读端的文件描述符,array[1]是写端的文件描述符。
pipe调用首先获得两个“最低可用文件描述符”,赋给array[0]和array[1],然后再把这两个文件描述符连接起来。
pipedemo2.c 使用管道向自己发送数据
说明了如何将pipe和fork结合起来,创建一对通过管道来通信的进程
#include <stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#define CHILD_MESS "I want a cookie\n"
#define PAR_MESS "testing..\n"
#define oops(m,x) { perror(m); exit(x); } //还可以这样宏定义语句块
main()
{
int pipefd[2];
int len;
char buf[BUFSIZ];
int read_len;
if ( pipe( pipefd ) == -1 ) // 创建一个管道:apipe[0]读,apipe[1]写
oops("cannot get a pipe", 1);
switch( fork() ){
case -1:
oops("cannot fork", 2);
case 0:
len = strlen(CHILD_MESS);
while ( 1 ){
if (write( pipefd[1], CHILD_MESS, len) != len )
oops("write", 3);
sleep(5);
}
default:
len = strlen( PAR_MESS );
while ( 1 ){
if ( write( pipefd[1], PAR_MESS, len)!=len )
oops("write", 4);
sleep(1);
read_len = read( pipefd[0], buf, BUFSIZ );
if ( read_len <= 0 )
break;
write( 1 , buf, read_len );
}
}
}
在程序中。显示来从键盘到进程,从进程到管道,再从管道到进程以及从进程回到终端的数据传输流。
stdinredir1.c 将stdin定向到文件
#include <stdio.h>
#include <fcntl.h>
int main()
{
int fd ;
char line[100];
fgets( line, 100, stdin ); printf("%s", line );
fgets( line, 100, stdin ); printf("%s", line );
fgets( line, 100, stdin ); printf("%s", line );
close(0); // 关闭标准输入流
fd = open("/etc/passwd", O_RDONLY); // 打开文件,重定向
if ( fd != 0 ){
fprintf(stderr,"Could not open data as fd 0\n");
exit(1);
}
fgets( line, 100, stdin ); printf("%s", line );
fgets( line, 100, stdin ); printf("%s", line );
fgets( line, 100, stdin ); printf("%s", line );
}
close-then-open
stdinredir2.c
#include <stdio.h>
#include<stdlib.h>
#include <fcntl.h>
//#define CLOSE_DUP
//#define USE_DUP2
main()
{
int fd ;
int newfd;
char line[100];
fgets( line, 100, stdin ); printf("%s", line );
fgets( line, 100, stdin ); printf("%s", line );
fgets( line, 100, stdin ); printf("%s", line );
fd = open("data", O_RDONLY); // 首先打开文件fd,得到3
#ifdef CLOSE_DUP
close(0); // 关闭文件标志符0,即stdin
newfd = dup(fd);
#else
newfd = dup2(fd,0);
#endif
if ( newfd != 0 ){
fprintf(stderr,"Could not duplicate fd to 0\n");
exit(1);
}
close(fd); // 关闭fd
fgets( line, 100, stdin ); printf("%s", line );
// 从stdin=0获取字符串,此时0标记的是fd'
fgets( line, 100, stdin ); printf("%s", line );
fgets( line, 100, stdin ); printf("%s", line );
}
open..dup2..close
只是dup2(fd,0)将close(0),dup(fd)合在一起
testtty.c
write函数
write(int handle,void *buf,int nbyte); 第一个参数是文件描述符,第二个参数是指向一端内存单元的指针,第三个参数是要写入指定文件的字节个数;成功时返回字节个数,否则返回-1。
whotofile.c 重定向到文件
共有3个基本的概念,利用它们是的Unix下的程序可以轻易地将标准输入、输出和错误信息输出连接到文件:
1.标准输入、输出以及错误输出分别对应于文件描述符0、1、2
2.内核总是使用最低可用文件描述符
3.文件描述符集合通过exec调用传递,且不会被改变
signal
sigactdemo.c
#include <stdio.h>
#include<unistd.h>
#include <signal.h>
#define INPUTLEN 100
void inthandler();
int main()
{
struct sigaction newhandler;//见下方解释
sigset_t blocked; //信号集,用来描述信号的集合,与信号阻塞相关函数配合使用
char x[INPUTLEN];
newhandler.sa_handler = inthandler; //函数指针
newhandler.sa_flags = SA_RESTART|SA_NODEFER
|SA_RESETHAND; //sa_flags是一个位掩码。这里,第一个参数使得被信号打断的一些原语“正常返回”
sigemptyset(&blocked);
sigaddset(&blocked, SIGQUIT);
newhandler.sa_mask = blocked;
if (sigaction(SIGINT, &newhandler, NULL) == -1)
perror("sigaction");
else
while (1) {
fgets(x, INPUTLEN, stdin);
printf("input: %s", x);
}
return 0;
}
void inthandler(int s)
{
printf("Called with signal %d\n", s);
sleep(s * 4);
printf("done handling signal %d\n", s);
}
参数结构sigaction定义如下
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}
flag
SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号
函数sigaction
int sigaction(int signum,const struct sigaction *act ,struct sigaction *oldact);
sigaction()会依参数signum指定的信号编号来设置该信号的处理函数。参数signum可以指定SIGKILL和SIGSTOP以外的所有信号。
sigactdemo2.c
休息seconds秒后返回;或者被信号中断且信号处理函数返回后sleep()返回0。所以如果不计较返回值的话,pause()的功能相当于无限期的sleep()。
sigdemo1.c
连续输出五个hello,每两个间隔是两秒
在这期间,每次输入的Ctrl+C都被处理成打印OUCH
sigdemo2.c
一直输出haha,按Ctrl+C不能停止。
SIG_DFL,SIG_IGN 分别表示无返回值的函数指针,指针值分别是0和1,这两个指针值逻辑上讲是实际程序中不可能出现的函数地址值。
SIG_DFL:默认信号处理程序
SIG_IGN:忽略信号的处理程序
sigdemo3.c
#include <stdio.h>
#include<string.h>
#include <signal.h>
#include<unistd.h>
#define INPUTLEN 100
int main(int argc, char *argv[])
{
void inthandler(int);
void quithandler(int);
char input[INPUTLEN];
int nchars;
signal(SIGINT, inthandler);//^C
signal(SIGQUIT, quithandler);//^\
do {//输入什么,就输出什么(在read函数不发生错误的情况下)
printf("\nType a message\n");
nchars = read(0, input, (INPUTLEN - 1));
if (nchars == -1)
perror("read returned an error");
else {
input[nchars] = '\0';
printf("You typed: %s", input);
}
}
while (strncmp(input, "quit", 4) != 0);//只有输入quit的时候才会退出
return 0;
}
void inthandler(int s)
{
printf(" Received signal %d .. waiting\n", s);
sleep(2);
printf(" Leaving inthandler \n");
}
void quithandler(int s)
{
printf(" Received signal %d .. waiting\n", s);
sleep(3);
printf(" Leaving quithandler \n");
}
多信号处理SIGX打断SIGX的情况
exec1
execvp()会从PATH 环境变量所指的目录中查找符合参数file 的文件名,找到后便执行该文件,然后将第二个参数argv传给该欲执行的文件。
如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno中。
exevp函数调用成功没有返回,所以没有打印出“* * * ls is done. bye”这句话
exec2
它与exec1的区别就在于exevp函数的第一个参数,exec1传的是ls,exec2直接用的arglist[0],不过由定义可得这两个等价,所以运行结果是相同的。
exec3
int execlp(const char * file,const char * arg,....);
execlp()会从PATH 环境变量所指的目录中查找符合参数file的文件名,找到后便执行该文件,然后将第二个以后的参数当做该文件的argv[0]、argv[1]……,最后一个参数必须用空指针(NULL)作结束。
指定了环境变量,然后依然执行了ls -l指令,成功后没有返回,所以最后一句话不会输出。运行结果同exec1。
forkdemo1
这个代码先是打印进程pid,然后调用fork函数生成子进程,休眠一秒后再次打印进程id,这时父进程打印子进程pid,子进程返回0。
父进程通过调用fork函数创建一个新的运行子进程。
调用一次,返回两次。一次返回到父进程,一次返回到新创建的子进程。
forkdemo2
这个代码调用两次fork,一共产生四个子进程,所以会打印四个aftre输出。
forkdemo3
fork产生子进程,父进程返回子进程pid,不为0,所以输出父进程的那句话,子进程返回0,所以会输出子进程那句话。
forkdemo4
先打印进程pid,然后fork创建子进程,父进程返回子进程pid,所以输出parent一句,休眠十秒;子进程返回0,所以输出child与之后一句。
forkgdb
父进程打印是先打印两句,然后休眠一秒,然后打印一句,子进程先打印一句,然后休眠一秒,然后打印两句。并且这两个线程是并发的,所以可以看到在一个线程休眠的那一秒,另一个线程在执行,并且线程之间相互独立互不干扰。
psh1
依次你输入要执行的指令与参数,回车表示输入结束,然后输入的每个参数对应到函数中,再调用对应的指令。
第一个是程序名,然后依次是程序参数。
一个字符串,一个字符串构造参数列表argist,最后在数组末尾加上NULL
将arglist[0]和arglist数组传给execvp。
程序正常运行,execvp命令指定的程序代码覆盖了shell程序代码,并在命令结束之后退出,shell就不能再接受新的命令。
psh2
比起psh1多了循环判断,不退出的话就可以一直保持在输入指令,并且对于子程序存在的状态条件。
为了解决这个问题,程序通过调用fork来复制自己。
调用fork函数之后内核的工作过程:
分配新的内存块和内核数据结构
复制原来的进程到新的进程
向运行进程集添加新的进程
将控制返回给两个进程
testbuf1
效果是先输出hello,然后保持在循环中不结束进程。
testbuf2
fflush(stdout)的效果和换行符\n是一样的。
testbuf3
将内容格式化输出到标准错误、输出流中。
testpid
输出当前进程pid和当前进程的父进程的pid。
testpp
问题在于没给pp分配空间就调用了pp[0],毕竟声明的时候只是一个指针,而指针必须要初始化。
应该改成:
include <stdio.h>
include <stdlib.h>
int main()
{
char pp;
pp = (char)malloc(20);
pp[0] = (char*)malloc(20);
return 0;
}
testsystem
system()——执行shell命令,也就是向dos发送一条指令。这里是后面可以跟两个参数,然后向dos发送这两个命令,分别执行。
waitdemo1
如果有子进程,则终止子进程,成功返回子进程pid。
waitdemo2.c 获取子进程状态
多了一个子进程的状态区分,把状态拆分成三块,exit,sig和core。
wait阻塞调用它的程序直到子进程结束,返回结束进程的PID,父进程通过传给wait的参数中获取子进程以何种方式退出。如果子进程调用exit退出,那么内核把exit的返回值存放到这个整数变量中的高八位,如果进程是被杀死的,那么内核将信号序号存放在这个变量的低7位,中间一位用来指明发生错误并产生了core dump。
问题和解决过程
1.在linux中执行.sh文件
首先你要让文件有能够执行的权限,比如你的文件是a.sh那么你可以 chmod +x a.sh 然后运行文件就可以了 ./a.sh 这样运行是a.sh在当前工作目录,如果文件没在当前目录,那么就需要用绝对路径来执行,比如 /opt/a.sh /opt/test/a.sh
2.关于指针数组与数组指针的区别
数组指针(也称行指针)
定义 int (*p)[n]😭)优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。
如要将二维数组赋给一指针,应这样赋值:
int a[3][4];
int (*p)[4]; //该语句是定义一个数组指针,指向含4个元素的一维数组。
p=a;//将该二维数组的首地址赋给p,也就是a[0]或&a[0][0]
p++; //该语句执行过后,也就是p=p+1;p跨过行a[0][]指向了行a[1][]
数组指针也称指向一维数组的指针,亦称行指针。
指针数组
定义 int p[n];[]优先级高,先与p结合成为一个数组,再由int说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素,这样赋值是错误的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]...p[n-1],而且它们分别是指针变量可以用来存放变量地址。但可以这样 p=a; 这里p表示指针数组第一个元素的值,a的首地址的值。
如要将二维数组赋给一指针数组:
int *p[3];
int a[3][4];
p++; //该语句表示p数组指向下一个数组元素。注:此数组每一个元素都是一个指针
for(i=0;i<3;i++)
p[i]=a[i];
这里int *p[3] 表示一个一维数组内存放着三个指针变量,分别是p[0]、p[1]、p[2] 所以要分别赋值。
3.函数指针和指针函数的区别
指针函数是指带指针的函数,即本质是一个函数。函数返回类型是某一类型的指针
类型标识符 *函数名(参数表) ;具体格式:int *f(x,y);
首先它是一个函数,只不过这个函数的返回值是一个地址值。指针函数一定有函数返回值,而且在主调函数中,函数返回值必须赋给同类型的指针变量。例如:
01.float *fun();
02.float *p;
03.p = fun(a);
函数指针是指向函数的指针变量,即本质是一个指针变量。
指向函数的指针包含了函数的地址,可以通过它来调用函数。声明格式如下: 类型说明符 (*函数名)(参数)
使用的时候:
01.int (*f)(int x); /*声明一个函数指针 */
02.f=func; /*将func函数的首地址赋给指针f */
4.管道实现父子进程之间通信
其基本原理是这样的:假如原先在父进程中文件描述符3和4通过管道1连接起来(3是读端,4是写端),则fork创建子进程后,子进程中的文件描述符3和4也通过管道1连接起来(3是读端,4是写端)。这样一来,在父进程通过文件描述符4向管道写入内容后,在子进程中就可以通过文件描述符3从管道中读出数据(当然在父进程中也可以通过文件描述符3从管道中读出数据)。
5.重定向I/O的是shell而不是程序
看listargs.c代码分析
代码托管
其他(感悟、思考等,可选)
本周的代码相当的多,不过收获还是很大的。在参考学姐博客的基础上,对代码进行了一步一步的实践,学姐对我的帮助很大,能够使我更深入的理解和分析了代码,了解了进程创建和控制的系统调用及函数使用,理解了数组指针、指针数组、函数指针、指针函数的区别,感觉这样的学习状态很充实,不仅仅是这一门课,这样的方法用到其他的课程上面帮助应该也是很大的。
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第一周 | 200/200 | 2/2 | 30/30 | |
第二周 | 300/500 | 1/3 | 20/50 | |
第三周 | 100/600 | 1/4 | 20/70 | |
第五周 | 300/900 | 1/5 | 30/100 | |
第六周 | 136/1036 | 1/6 | 20/120 | |
第七周 | 124/1160 | 1/7 | 20/140 | |
第八周 | 0/1160 | 3/10 | 20/160 | |
第九周 | 338/1498 | 3/13 | 25/185 | |
第十周 | 505/2003 | 2/15 | 25/210 | |
第十一周 | 1079/3082 | 1/16 | 30/240 |