标签:apue
该文档由个人总结,一级标题的序号对应《APUE》第一版的各章,但是二级标题和该书无关,其序号和内容完全是根据个人判断和个人需求进行编写。
本章所说明的函数经常被称之为不带缓存的I/O(与第5章中说明的标准I/O函数相对照)
大多数UNIX文件I/O只需用到5个函数:open、read、write、lseek、close。
需注意的是write后如需要read,则需要在read前添加lseek,因为write后文件的偏移量在write的最后一个位置(而该位置可能在文件尾)。
下图截自《APUE》,这节的总结都和该图有关。
上图说明了进程的三张表之间的关系,也说明了I/O的数据结构。
内核使用了三种数据结构,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
1, 每个进程在进程表中有一个记录项,每个记录项中有一张打开文件描述附表(见上图中的“进程表项”),在每张文件描述符表中每个描述符占用一项。与每个文件描述符相关联的是:
a, 文件描述符标志(上图进程表项中的fd 0, fd 1)。
b, 指向一个文件表项的指针(上图进程表项指向外面的指针)。
2, 内核为所有打开文件维持一张文件表。每个文件表项包含:
a, 文件状态标志(读、写、增写、同步、非阻塞等)
b, 当前文件位移量。
c, 指向该文件v节点表项的指针。
3, 每个打开的文件(或设备)都有一个v节点结构。
好,在此基础上参考下图来说明UNIX如何实现文件共享。
假设有2个进程打开同一个文件,那么这2个进程都得到一个文件表项,不过v节点表项只有一个。“这2个进程都得到一个文件表项”的原因是:每个进程都有它自己对该文件的当前位移量。
在早期的UNIX版本不支持open的O_APPEND选择项,所以,若需在文件的尾端写数据则需要写如下代码:
If( lseek( fd, 0L, 2) < 0) //position to EOF
Err_sys(“lseek error”);
If( write(fd, buff, 100) != 100) //write
Err_sys(“write error”);
对单个进程来说没问题,不过如果是下面这种情况:2个进程打开同一个文件,并都将文件的位移量移到了文件的尾端(假设是第1500字节处),由文件共享可知,这两个进程都有自己的文件表项(即都有它自己对该文件的当前位移量),这时第一个进程调用write,然后,内核切换到另一个进程调用write,但是2个进程都是从1500字节处写,所以第一个进程所写的内容就被破坏了。
解决方法就是让“位移到文件尾端,然后写数据”这两步变成一个原子操作,而这就需要用到open函数的O_APPEND选项,该选项的作用是:每次写之前都将进程的当前位移量设置到文件的尾端处。
我们知道,对open函数,如果同时制定了O_CREAT和O_EXCL时,如果文件已经存在,则open失败。这是一个原子操作。
如果没有这样一个原子操作的话,我们可能需要编写下列程序段:
If(( fd = open(pathname, O_WRONLY))< 0)
If( errno == ENOENT){
If(( fd =creat(pathname, mode)) < 0)
Err_sys(“creat error”);
}else
Err_sys(“open error”);
道理同上,单个进程没问题,但如果在打开和创建之间,另一个进程创建了该文件的话,那么该进程在执行creat时就会将另一个进程写进去的数据擦去(假设另一个进程在创建文件后又向该文件中写数据的话)。
复制一现存的文件描述符可以有下面两类方法:
第一类:让系统来指定新的文件描述符。
这类返回当前可用文件描述符的最小值。
调用:
Dup( filedes);
等同于:
Fcntl(filedes, F_DUPFD, 0);
第二类:手段指定新的文件描述符。
如果指定的文件描述符已经打开,则先将其关闭。
调用:
Dup2(filedes_old, filedes_new);
等同于:
Close(filedes_new);
Fcntl(filedes_old,F_DUPFD, filedes_new);
关于第一类2种方法都可以,但第二类最好只用dup2(),因为dup2()是一个原子操作,而后者不是。当然并不是说fcntl不能使用,毕竟fcntl可以做许多dup/dup2所不能做的事情。
文件系统的其他特征和文件的性质。UNIX文件系统的结构以及符号连接。
谈到标准I/O就的谈到流缓存。而这也是标准I/O库中的一个重点。
标准I/O库提供了三种类型的缓存:
a 全缓存;b 行缓存; c 不带缓存
详解如下:
对于全缓存:填满I/O缓存后才进行实际I/O操作。一般来说,在一个流上执行第一次I/O操作时,相关标准I/O函数用malloc获取需使用的缓存。(驻在磁盘上的文件通常是全缓存的)。
对于行缓存:在输入和输出中遇到新行符时,进行I/O操作。不过对于行缓存有2个限制:1,因为每一行的缓存长度是固定的,所以只要填满了缓存,那么即使没有遇到换行符,也会进行I/O操作;2,只要通过标准输入输出库要求从一个不带缓存的流或者一个行缓存的流得到数据,那么就会刷新所有的行缓存输出流。
对于不带缓存:就是直接调用文件I/O,即第三章的内容。
在ANSI C 中:
当且仅当标准输入和标准输出不涉及交互作用设备时,它们才是全缓存的。
标准出错绝不会是全缓存的。
需要注意的是:如果在一个函数中分配了一个自动变量类的标准I/O缓存,则从该函数返回之前必须关闭该流。一般而言,应由系统选择缓存的长度,并自动分配缓存。这样的话,标准I/O库在关闭此流时将自动释放此缓存。
最后提一点:在好些系统中,默认的是当标准输入、输出连至终端时,它们是行缓存的。当将流重新定向到普通文件时,它们就变成是全缓存的,其缓存长度是该文件系统优先选用的I/O长度(从stat结构中得到的st_blksize)。标准出错为非缓存,而普通文件按系统默认是全缓存的。(看程序5-3)
注意,以读和写类型打开一文件时(type中含+号),具有下列限制:
如果中间没有fflush、fseek、fsetpos和rewind,则在输出后面不能直接跟随输入。
如果中间没有fseek、fsetpos或rewind,或者一个输出操作没有到达文件尾端,则在输入操作之后不能直接跟随输出。
有getc、fgetc、getchar。getchar等用于getc(stdin)。前两者的区别是:getc可被实现为宏,而fgetc则不能。
在一个流读之后调用它,可将字符在送回流中。当然送回到流中的字符以后可从流中读出,但读出字符的顺序与送回的顺序相反。
注意:送回的字符不一定必须是上一次读到的字符。EOF不能回送。
gets和puts就忘掉他们吧。只用fgets和fputs即可,当然是用fgets和fputs时要在每行终止处自己加一个新行符。这点需要注意。
char* tmpnam(char*ptr);
若ptr是NULL,所产生的路径名存放在一个静态区中,指向该静态区的指针作为函数值返回。下一次在调用tmpnam时,会重写该静态区。这意味着:如果我们调用该函数多次,而且像保存路径名,则我们应当保存该路径名的副本,而不是指针的副本。
若ptr不是NULL,则认为它指向长度至少是L_tmpnam个字符的数组。(常数L_tmpnam定义在头文件<stdio.h>中。)所产生的路径名存放在该数组中,ptr也作为函数值返回。
FILE*tmpfile(void);
tmpfile函数经常使用的标准UNIX技术是先调用tmpnam产生一个唯一的路径名,然后立刻unlink它。
关于exit和_exit请看“附5”。
由于历史原因--!,C程序一直由下列几部分组成:
正文段、初始化数据段、非初始化数据等、栈、堆。
这是一种典型的安排方式,但并不要求一定以这种方式安排其存储空间。
下面对上面5部分进行解释。
1正文段
CPU执行的机器指令部分。一般该段是可共享的,但常常是只读的。
2初始化数据段
包含了程序中需赋初值的变量。如:函数外说明int i=0;就放在这里。
3非初始化数据段
即BBS段。在程序开始执行之前,内核将该段初始化为0.如:函数外说明:long sum[1000];此变量就放在这里。
4 栈
自动变量以及每次函数调用时所需保存的信息存放在此处。
5 堆
在堆中通常进行动态存储分配。一般来说,堆位于非初始化数据段顶和栈底之间。
ANSI C说明了三个用于存储器空间分配的函数
1. Malloc. 存储器中初始值不确定
Void* malloc(size_t size);
2. Calloc. 分配的空间中每一位都初始化为0
Void* calloc(unsigned n,unsigned size);
3. Realloc. 更改以前分配区的长度。当增加时,可能需将以前分配区的内容一道另一个足够大的区域,而新增区域内的初始值则不确定。
Void* realloc(void* ptr, size_t newsize);
Void free(void* ptr); 释放ptr指向的存储空间。
大多数实现所分配的存储空间比所要求的稍大一些,因为需要额外的空间来记录管理信息(如:分配块的长度、指向下一个分配块的指针等等)。这意味着如果写过一个已分配区的尾端,则会改写那些管理信息。这种类型的错误时灾难性的,但因为这种错误不会很快暴露出来,所以很难发现。而将指向分配块的指针向后移动亦可能会改写本块的管理信息。
其他可能产生的致命错误是:释放了一个已经释放的块;调用free时所用的指针不是3个alloc函数的返回值等。
4. Alloca
和malloc相同,只不过它在当前函数的栈上分配空间,而不是在堆中。优点是:当函数返回时,自动释放它所使用的栈。缺点是:某些系统在函数已被调用后不能增加栈长度,于是也就不支持alloca函数。
在C中,不允许使用goto。而执行这种跳转功能的函数是setjmp和longjmp。这两个函数对于处理发生在很深的嵌套函数调用中的出错情况非常有用。
Int setjmp(jmp_buf env);
Void longjmp(jmp_buf env, int val);
当检查到一个错误时,则用2个参数调用longjmp函数。第一个为setjmp时所用的env;第二个val为非0值(因为setjmp默认返回0)。使用第二个参数的原因是对于一个setjmp可以有多个longjmp。例如在方法1中longjmp的val为1,在方法2中longjmp的val为2,这样通过测试返回值就可判断是从方法1还是从方法2来的longjmp了。
这牵扯到一个问题:当longjmp返回后,之前的变量能否恢复到以前调用setjmp时的值(即回滚原先的值),或者这些变量保持为调用do_line时的值。答案是:看情况。大多数情况不会滚这些自动变量和寄存器变量的值,但也只是大多数情况下。所以如果有个一想不回滚的变量,可将其定义为volatile属性。即将其说明为全局和静态变量。
如上例,open_data打开了一个I/O流,然后为该流设置了缓存。
但是当其返回后,它在栈上所使用的空间将由下一个调用函数的栈使用。不过标准I/O库函数仍将使用原先为databuf在栈上分配的空间作为该流的缓存,这就造成了冲突和混乱。所以应在全局存储空间静态的(static或extern),或者动态的(使用alloc函数)为数组databuf分配空间。
创建新进程、执行程序、进程终止。
实际、有效合保存的用户和组ID,他们如何受到进程控制原语的影响。
解释器文件合system函数
ID为0的进程:常常被称为交换进程或者系统进程,该进程不执行任何磁盘上的程序—他是内核的一部分。
ID为1的进程:就是init啦,这个进程绝不会终止。而且他是所有孤儿进程的父进程。
ID为2的进程:页精灵进程。负责支持虚存系统的请页操作。与交换进程一样,该进程也是内核进程。
顺便一提,这三个都是精灵进程。
在程序8-1上面有这么一段关于程序8-1的话,额。。。。先看程序8-1
还记得I/O分为带缓存的I/O和不带缓存的I/O吗?
在这个例子中write只写到标准输出一次,这个好理解,因为write是不带缓存的。但是针对下面的printf(“before fork\n”);它会在终端输出一行before fork。但,如果将其定位到一个文件,那么在文件中就会出现2行before fork。
即:修改代码为:
FILE* fp;
Charfile[256] = “8-1”;
Fp = fopen(“file”,“w”);
Fprintf(fp, “beforefork\n”, NULL);
的话,那么在文件8-1中的结果是
before fork
before fork
为什么呢?
因为其为带缓存的I/O,而其缓存类型如果是连到终端设备,则其实行缓存的,反之是全缓存。所以,对于printf,它连到终端设备,而需要输出的内容是before fork\n,含有换行符,所以在fork之前就将内容从缓存输出到了终端中,因而只有一行输出。(我们知道fork是复制父进程的数据空间到子进程中,而这数据空间就包括缓存数据,这里缓存中的数据被输出到了终端,所以子进程得到的缓存中不再包含before fork\n)。这里我们做个试验,将before fork\n改成befork fork。就会发现终端输出了两遍before fork。(因为是行缓存,又因为这里没有了换行符,又又因为在fork之前一没有换行符二没有什么能让缓冲区满的语句,所以缓存中的数据在fork之前不会输出到终端,所以复制给子进程的缓存中就包含了before fork)。
对于输出到文件的,由上面那句红色的话可知,他是全缓存,缓存不满不输出,所以子进程也从父进程的缓存中复制到了该数据。从而文件中出现了2遍before fork。
如果父进程打开了一个文件,在关闭该文件前fork了一个子进程的话会发生什么?
没错,子进程通过复制得到父进程打开该文件的文件描述符,这也就相当于子进程打开了该文件。因此,就不得不说说文件共享了。首先看下图。
由图可知,父子进程均有自己对文件的文件描述符(所以子进程在结束前也要将自己的文件描述符close掉),不过父子进程的文件描述符均指向同一个“文件表”,这意味着父子进程共享该文件的位移量。所以,父子进程对该文件的写不会互相影响。
1,系统中有了太多的进程(这通常意味着某个方面出了问题)
2,该用户ID的进程总数超过了系统限制。(在APUE的表2-7中说明CHILD_MAX规定了每个用户ID在任一时刻可具有的最大进程数)
1,一个父进程希望复制自己,使父子进程同时执行不同的代码段。(如:在网络服务进程中,父进程等待委托者的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则等待下一个服务请求。)
2,一个进程要执行一个不同的程序。(这对shell是常见的情况。在这种情况下,子进程在从fork返回后立即调用exec----顺便一提,这种情况可以说是使用vfork的唯一情况。)
区别:
fork复制父进程的数据给子进程,这是子进程在另一片地址中;
vfrok则不复制,它的结果是子进程直接在父进程的地址中执行(这意味着子进程的操作会修改父进程内存中的数据);
vfork保证子进程先执行。
为什么说“vfork和exec”而不说“fork和exec”啊,这是因为使用vfork的情况一般来说也就是vfork后接exec,所以fork就不要和vfork抢了~O(∩_∩)O~
啊,开个玩笑开个玩笑。不过实际情况也就是这样,因为调用exec(或exit)后就会跳转到和exec语句中的内容相对应的地址空间中(这点请学习exec函数),所以用fork先复制一片内存给子进程然后再跳转到其他地址就显得多此一举而且浪费空间了,因此vfork就很不错~。
感觉了解就行,但为了以后可以在别人面前卖弄。。。。(我说我是开玩笑的你信吗?)还是总结下吧。
言归正传,进程终止的情况如下:
1、 正常终止
a) 在main函数内执行return语句。(等效于调用exit)
b) 调用exit函数。(终止处理程序,然后关闭所有标准I/O流等----也因此,如果vfork出的子进程中用了exit,那么父进程中还没有I/O的内容(如,子进程语句后面的printf)就不会运行了。)不过因为ANSI C不处理文件描述符、多进程(父子进程)以及作业控制,所以这一定义对UNIX系统而言是不完整的。
c) 调用_exit函数。此函数由exit调用,它处理UNIX的特定细节。
2、 异常终止
a) 调用abort。它产生SIGABRT信号,因此是下一种异常终止的特例。(你就理解为下一种是长方形,这种是正方形,而正方形是长方形的特例)
b) 当进程接收到某个信号时。(进程越出其地址空间访问存储单元,或者除以0,内核就会为该进程产生相应的信号。)
还记得上面的红字吗?虽然exit不处理文件描述符,但是在进程终止的最后都会执行内核中的一段代码。这段代码为相应的进程关闭所有打开的描述符,释放它所使用的存储器等。
当然,我们的希望是终止进程能够通知其父进程它是如何终止的,在此需要注意的是,对于exit和_exit(正常终止),是依靠传递它们的退出状态参数来实现的。但是在异常终止的情况,内核(注意不是进程本身)产生一个指示其异常终止原因的终止状态。(对于上面的情况,该终止进程的父进程均能通过调用wait和waitpid函数取得其终止状态。)
注意这里的“退出状态”和“终止状态”,在最后调用_exit时内核将其推出状态转换成终止状态。
总之,如果子进程正常终止,那么父进程才能获得子进程的退出状态。
Pid_t wait(intstat);
Pid_twaitpid(pid_t pid, int stat, int options);
使用的wait和waitpid是预防僵死进程的重要方法之一。
1、阻塞(如果其所有子进程都还在运行)
2、带子进程的终止状态立即返回(如果一个子进程已经终止,正等待父进程存取其终止状态)
3、出错立即返回(如果它没有任何子进程)
当进程正常/异常终止时,内核就向其父进程发送SIGCHLD信号,如果进程是因为接收到SIGCHLD信号而调用wait,则可期望wait会立即返回。但是在一个任一时刻调用wait,则进程可能会阻塞。
1、在一个子进程终止前,wait使其调用者阻塞,而waitpid有一个选择项,可使调用者不阻塞。
2、waitpid不等待第一个终止的子进程(它有若干个选择项,可以控制它所等待的进程)
3、对于wait,其唯一出错的调用时没有子进程。但是对于waitpid,如果指定的进程或进程组不存在,或者调用进程没有子进程都能出错。
对于wait,我们可以这么用:
Pid_t pid;
Int stat;
If((pid = fork()) <0) err_sys(“fork error”);
Else if(pid == 0)exit(7);
If(wait(&stat) !=pid) err_sys(“wait error”);
Printf(“%d\n”, stat);
(如果不关心进程是如何结束的,可将wait的参数设置为NULL)
但是这样有缺点:除了父进程可能会一直等待这点外,我们若想等待特定的进程也很麻烦。
这时我们就用到了waitpid。
Waitpid函数提供了wait函数没有提供的三个功能:
1、 waitpid可以等待一个特定的进程(wait返回任一终止子进程的状态)。
2、 waitpid提供了wait的非阻塞版本。(有时希望取得一个子进程的状态,但不想阻塞)
3、 waitpid支持作业控制。
对于waitpid的pid参数的解释与其值有关:
Pid==-1 等待任一子进程(这方面waitpid和wait等效)
Pid > 0 等待其进程ID与pid相等的子进程
Pid == 0 等待其组ID等于调用进程的组ID的任一子进程
Pid < -1 等待其组ID等译pid的绝对值的任一子进程
在linux中,并不存在exec()这样一个函数形式,实际上它是一组函数,一共有6个,如下:
#include <unistd.h>
int execl(const char *path, constchar *arg, ...);
int execlp(const char *file, constchar *arg, ...);
int execle(const char *path, constchar *arg, ..., char *const envp[]);
int execv(const char *path, char*const argv[]);
int execvp(const char *file, char*const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
这6个函数的记忆方式如下:
前面均以exec开头,l:取一个参数表,v:取一个argv[]。e:取envp[]数组,而不是使用当前环境变量。P取filename做为参数,并在PATH中寻找可执行文件。
注意:不管是取一个参数表还是取一个argv[],都要在末尾写一个NULL,告诉它参数结束。
请看下面的例子:
char *envp[]={"PATH=/tmp",
"USER=lei",
"STATUS=testing",
NULL};
char*argv_execv[]={"echo", "excuted by execv", NULL};
char*argv_execvp[]={"echo", "executed by execvp", NULL};
char*argv_execve[]={"env", NULL};
if(fork()==0)
if(execl("/bin/echo","echo", "executed by execl", NULL)<0)
perror("Erron execl");
if(fork()==0)
if(execlp("echo","echo", "executed by execlp", NULL)<0)
perror("Erron execlp");
if(fork()==0)
if(execle("/usr/bin/env","env", NULL, envp)<0)
perror("Erron execle");
if(fork()==0)
if(execv("/bin/echo",argv_execv)<0)
perror("Erron execv");
if(fork()==0)
if(execvp("echo",argv_execvp)<0)
perror("Erron execvp");
if(fork()==0)
if(execve("/usr/bin/env",argv_execve, envp)<0)
perror("Erron execve");
#include<stdlib.h>
Int system(constchar cmdstring);
如果cmdstrinf为一个空指针,则仅当命令处理程序可用时,system返回非0值,这一特征可以决定在一个给定的操作系统上是否支持system函数。
使用system而不使用fork和exec的优点是:system进行了所需的各种出错处理,以及各种信号处理。
对于ps –xj | cat1 |cat2看下图
可以看到,对于每个SHELL命令,shell都对fork一个sh来执行它,为了让SHELL知道何时结束,LINUX中让管道的最后一个命令为登陆SHELL的子进程,这样当最后一个命令结束后父进程(登陆SHELL)就会知道执行完毕了。
不过对于只有一个管道的命令,如:ps –xj | cat1,看下图
这2个的父进程都是登陆SHELL。
信号是软件中断。
信号提供一种处理异步事件的方法:终端用户键入中断键,则会通过信号机构停止一个程序。
虽然有些废话,不过还是不得不说一下信号的三种操作
1、 忽略此信号。大多数信号都可以使用该处理方式。但是有两种信号不能被忽略。它们是:SIGKILL和SIGSTOP。(之所以不能被忽略是因为:它们向超级用户提供一种是进程终止或停止的可靠方法。另外,如果忽略某些有硬件异常产生的信号(如非法存储访问或除以0),则进程的行为时未定义的)
2、 捕捉信号。实现此处理方式的方式是为该信号写信号处理函数。
3、 执行系统默认动作。信号的默认动作看下图。但是注意:大多数的信号的系统默认动作是终止该进程。
在上图中的“终止w/core”表示在进程的当前工作目录下的core文件中复制了该进程的存储图像,(大多数UNIX调试程序都是用core文件以检查进程在终止时的状态)。
不过在下列条件下不产生core文件:
1、 进程是“设置-用户-ID”,而且当前用户并非程序文件的所有者;
2、 进程是“设置-组-ID”,而且当前用户并非该程序文件的所有者;
3、 用户没有写当前工作目录的许可权;
4、 文件太大。
Core文件的许可权(假定该文件在此之前并不存在)通常是用户读/写,组读和其他读。
SIGABRT:调用abort函数时产生。进程异常终止。
SIGALRM:超过用alarm函数设置的时间时产生此信号。(若由settitimer(2)函数设置的间隔时间已经过时,也产生此信号)
SIGBUS:指示一个实现定义的硬件故障。
SIGEMT:指示一个实现定义的硬件故障。
SIGTRAP:指示一个实现定义的硬件故障。
SIGCHLD:在一个进程终止或停止时,SIGCHLD信号被送给其父进程。按系统默认:忽略此信号。如果父进程希望了解其子进程的这种状态改变,则应捕捉此信号。信号捕捉函数中通常要调用wait函数取得子进程的ID和其终止状态。
(系统V的早期版本有一个名为SIGCLD的类似信号。这一信号具有非标准的语义,SVR2的手册页警告在心的程序中尽量不要使用这种信号。而应当使用标准的SIGCHLD)
SIGCONT:需要继续运行的处于停止状态的进程受到该信号后继续运行,否则默认动作时忽略此信号。
SIGHUP:算术运算异常。如:除以0,浮点溢出等。
SIGHUP:如果终端界面检测到一个连接断开,则将此信号送给与该终端相关的控制进程(对话期首进程)。(如果对话期前进程终止,也产生此信号。在此情况下,此信号送给前台的每一个进程。)通常此信号通知精灵进程已再度它们的配置文件。选用SIGHUP的理由是:因为一个精灵进程不会有一个控制终端,而且通常绝不会接收到这种信号。
SIGILL:进程执行一条非法硬件指令。
SIGINFO:这是一种4.3+BSD信号。当用户按状态键(一般为CTRL+T)时,终端驱动程序产生此信号并送至前台进程组中的每一个进程。此信号通常造成在终端上显示进程组中个进程的状态信息。
SIGINT:按中断键(DELETE/CTRL+C)时,终端驱动程序产生此信号并送至前台进程组中的每一个进程。
SIGIO:指示一个异步I/O事件。
SIGKILL:这是两个不能被捕捉或忽略的信号中的一个。它向系统管理员提供了一种可以杀死任意进程的可靠方法。
SIGPIPE:在读进程已经终止时写管道,则产生此信号。(若进程写一个已经终止的套接口也产生此信号。)
SIGPOLL:在一个可轮回设备上发生一特定事件时产生此信号。(SVR4信号)
SIGPROF:当setitimer(2)函数设置的更改统计时间超过时产生。
SIGPWR:该信号解释起来有些长,具体如下:这是一种SVR4信号,它依赖于系统。它主要用于具有不间断电源(UPS)的系统上。如果电源失效,则UPS起作用,而且通常软件会收到通知。在这种情况下,系统依靠蓄电池电源继续运行,所以无需任何处理。但是如果蓄电池也将不支持工作免责软件通常会再次接收到通知,此时,它在15~30秒内使系统各部分都停止运行。此时应当传递SIGPWR信号。在大多数系统中使接到蓄电池电压过低的进程将信号SIGPWR发送给init进程,然后由init处理停机操作。
SIGQUIT:当用户在终端上按退出键(CTRL+\)时产生并送至前台进程组中的所有进程。(此进程不仅终止前台进程组(如SIGINT所做的那样),同时产生一个core文件)
SIGSEGV:指示进程进行了一次无效的存储访问。
SIGSTOP:一个作业控制信号,它停止一个进程。(它类似交互停止信号(SIGTSTP),但是SIGSTOP不能被捕捉或忽略)
SIGSYS:指示一个无效的系统调用。(由于某种未知原因,进程执行了一条系统调用指令,但其指示系统调用类型的参数却是无效的)
SIGTERM:由kill命令发送的系统默认终止信号。
SIGTSTP:交互停止信号,当用户在终端上按挂起键(CTRL+Z)时产生。
SIGTTIN:当一个后台进程组试图读其控制终端时,终端驱动程序产生此信号。(下列情况不产生此信号----此时读操作返回出错,errno设置为EIO:1、读进程忽略或阻塞此信号;2、读进程所属的进程组是孤儿进程)
SIGTTOU:当一个后台进程组试图写其控制终端时产生此信号。(与上述SIGTTIN信号不同,一个进程可以选择为允许后台进程写控制终端。如不允许后台进程写,则下面2中情况不会产生该信号----此时写操作返回出错,errno设置为EIO:1、写进程忽略或阻塞此信号;2、写进程所属进程组是孤儿进程组)
SIGURG:此信号通知进程已经发生一个紧急情况。(在网络连接上,接到非规定波特率的数据时,此信号可选择的产生)
SIGUSR1:一个用户定义的信号,可用于应用程序。
SIGUSR2:一个用户定义的信号,可用于应用程序。
SIGVTALRM:当一个由setitimer(2)函数设置的虚拟间隔时间已经超过时产生此信号。
SIGWINCH:如果一个进程用ioctl的“设置窗口大小”命令更改了窗口大小,则内核将SIGWINCH信号送至前台进程组。
SIGXCPU:进程超过了其软CPU时间限制时产生。
SIGXFSZ:进程超过了其软文件长度限制时SVR4和4.3+BSD产生此信号。
执行一个程序时,所有信号的状态时系统默认/忽略。通常所有信号都被设置为系统默认动作,除非调用exec的进程忽略该信号。需要注意的是,exec函数将原先设置为要捕捉的信号都更改为默认动作,其他信号的状态则不变(一个进程原先要捕捉的信号,在执行一个新程序后就自然地不能再捕捉了,因为信号捕捉函数的地址很可能在所执行的新程序文件中已无意义)。
SHELL自动将后台进程中对中断和退出信号的处理方式设置为忽略。(因为如果不这样设置的话当按下中断键时,它不但终止前台进程,也终止所有后台进程)
在早期的UNIX版本中,信号是不可靠的。
这里不可靠指的是:一个信号发生后可能会被丢失,但是进程却不知道这一点。
那时,进程对信号的控制能力很低,它能捕捉信号或者忽略它,但是有些很需要的功能它却不具备。如:有时用户希望通知内核阻塞某一信号,不过该阻塞有如下要求----不要忽略该信号,在其发生时记住它,直到进程满足一定条件后在通知它。这种能力当时就不具备。
追其原因是因为:在早期版本中,信号一旦发生,内核就随机将信号动作复位为默认值(虽然可以通过捕捉每种信号各一次而避免这点)。下面是早期版本处理中断信号的经典实例代码:
Int sig_int();
……
Signal(SIGINT,sig_int); //建立处理程序
……
Sig_int(){
Signal(SIGINT,sig_int); //为下次信号的发生重建处理程序
…… //信号处理代码
}
但是有一个问题:在信号发生后到信号调用signal函数之间有一段时间,若在这段这段时间中发生另一次该信号,那么这次对该信号的处理会执行默认动作(对于中断信号就会终止该进程)。对于这种类型的程序段在大多数时间都能正常工作,但是结果却可能不是我们想要的。
还有一个问题:在进程不希望某种信号发生时,它不能关闭该信号,只能做到忽略该信号。如:下面的关于“阻止下列信号发生,如果它们确实产生了,请记住它们”的经典实力代码:
Int sig_int_flag;
Main(){
Intsig_int();
……
Signal(SIGINT,sig_int);
……
While(sig_int_flag== 0)
Pause();
……
}
Sig_int(){
Signal(SIGINT,sig_int);
Sig_int_flag= 1;
}
其中,进程调用pause函数使自己睡眠,直到捕捉到一个信号后内核将进程唤醒。嗯。。。。这是正常情况。为什么这么说?因为在“While(sig_int_flag == 0)”和“Pause();”之间发生信号的话(而且该信号不会再次发生),那么此进程就会一直睡眠下去了。于是这次发生的信号就丢失了。
在上面的基础上,我们来看看SIGCLD信号,为什么要单独把它拿出来呢?这是因为由于历史原因,系统V出了SIGCLD信号的方式和其他的不同。下面详细解释下:
SIGCLD的语义为:子进程状态改变后产生此信号。
该信号的默认动作是SIG_DFL(忽略)。这有一个缺点:该动作的作用是不理会该信号,但是也不舍弃子进程的状态,因此如果不用wait和waitpid对其子进程进行状态信息回收的话,就会产生僵尸进程。
如果将其动作指定为SIG_IGN,那么在忽略SIGCLD信号的基础上子进程的状态也会被丢弃(也就是自动回收),因此不会产生僵尸进程。不过问题是:wait和waitpid无法捕捉到子进程的状态信息了(这时如果你随后调用了wait,那么会阻塞到所有的子进程结束,然后wait返回-1,errno设置为ECHILD,即无进程等待)。
既然如此,那么如果我们自定义其处理函数的话又会是怎么样呢?请看下面的处理函数的经典实例代码:
Sig_xxx(){
Signal(SIGXXX,sig_xxx); //重建处理函数
……
}
SIGCLD会立即检查是否有子进程准备好被等待,而这就是SIGCLD的最大漏洞。还记得SIGCLD的默认动作吗?对,是忽略但是不释放子进程的状态。因此,如果在重建信号处理函数前没有事先wait处理掉信号信息的话,就会出现如下情况:每次设置SIGCLD处理方式时,都回去检查是否有信号到来,如果此时信号的确到来了,就会调用自定义的信号处理函数,然后调用重建处理函数的代码,在重建的时候仍会检查信号是否到来,此时信号未被处理,会再次出发自定义的信号处理函数,一直循环(不过在RH7.2上上述问题不存在,因为现今的UNIX系统均提供可靠的信号机制,而且现今的许多UNIX系统对SIGCLD的定义是:#define SIGCLD SIGCHLD)。
不过现在有一个新的信号:SIGCHLD。该信号就解决的上面的问题----该信号的语义为:子进程状态改变后产生此信号,父进程需要调用一个wait函数以确定发生了什么。
非阻塞I/O、记录锁、系统V流机制、I/O多路转接(select和poll函数)、readv和writev函数,存储映照I/O(mmap)
在《APUE》的10.5节中曾将系统调用分为两类:低速系统调用和其他。
但是需要注意:虽然读、写磁盘会使调用在短暂时间内阻塞,但并不能将他们视为“低速”。
对于一个给定描述符有两种方法对其指定非阻塞I/O:
1, 如果是调用open以获得该描述符,则可指定O_NONBLOCK标识。
2, 对已已经打开的一个描述符,则可调用fcntl打开O_NONBLOCK文件状态标识。
其功能是:一个进程正在读或者修改文件的某个部分时,可以组织其他进程修改同一个文件区。
关于记录锁的自动继承和释放有三条规则:
1) 锁与进程、文件两方面有关。这有两重含义:第一重为当一个进程终止时,它所建立的锁全部释放;第二重为任何时候关闭一个描述符,则该进程通过这一描述符可以存放的文件上的任何一把锁都将释放(这些锁都是该进程设置的)。
这就意味着如果执行下列四步:
fd1 = open(pathname,….);
lock_reg(fd,F_SETLK, F_RDLCK, offset, whence, len);
fd2 = dup(fd1); //或者fd2 =open(pathname, ….);
close(fd2);
则在close(fd2)后,在fd1上设置的锁被释放。
int lock_reg(int fd,int cmd, int type, off_t offset, int whence, off_t len){
structflock lock;
lock.l_type= type;
lock.l_start= offset;
lock.l_whence= whence;
lock.l_len= len;
return(fcntl(fd, cmd, &lock));
}
2) 由fork产生的子程序不继承父进程所设置的锁。
3) 在执行exec后,新程序可以继承原程序的锁。
如果我们只从一个描述符读,那么一个read/fread完事。但是如果从多个描述符读呢?试想一下下面的情况:1、正在读的那个描述符正在被写,而另一个描述符早准备好了,难道我们需要在这个描述符上一直等下去? 2、如果某个描述符对应的文件一直没有内容,难道我们要“不停地read然后发现是空返回,之后等待若干秒后在read”(此为轮询)这样浪费资源?当然不能,那么我们就想了,能不能是描述符有内容了并且准备好了后通知我们让我们去读呢?当然能,而目前来说,一种比较好的技术就是I/O多路转接。
先构造一张有关描述符的表,然后调用一个函数,它要到这些描述符中的一个已准备好进行I/O时才返回。在返回时,它告诉进程哪一个描述符已准备好可以进行I/O。
有两个函数:select和poll。(当然某些内核可能提供了更高级的实现,比如pselect等等)
select的参数告诉内核:
1、 我们所关心的描述符。
2、 对于每个描述符我们所关心的条件(如:是否读一个给定的描述符?是否想写一个给定的描述符?是否关心一个描述符的异常条件?)
3、 希望等待多长时间(永远等待/等待一固定时间/不等待)
Select返回时,内核告诉我们:
1、 已准备好的描述符的数量
2、 哪一个描述符已准备好读、写或异常条件。
int select(intmaxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *tvptr);
对于timaval:
tvptr == NULL; 永远等待
tvptr->tv_sec==0 &&tvptr->tv_usec==0; 完全不等待
tvptr->tv_sec=x &&tvptr->tv_usec!=y; 等待x秒y微秒
对于readfds:说明了文件是否可读;
对于writefds:说明了文件是否可写;
对于errorfds:说明了文件是否可被河蟹啦!额。。。。是文件是否处于异常条件;
Fd_set reset;
Int fd;
FD_ZERO(&rset); //清除所有位
FD_SET(fd,&reset); //设置关心的位
If(FD_ISSET(fd,&rset)){….} //从select返回时,用FD_ISSET测试该集中的某个给定位是否仍旧设置。
顺便一提,如果在一个描述符上碰到了文件结束,那么select认为该描述符是可读的。然后调用read的话,它返回0。(很多人错误的认为,当到达文件结尾处时,select会指示一个异常条件。)
与select不同,poll不是为每个条件构造一个描述符集,而是构造一个pollfd结构数组,每个数组元素制定一个描述符编号以及对其所关心的条件。
int poll ( structpollfd * fds, unsigned int nfds, int timeout);
structpollfd {
intfd; /* 文件描述符*/
short events; /* 等待的事件*/
short revents; /* 实际发生了的事件*/
} ;
关于pollfd的events和revents标志看下图:
值得一提的是最后三个:即使在events字段中没有指定这三个值,如果相应条件发生,则在revents中也返回它们。
关于timeout:
Timeout == 0 : 不等待;
Timeout == INFTIM : 永远等待;
Timeout > 0 : 等待timeout毫秒。
如果需要操作的描述符(如文件描述符)少的话select和poll在性能上几乎没有差异,而且因为select实现起来相对简单而成为了首选,不过若描述符很多的话那poll就快于select了,因为select需要遍历所有的描述符而poll就跳过了这一步。
若线程数少的话多线程(每个线程会申请8M的空间----当然不同的内核会有不同,因此如果线程过多那么内存是不够的)。
需要操作的文件太大(上百M)的话也多线程。
反正就I/O多路转接了。
下表为不同实现所支持的不同形式的IPC。
上表中的前7种IPC通常限于同一台主机的各个进程间的IPC。最后两种:套接口和流支持不同主机上各个进程间的IPC。
管道有两种限制:
1、 它们是半双工的。数据只能在一个方向上流动。
2、 它们只能在具有公共祖先的进程之间使用。通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
不过流管道没有第一种限制(即它是全双工的),FIFO和命名流管道则没有第二种限制。
创建管道的函数:
Int pipe(int filedes[2]); //filedes[0]:为读而打开, filedes[1]:为写而打开。
可以使用fstat以及S_ISFIFO宏来测试管道。
单个进程中的管道几乎没有任何用处,通常,调用pipe的进程接着调用fork,这样就创建了从父进程到子进程或反之的IPC通道。如图所示:
图中的红蓝两条线是我添加的,毕竟是父子进程互相联系,而原图(没有这两条线)给人感觉是父进程内部和子进程内部通信(那样的话还要IPC干吗?)。
当管道的一端被关闭后,下列规则起作用:
1、 当读一个写端已被关闭的管道时,在所有数据都被读取后,read返回0,以指示达到了文件结束处。(若管道的写端还有进程时,就不会产生文件结束。)
2、 如写一个读端被关闭的管道,则产生信号SIGPIPE。如果忽略该信号或者捕捉该信号并从其处理程序返回,则write出错返回,errno设置为EPIPE。
在limits.h中定义了常数PIPE_BUF(一般值为4096),该值规定了内核中管道缓存器的大小。如果对管道进行write调用,而且要求写的字数小于等于PIPE_BUF,则此操作不会与其他进程对同一管道(或FIFO)的write操作穿插进行。但,若有多个进程同时写一个管道(或FIFO),而且某个或某些进程要求写的字节数超过PIPE_BUF,则数据可能会与其他写操作的数据相穿插。
因为如下情况很常见:创建一个连接到另一个进程的管道,然后读其输出或向其发送输入。所以有了这两个函数。
这两个函数实现的操作是:创建一个管道,fork一个子进程,关闭管道的不适用端,exec一个shell以执行命令,等待命令终止。
函数如下:
FILE* popen(const char* cmdstring,const char type); //type : ”r”, “w”
Int pclose(FILE* fp);
管道:只能有一个进程创建,并写入值。
命名管道:可以支持不相关的进程之间使用这个管道。
协同进程:允许一个进程将其stdin及stdout这两个标准IO绑定到对应的进程。可以向同一个进程写入数据,并从里面读取数据。
创建命名管道的函数:
#include <sys/types.h>
#include <sys/stst.h>
Int mkfifo(const char pathname, mode_tmode);
成功返回0,出错返回-1。
创建了一个FIFO后,可以用open打开它。(一般的文件I/O函数—close,read, write, unlink等均适用于FIFO)。
注意:当打开一个FIFO时,非阻塞标志(O_NONBLOCK)产生下列影响:
1、 在一般情况中(没有说明O_NONBLOCK),一进程为读打开了某FIFO,但是没有进程为写而打开它,那该FIFO一直阻塞到某个其他进程为写打开此FIFO。类似,为写而打开一个FIFO要阻塞到某个其他进程为读而打开它。
2、 如果指定了O_NONBLOCK,则如果没有进程已经为写而打开该FIFO,那么用只读打开该FIFO的进程会立即返回。但是,如果没有进程已经为读而打开此FIFO,那么只写打开将出错返回,其errno是ENXIO。(因此如果两个进程均用O_NONBLOCK打开此FIFO的话,那就会出问题了—因为不可能同时为读为写打开FIFO,所以全会立即返回,所以根据需要某个进程用O_NONBLOCK打开,另外一个不用该方式打开)
类似于管道,若写一个尚无进程为读而打开FIFO,则产生信号SIGPIPE。若某个FIFO的最有一个写进程关闭了该FIFO,则将为该FIFO的读进程产生一个文件结束标志。
一个给定的FIFO有多个写进程是常见的。这就意味着如果不希望多个进程缩写的数据互相穿插,则需考虑原子写操作。
正如对于管道一样,常数PIPE_BUF说明了可被原子写到FIFO的最大数据量。
1、FIFO由shell命令使用一遍将数据从一条管道线传送到另一条,为此无需创建中间临时文件。
2、FIFO用于客户机-服务器应用程序中,以在客户机和服务器之间传递数据。
三种系统V IPC:消息队列、信号量以及共享存储器它们各自的功能以及特征。
都用一个非负整数的标识符加以引用。(如:为了对一个消息队列发送/或取消息,只需知道其队列标识符。)
于文件描述符不同,IPC标识符不是小的整数。当一个IPC被创建,以后又被删除时,与这结构相关的标志连续加1,直至达到一个整数的最大正直,然后又转回到0。(即使在IPC结构被删除后也记住该值,每次使用此结构时则增1,该值被称为“槽使用顺序号”)
当创建IPC结构时,一定要制定一个关键字(key),其数据类型是key_t。内核会将关键字变换成标识符。
1、 服务器使用关键字IPC_PRIVATE创建IPC结构,然后将返回的标识符放在某处(如一个文件)以便客户机取用。这种技术有个缺点:服务器要将整形标识符写到文件中,然后客户机在此后又要读文件取得此标识符。(关键字IPC_PRIVATE保证服务器创建一个新IPC结构,该关键字也可用于父、子关系进程。父进程制定IPC_PRIVATE创建一个新IPC结构,所返回的标识符在fork后可有子进程使用。子进程可将此标识符作为exec函数的一个参数传给一个新程序。)
2、 在一个公用头文件中定义一个客户机和服务器都认可的关键字。然后服务器制定此关键字创建一个新的IPC结构。缺点是:如果该关键字已经和一个IPC结构相结合,那么执行get函数(msgget、semget、shmget)时会出错返回。所以服务器需注意处理这一错误:删除已经存在的IPC结构,然后试着再创建它。
3、 客户机和服务器认同一个路径名和课题ID(0~255之间的字符值),然后调用ftok将这两个值变换为一个关键字,然后再方法2中使用次关键字。缺点是:ftok虽然可以生成一个特殊的关键字,但是该关键字生成什么样我们不知道,所以一般避免使用ftok,改为在头文件中存放一个大家都知道的关键字。
这三个函数式msgget、semget和shmget。
这三个get函数都有两个类似的参数key和一个整形的flag。若满足下列条件,则创建一个新的IPC结构(通常由服务器创建):
1、 key是IPC_PRIVATE。
2、 key未和特定类型的IPC结构相结合,flag中制定了IPC_CREAT位。(为访问现存的队列—通常由客户机进行,key必须等于创建该队列时所指定的关键字,并且不应制定IPC_CREAT)
如果目的是访问一个现存队列,那么决不能指定IPC_PRIVATE作为关键字。因为该关键字总是用于创建一个新队列。(为了访问一个用于IPC_PRIVATE作为关键字创建的现存队列,一定要知道与该队列相结合的标识符,然后再其他IPC调用中(如msgsnd、msgrcv)中使用该标识符)
如果希望创建一个新IPC结构,保证不是引用具有同一标识符的一个现行IPC结构,那么必须在flag中同时指定IPC_CREAT和IPC_EXCL位。这样如果IPC结构已经存在就会造成出错,返回EEXIST(这与指定了O_CREAT和O_EXCL标志的open相类似)。
struct ipc_perm{
uid_t uid; //拥有者有效的用户id
gid_t gid; //拥有者有效的组id
uid_t cuid; //创造者有效的用户id
gid_t cgid; //创造者有效的组id
mode_t mode; //使用模式
ulong seq; //槽使用序列号
key_t key; //key
}
在创建IPC结构时,除seq以外的所有字段都赋初值。以后可以调用msgctl、semctl或shmctl修改uid、gid和mode字段。
缺点:
1,IPC结构在系统范围内起作用,没有访问技术。
如:
对于消息队列:如果创建了一个消息队列,在该队列中放入了几则消息,然后终止,但是该消息队列及其内容并不被删除。它们余留在系统直至:由某个进程调用magrcv和msgctl读消息或删除消息队列,或某个进程执行ipcrm命令删除消息队列;或由正在启动的系统删除消息队列。
对于管道pipe,当最后一个访问管道终止时,管道就被完全删除了。
对于FIFO而言虽然当最后一个引用FIFO的进程终止时其名字仍保留在系统中,直至显示的删除它,但是留在FIFO中的数据却在此时全部删除。)
2,IPC结构并不按名字为文件系统所知。
我们不能用open、write、close这类函数来存取他们或修改它们的特征。为了支持它们不能不增加多个全新的调用(msgget、semop、shmat等)。
我们不能用ls看到它们,不能用rm删除它们,不能用chmod命令更改它们的存取全。于是,也不得不增加了全新的命令ipcs和ipcrm。
因为这些IPC不适用文件描述符,所以不能对它们使用多路转接I/O函数:select和poll。这就使得一次使用多个IPC结构,以及用文件或设备I/O来使用IPC结构很难做到。(例如:没有某种形式的忙-等待循环,就不能使一个服务器等待一个消息放在两个消息队列的任一一个中)
优点:
1,可靠;2,受控制的;3,面向记录;4,可以用非先进先出方式处理。(流也具有这些优点)
消息队列是消息的连接表,存放在内核中并由消息队列标识符标识。
下面介绍下和消息队列有关的函数:
msgget:用于创建一个新队列/打开一个现存的队列。
msgsnd:用于将新消息添加到队列尾端。(每个消息包含一个正长整形类型字段,一个非负长度以及实际数据字节(对应于长度),所有这些都在将消息添加到队列时,传送给msgsnd)。
msgcrv:用于从队列中取消息(我们并不一定要以先进先出次序取消息,也可以按消息的类型字段取消息)。
每个队列都有一个msqid_ds结构与其相关。此结构如下:
下面详细说明:
1、新建/打开一个消息队列:
int msgget(key_tkey, int flag); //成功返回消息队列的ID,失败为-1
当创建一个新队列时,初始化msqid_ds结构的下列成员:
msg_qnum、msg_lspid、msg_lrpid、msg_stime和msg_rtime均设置为0;
msg_ctime设置为当前时间。
msg_qbytes设置为系统限制值。
2、对队列执行多种操作:
int msgctl(intmsqid, int cmd, struct msqid_ds* buf); //成功返回0,出错为-1;
cmd参数如下(下面三个参数也可用于信号量和共享存储):
IPC_STAT:取此队列的msqid_ds结构,并将其存放在buf指向的结构中。
IPC_SET:按由buf指向的结构中的值,设置于此队列相关的结构中的下列四个字段:msg_perm.uid、msg_perm.gid、msg_perm.mode和msg_perm.qbytes。(此命令只能有下列两种进程执行:1、其有效用户ID等于msg_perm.cuid或msg_perm.uid;2、具有超级用户特权的进程。只有超级用户才能增加msg_qbytes的值)
IPC_RMID:从系统中删除该消息队列以及仍在该队列上的所有数据。(该删除立即生效。仍在使用这一消息队列的其他进程在他们下一次试图对此队列进行操作时,将出错返回EIDRM。此命令只能由下列两种进程执行:1、其有效用户ID=msg_perm.cuid或msg_perm.uid;2、具有超级用户特权和进程)
3、将数据放到消息队列上:
int magsnd(intmsqid, const void* ptr, size_t nbytes,int flag); //成功返回0,反之-1
4、从队列中取消息
int msgrcv(intmsqid, void* ptr, size_t nbytes, long type, int flag);
《APUE》的第三章为“不带缓存的I/O”,第五章为“带缓存的I/O”。
首先,我们需要明确一点,上面两个是“术语”,不是“述语”(描述性质的语言)。
其实“不带缓存的I/O”实际上也是带缓存的,只不过此缓存非比缓存,这里的“不带缓存”指的是“不带流缓存”,而这也就是和“带缓存的I/O”的区别了。
下面让我详细解释下:
《APUE》上对“不带缓存的I/O”的定义是:每个read和write都调用内核中的一个系统调用。什么意思?
是这样的:当我们调用write函数时,直接调用系统调用,将数据写入到块缓存进行排队,当块缓存达到一定量时,才会把数据写入磁盘。
而带缓存的I/O对其进行了改进,它提供了一个流缓存,当用fwrite函数时,先把数据写入到流缓存中,当达到一定条件,如:流缓存区满了、刷新流缓存时,才会把数据一次性送往内核提供的块缓存中,再经块缓存写入磁盘。
这样说如果还有些不清楚的话请看下面:
不带缓存的I/O的操作(以写为例)
1 将数据写入内核提供的块缓存
2 经块缓存写入磁盘
带缓存的I/O的操作(以写为例)
1 将数据写入流缓存直至达到条件
2 将数据一次性写入内核提供的块缓存
3 经块缓存写入磁盘
怎么样?这样就清楚些了吧。“带缓存的I/O”比“不带缓存的I/O”多了一步,而另外两步一样。
其实,标准库中的“带缓存的I/O”就是调用系统提供的“不带缓存的I/O”实现的。
最后,总结一下:“不带缓存的I/O”是相对于“带缓存的I/O”等流函数来说明的,因为后者的会先将数据在流缓存中进行操作,前者则无此步骤而直接和内核提供的块缓存进行交互,所以称前者是“不带缓存”的,其实对于内核来说,它还是进行了缓存的。
先总结下关于流的一些翻译:
1,流是与磁盘或其他外围设备关联的数据的源或目的地。
2,流是(表达)读写数据的一种可移植的方法,它为一般的I/O操作提供了灵活有效的手段。一个流是一个由指针操作的文件或者是一个物理设备,而这个指针正是指向了这个流。
3, 不管是交互与诸如终端盒磁带驱动器之类的物理设备,还是存取与由结构化存储设备支撑的文件,输入和输出(信息)都被映射为逻辑数据流,而流的属性却远不是诸多输入输出属性的统一。
4, ANSI C进一步对I/O的概念进行了抽象。就C程序而言,所有的I/O操作知识简单地从程序移进或移出字节的事情。因此毫不惊奇的是,这种字节流便被称为流。程序只需要关心创建正确的输出字节数据,以及正确的解释从输入数据的字节数据。特定I/O设备的细节对程序员是隐藏的。
定义大致如上,下面总结一下。
1, 流是一个抽象的概念,并不是一个物理设备的概念,如果用某个看得见摸得着的物理设备做参考来理解流的话那就大错特错了。
2, 流是对I/O系统中的一种I/O机制和功能的抽象。就像运输工具是对一切运动载体的抽象一样。
3, 流是一种“动”的概念,静止存储在介质上的信息只有当他按一定的序列准备“运动”时才成为流。(静止的信息具有流的潜力,但不一定是流,就像没有汽油的汽车一样,它具有成为运输工具的潜力,但还不是运输工具)。流有源头也有目的地(并且他将源头和目的地相关联),并且一定带有某种信息(好像说了句废话)。
何为“原子操作”呢?
其实说白了,就是一个由多步操作组成,这些步骤要不执行就一个都不执行,如果执行的话,那么从第一步开始到最后一步结束绝对不会被信号等线程调度机制打断。
《APUE》上说的“原子的执行”也就是这个意思了。
其重要性在哪呢?
我们知道,CPU在用极快的速度不停地切换运行程序,这样的好处是可以“同时”运行好多程序,但坏处就是可能会造成一些让我们头痛不已的问题。
举个例子:
我们想完成如下的操作:
1. 打开一个文件(假设该文件已创建而且里面有我们需要的内容);
2. 给该文件+读锁;
3. 读取文件的内容;
4. 解锁;
5. 关闭文件。
这个操作看起来挺安全的。但是如果出现这种情况呢:
在上述的步骤1和2之间(即“打开文件”和“加读锁”之间)突然有一个进程打开了这个文件(这是CPU切换到了这个进程,而原来的进程则被暂时搁置了,也就是原来的进程被打断了),并往里面写入了一些内容后退出了。那么我们读到的内容可能就不是我们希望的。
之所以会出现上面的问题,就是因为上面5步不是“原子操作”,如果是“原子操作”的话,那么从第1步开始到最后一步结束为止,不会出现被打断的情况了。
由此,“原子操作”的重要性不言而喻。
关于“原子操作”的误区:
下面这句代码是不是原子操作呢?
temp += 1;
是?不是?是不是?
其实不是。
因为这句代码在翻译成汇编的话如下:
mov ax,[temp] //将temp的值传到寄存器ax中(也就是将其值传到一个内存地址中)
inc ax //对寄存器ax中的值+1
mov [temp],ax //将寄存器ax中的值传回temp
可见,只有一行的代码不见得就是“原子操作”。
传统的UNIX实现在内核中没有缓冲存储器,大多数磁盘I/O都通过缓存进行。当将数据写到文件上时,通常该数据先由内核复制到缓存中,如果该该缓存尚未写满,则并不将其排入输出队列,而是等待其写满或者当内核需要重用该缓存以便存放其他磁盘块数据时,再将该缓存排入输出队列,然后待其达到对首时,才进行实际的I/O操作。这种输出方式就是延迟写。
延迟写减少了磁盘读写次数,不过降低了文件内容的更新速度,是的欲写到文件中的数据在一段时间内并没有写到磁盘上。因此当系统发生故障时,这种延迟可能造成文件更新内容的丢失。而对了防止这种丢失,保证磁盘上实际文件系统与缓存中内容的一致性,UNIX系统提供了sync和fsync两个系统调用函数。
_exit()函数:直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;
exit()函数则在这些基础上作了一些包装,在执行退出之前加了若干道工序。
exit()函数与_exit()函数最大的区别就在于exit()函数在调用 exit 系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件。在Linux的标准函数库中,有一套称作“高级I/O”的函数,我们熟知 printf()、fopen()、fread()、fwrite()都在此列,它们也被称作“缓冲I/O(bufferedI/O)”,其特征是对应每一个打开的文件,在内存中都有一片缓冲区,每次读文件时,会多读出若干条记录,这样下次读文件时就可以直接从内存的缓冲区中读取,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(达到一定数量,或遇到特定字符,如换行符\n和文件结束 EOF),再将缓冲区中的内容一次性写入文件,这样就大大增加了文件读写的速度,但也为我们编程带来了一点点麻烦。如果有一些数据,我们认为已经写入了文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时我们用_exit()函数直接将进程关闭,缓冲区中的数据就会丢失,反之,如果想保证数据的完整性,就一定要使用exit()函数。
在一个进程调用了exit之后,该进程并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构。
孤儿进程:一个进程结束时,内核对所有的活动进程逐个检查,如果某个进程是该进程的子进程,则将其父进程的ID更改为1.这时这个进程就成为了孤儿进程。
僵死进程:一个已经终止,但是其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程成为僵死进程。(僵死进程几乎放弃了所有的内存空间,没有任何可执行代码,不能被调用,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,它需要其父进程为其收尸。)
僵死进程的产生:我们知道,在每个进程退出的时候,内核会释放进程的资源(如:打开的文件,占用的内存等),但是仍会为其保留一些信息(进程ID,退出状态,运行时间等),知道父进程通过wait/waitpid获取后才释放。在此之前,该进程就一直处于僵死状态,该进程也就是僵死进程了。
僵死进程的避免:
1、 父进程通过wait和waitpid等函数等待子进程结束(但这会导致父进程挂起)
2、 如果父进程没时间等待子进程结束,那么可以用signal函数为SIGCHLD安装信号处理函数。这样子进程结束后,父进程会收到该信号,可以在信号处理函数中调用wait回收。
3、 如果父进程对于子进程什么时候结束根本不关心,那么可以用signal(SIGCHLD, SIG_IGN)通知内核。这时,子进程结束后内核会对其进行回收。或者用sigaction函数为SIGCHLD设置SA_NOCLDWAIT,这样子进程结束后,就不会进入僵死状态。
struct sigaction sa;
sa.sa_handler = SIG_IGN;
sa.sa_flags = SA_NOCLDWAIT;
sigemptyset(&sa.sa_mask);
sigaction(SIGCHLD, &sa, NULL);
4、 fork两次:父进程fork一个子进程,然后继续工作,子进程fork一个孙进程后退出,那么孙进程被init接管(这时孙进程就是孤儿进程),孙进程结束后,init会回收。不过注意子进程的回收还要父进程来做。
注:一个进程如果被init领养,那么它就不会再变成僵死进程,因为init被编写为只要有一个子进程终止,init就会调用一个wait函数取得其终止状态。这样就防止了系统中有很多僵死进程。
首先我们先看看两者的定义。
可重入函数:可以由多于一个任务并发使用,而不必担心数据错误。
不可重入函数:不能由超过一个任务所共享,除非能确保函数的互斥(或者使用信号量,或者在代码的关键部分禁用中断)。对于不可重入函数,其实现不保证函数在多线程环境下死正确的。
看完定义来让我们看一个例子:
char *get_buffer(){ //定义一个缓冲区
static char buf[100];
return buf;
}
void *thread1(void *params) { //向上面定义的缓冲区中写数据
char *buf =get_buffer();
strcpy(buf, "string1");
}
void *thread2(void *params){ //同楼上
char *buf = get_buffer();
strcpy(buf,"string2");
}
上面的函数就是不可重入的。因为当2个以上的线程都使用get_buffer的返回值去访问buf缓冲区的时候,先向buf写入的数据就可能被后写入的数据覆盖。Thread1不能保证buf的内容是“string1”,而thread2不能保证buf的内容是“string2”。
如果理解了什么是可重入和不可重入函数的话,那么都有哪些函数时可重入的呢?请看下图。
没有在上表中的大多数函数是不可再入的,其原因为:它们使用静态数据结构,或它们调用malloc或free,或它们是标准I/O函数(标准I/O库的很多实现都以不可再入方式使用全局数据结构)。
我们需要知道:每个进程只有一个errno变量。而这就伴随着一个问题,信号处理程序中即使调用上述列表的值,但最后的errno却不一定是我们想要的。考虑下么的情况:有一个信号处理程序,它恰好在main刚设置errno之后调用。如果该信号处理程序调用read,则它可能更改errno的值从而取代了刚由main设置的值(就拿SIGCHLD信号来说,因为其信号处理程序要调用一种wait函数,而各种wait函数都能改变errno)。
因此,作为一个通用规则,当在信号处理程序中调用上表中列出的函数时,应当在其前保存errno,在其后恢复errno。
那么在信号处理程序中调用一个不可重入函数会出现什么情况呢?
当产生信号时,内核通常在进程表中设置某种形式的一个标志。当对信号做了这种动作时,我们说向一个进程递送了一个信号。(即信号被处理)
而信号未决就是在信号产生到递送之间的这段时间间隔。
关于信号阻塞:这里要注意一点,这里的阻塞不是阻塞其产生,而是说在信号产生后阻塞其发生作用(如果一个信号被阻塞了,那么在其被阻塞的这段时间也是信号未决)。
这里顺便在说一点:如果在进程解除对某个信号的阻塞之前,这种信号发生了多次,那么会如何?POSIX.1允许系统递送该信号一次或多次。如果递送该信号多次,则称这些信号排了队。但是大多数的UNIX并不对信号排队。代之以,UNIX内核只递送这种信号一次。
在《APUE》的10.5节(中断的系统调用)中将系统调用分为两类:低速系统调用和其他。
关于“低速系统调用”我没有查找到其定义,在《APUE》中对其的解释是:低速系统调用是可能会使进程永远阻塞的一类系统调用。
下面是其情况:
1, 如果数据并不存在,则读文件可能会使调用者永远阻塞(例如堵管道,终端设备和网络设备)。
2, 如果数据不能立即被接受,则写这些同样的文件也会使调用者永远阻塞。
3, 在某些条件发生之前,打开文件会被阻塞(例如打开一个终端设备可能需等到与之连接的调制解调器应答;又例如若只以写方式打开FIFO,那么在没有其他进程以用读方式打开FIFO时也要等待)。
4, 对已经加上强制性记录锁的文件进行读、写。
5, 某些ioctl操作。
6, 某些进程间通信函数。
有一点需要注意:非阻塞I/O使我们可以调用不会永远阻塞的I/O操作,例如open、read和write。如果这种操作不能完成,则立刻出错返回,表示该操作如果继续执行将继续阻塞下去。
这两组概念均涉及到IO处理。
首先我们先说同步、异步:
这两个概念均与消息的通知机制有关。
首先我们说说我们的大学生活,大学中最让人印象深刻的事情之一应该是吃饭,为什么这么说呢?应为在吃饭时我们会为自己去吃还是让别人带而苦恼。如果选择自己去吃,那么我们就得去食堂排队买饭,如果让别人带饭的话,那么我们就不用去排队,直到别人把饭带过来。
这时吃饭就相当于程序中消息触发后我们要做的动作,前者(排队买饭,买到饭了在开始吃----即排队等候)就是同步,后者(该干什么干什么,等别人带饭过来了在开始吃----即等待通知)就是异步。
然后我们再说阻塞、非阻塞:
这两个概念均与程序等待消息时的状态有关(无所谓同步或异步)。
不管我们是排队买饭还是等别人带饭,如果在这个过程中除了等待外不能做其他事情,那么就是阻塞。反之,如果在等待的时候,我们做些其他事情,那么就是非阻塞。
这两个都和12.2记录锁有关系。
在说这个之前要说一个概念,合作进程。
所谓合作进程是指:如果该库中的所有函数都以一致的方法处理记录锁,则称使用这些函数存取数据库的任何进程集为合作进程。
什么叫一致的方法处理记录锁?举个例子:我有几个进程(不一定有亲缘关系)都通过fcntl机制来操作文件,这就叫一致的方法。如果有一个进程,不使用fcntl机制而是直接open,write文件,那这个进程和之前的进程就不是一致的方法。
好了,回归正题。
建议性锁的规定:每个使用上锁文件的进程都要检查是否有锁存在,当然还得尊重已有的锁。和系统总体上都坚持不使用建议性锁,它们依靠程序员遵守这个规定(Linux默认是采用建议性锁)
强制性锁:由内核执行。当文件被上锁来进行写入操作时,在锁定文件的进程释放该锁之前,内核会阻止任何对该文件的读或写访问,每次读或写访问都得检查锁是否存在。
lock()用于对文件施加建议性锁
fcntl()用于对文件施加建议性锁和强制性锁都行。同时还可以对文件某一记录进行上锁,即记录锁。
UNIX过滤程序从标准输入读取数据,对其进行适当处理后写到标准输出。这几个过滤进程通常在shell管道中线性的连接。
而协同进程就是:如果一程序产生某个过滤程序的输入,同时又读取该过滤程序的输出时,那该过滤程序就成为协同进程。
标签:apue
原文地址:http://blog.csdn.net/xueyingxue001/article/details/39023915