简介
本文总结了个人,一个数字,对应称号《APUE》第一版的每一章,但是,独立的二级标题和书,人需求进行编写。
3.文件I/O
本章所说明的函数常常被称之为不带缓存的I/O(与第5章中说明的标准I/O函数相对比)
3.1文件I/O函数
大多数UNIX文件I/O仅仅需用到5个函数:open、read、write、lseek、close。
需注意的是write后如须要read,则须要在read前加入lseek。由于write后文件的偏移量在write的最后一个位置(而该位置可能在文件尾)。
3.2 不同缓存长度对read和write函数的影响
3.3 怎样在进程间共享文件
下图截自《APUE》,这节的总结都和该图有关。
上图说明了进程的三张表之间的关系。也说明了I/O的数据结构。
内核使用了三种数据结构,它们之间的关系决定了在文件共享方面一个进程对还有一个进程可能产生的影响。
1。 每一个进程在进程表中有一个记录项,每一个记录项中有一张打开文件描写叙述附表(见上图中的“进程表项”),在每张文件描写叙述符表中每一个描写叙述符占用一项。与每一个文件描写叙述符相关联的是:
a, 文件描写叙述符标志(上图进程表项中的fd 0, fd 1)。
b, 指向一个文件表项的指针(上图进程表项指向外面的指针)。
2, 内核为全部打开文件维持一张文件表。
每一个文件表项包括:
a, 文件状态标志(读、写、增写、同步、非堵塞等)
b, 当前文件位移量。
c, 指向该文件v节点表项的指针。
3, 每一个打开的文件(或设备)都有一个v节点结构。
好。在此基础上參考下图来说明UNIX怎样实现文件共享。
如果有2个进程打开同一个文件,那么这2个进程都得到一个文件表项,只是v节点表项仅仅有一个。
“这2个进程都得到一个文件表项”的原因是:每一个进程都有它自己对该文件的当前位移量。
3.4 文件I/O的原子操作
3.4.1 向一个文件尾端处写
在早期的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选项。该选项的作用是:每次写之前都将进程的当前位移量设置到文件的尾端处。
3.4.2 创建一个文件
我们知道,对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时就会将还有一个进程写进去的数据擦去(如果还有一个进程在创建文件后又向该文件里写数据的话)。
3.4.3 复制一现存的文件描写叙述符
复制一现存的文件描写叙述符能够有以下两类方法:
第一类:让系统来指定新的文件描写叙述符。
这类返回当前可用文件描写叙述符的最小值。
调用:
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所不能做的事情。
4.文件和文件夹
文件系统的其它特征和文件的性质。UNIX文件系统的结构以及符号连接。
5.标准I/O
5.1.缓存
谈到标准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)
5.2. 流的读写
注意,以读和写类型打开一文件时(type中含+号),具有下列限制:
假设中间没有fflush、fseek、fsetpos和rewind。则在输出后面不能直接尾随输入。
假设中间没有fseek、fsetpos或rewind,或者一个输出操作没有到达文件尾端,则在输入操作之后不能直接尾随输出。
5.3. 输入函数
有getc、fgetc、getchar。
getchar等用于getc(stdin)。前两者的差别是:getc可被实现为宏。而fgetc则不能。
5.4. 关于int ungetc(int c, FILE* fp);
在一个流读之后调用它。可将字符在送回流中。
当然送回到流中的字符以后可从流中读出,但读出字符的顺序与送回的顺序相反。
注意:送回的字符不一定必须是上一次读到的字符。
EOF不能回送。
5.5. 关于gets,puts,fgets和fputs
gets和puts就忘掉他们吧。仅仅用fgets和fputs就可以,当然是用fgets和fputs时要在每行终止处自己加一个新行符。
这点须要注意。
5.6. 关于暂时文件
char* tmpnam(char*ptr);
若ptr是NULL,所产生的路径名存放在一个静态区中,指向该静态区的指针作为函数值返回。下一次在调用tmpnam时,会重写该静态区。这意味着:假设我们调用该函数多次,并且像保存路径名,则我们应当保存该路径名的副本。而不是指针的副本。
若ptr不是NULL。则觉得它指向长度至少是L_tmpnam个字符的数组。(常数L_tmpnam定义在头文件<stdio.h>中。)所产生的路径名存放在该数组中,ptr也作为函数值返回。
FILE*tmpfile(void);
tmpfile函数常常使用的标准UNIX技术是先调用tmpnam产生一个唯一的路径名。然后立马unlink它。
7.UNIX进程的环境
关于exit和_exit请看“附5”。
7.1 C程序的存储空间布局
因为历史原因--!,C程序一直由下列几部分组成:
正文段、初始化数据段、非初始化数据等、栈、堆。
这是一种典型的安排方式。但并不要求一定以这样的方式安排其存储空间。
以下对上面5部分进行解释。
1正文段
CPU运行的机器指令部分。一般该段是可共享的。但经常是仅仅读的。
2初始化数据段
包括了程序中需赋初值的变量。
如:函数外说明int i=0;就放在这里。
3非初始化数据段
即BBS段。
在程序開始运行之前,内核将该段初始化为0.如:函数外说明:long sum[1000];此变量就放在这里。
4 栈
自己主动变量以及每次函数调用时所需保存的信息存放在此处。
5 堆
在堆中通常进行动态存储分配。一般来说,堆位于非初始化数据段顶和栈底之间。
7.2存储器分配
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函数。
7.3setjmp和longjmp
在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了。
7.4 volatile和自己主动、寄存器、亦失变量
这牵扯到一个问题:当longjmp返回后,之前的变量是否能恢复到曾经调用setjmp时的值(即回滚原先的值)。或者这些变量保持为调用do_line时的值。答案是:看情况。大多数情况不会滚这些自己主动变量和寄存器变量的值。但也仅仅是大多数情况下。所以假设有个一想不回滚的变量,可将其定义为volatile属性。即将其说明为全局和静态变量。
7.5 自己主动变量的潜在问题
如上例,open_data打开了一个I/O流,然后为该流设置了缓存。
可是当其返回后。它在栈上所使用的空间将由下一个调用函数的栈使用。只是标准I/O库函数仍将使用原先为databuf在栈上分配的空间作为该流的缓存,这就造成了冲突和混乱。所以应在全局存储空间静态的(static或extern)。或者动态的(使用alloc函数)为数组databuf分配空间。
8.进程控制
创建新进程、运行程序、进程终止。
实际、有效合保存的用户和组ID,他们怎样受到进程控制原语的影响。
解释器文件合system函数
8.1几个精灵进程
ID为0的进程:经常被称为交换进程或者系统进程,该进程不运行不论什么磁盘上的程序—他是内核的一部分。
ID为1的进程:就是init啦,这个进程绝不会终止。
并且他是全部孤儿进程的父进程。
ID为2的进程:页精灵进程。负责支持虚存系统的请页操作。
与交换进程一样,该进程也是内核进程。
顺便一提,这三个都是精灵进程。
8.2fork
8.2.1关于fork和I/O的关系
在程序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。
8.2.2 关于fork和文件共享的关系
假设父进程打开了一个文件,在关闭该文件前fork了一个子进程的话会发生什么?
没错。子进程通过复制得到父进程打开该文件的文件描写叙述符,这也就相当于子进程打开了该文件。因此,就不得不说说文件共享了。首先看下图。
由图可知。父子进程均有自己对文件的文件描写叙述符(所以子进程在结束前也要将自己的文件描写叙述符close掉),只是父子进程的文件描写叙述符均指向同一个“文件表”,这意味着父子进程共享该文件的位移量。所以,父子进程对该文件的写不会互相影响。
8.2.3 fork失败的主要原因
1,系统中有了太多的进程(这通常意味着某个方面出了问题)
2,该用户ID的进程总数超过了系统限制。(在APUE的表2-7中说明CHILD_MAX规定了每一个用户ID在任一时刻可具有的最大进程数)
8.2.4 fork的两种使用方法
1,一个父进程希望复制自己,使父子进程同一时候运行不同的代码段。(如:在网络服务进程中,父进程等待托付者的服务请求。当这样的请求到达时,父进程调用fork,使子进程处理此请求。父进程则等待下一个服务请求。
)
2,一个进程要运行一个不同的程序。
(这对shell是常见的情况。在这样的情况下。子进程在从fork返回后马上调用exec----顺便一提。这样的情况能够说是使用vfork的唯一情况。
)
8.2.5 fork和vfork
差别:
fork复制父进程的数据给子进程,这是子进程在还有一片地址中;
vfrok则不复制,它的结果是子进程直接在父进程的地址中运行(这意味着子进程的操作会改动父进程内存中的数据);
vfork保证子进程先运行。
8.2.6 vfork和exec
为什么说“vfork和exec”而不说“fork和exec”啊。这是由于使用vfork的情况一般来说也就是vfork后接exec。所以fork就不要和vfork抢了~O(∩_∩)O~
啊,开个玩笑开个玩笑。只是实际情况也就是这样,由于调用exec(或exit)后就会跳转到和exec语句中的内容相相应的地址空间中(这点请学习exec函数),所以用fork先复制一片内存给子进程然后再跳转到其它地址就显得多此一举并且浪费空间了。因此vfork就非常不错~。
8.3 进程终止的情况
感觉了解即可。但为了以后能够在别人面前卖弄。
。。。(我说我是开玩笑的你信吗?)还是总结下吧。
言归正传,进程终止的情况例如以下:
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时内核将其推出状态转换成终止状态。
总之,假设子进程正常终止,那么父进程才干获得子进程的退出状态。
8.4 wait和waitpid
Pid_t wait(intstat);
Pid_twaitpid(pid_t pid, int stat, int options);
使用的wait和waitpid是预防僵死进程的重要方法之中的一个。
8.4.1 调用wait或waitpid的进程可能出现的3种情况:
1、堵塞(假设其全部子进程都还在执行)
2、带子进程的终止状态马上返回(假设一个子进程已经终止,正等待父进程存取其终止状态)
3、出错马上返回(假设它没有不论什么子进程)
当进程正常/异常终止时,内核就向其父进程发送SIGCHLD信号,假设进程是由于接收到SIGCHLD信号而调用wait,则可期望wait会马上返回。可是在一个任一时刻调用wait,则进程可能会堵塞。
8.4.2 wait和waitpid的差别
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的绝对值的任一子进程
8.5 exec
在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");
8.6 system
#include<stdlib.h>
Int system(constchar cmdstring);
假设cmdstrinf为一个空指针,则仅当命令处理程序可用时。system返回非0值,这一特征能够决定在一个给定的操作系统上是否支持system函数。
使用system而不使用fork和exec的长处是:system进行了所需的各种出错处理,以及各种信号处理。
9.进程关系
9.1shell运行程序
对于ps –xj | cat1 |cat2看下图
能够看到,对于每一个SHELL命令,shell都对fork一个sh来运行它,为了让SHELL知道何时结束,LINUX中让管道的最后一个命令为登陆SHELL的子进程,这样当最后一个命令结束后父进程(登陆SHELL)就会知道运行完成了。
只是对于仅仅有一个管道的命令。如:ps –xj | cat1,看下图
这2个的父进程都是登陆SHELL。
10. 信号
信号是软件中断。
信号提供一种处理异步事件的方法:终端用户键入中断键,则会通过信号机构停止一个程序。
10.1 信号的三种操作
尽管有些废话。只是还是不得不说一下信号的三种操作
1、 忽略此信号。大多数信号都能够使用该处理方式。可是有两种信号不能被忽略。它们是:SIGKILL和SIGSTOP。(之所以不能被忽略是由于:它们向超级用户提供一种是进程终止或停止的可靠方法。另外,假设忽略某些有硬件异常产生的信号(如非法存储訪问或除以0),则进程的行为时没有定义的)
2、 捕捉信号。实现此处理方式的方式是为该信号写信号处理函数。
3、 运行系统默认动作。信号的默认动作看下图。可是注意:大多数的信号的系统默认动作是终止该进程。
在上图中的“终止w/core”表示在进程的当前工作文件夹下的core文件里复制了该进程的存储图像,(大多数UNIX调试程序都是用core文件以检查进程在终止时的状态)。
只是在下列条件下不产生core文件:
1、 进程是“设置-用户-ID”。并且当前用户并不是程序文件的全部者。
2、 进程是“设置-组-ID”,并且当前用户并不是该程序文件的全部者。
3、 用户没有写当前工作文件夹的许可权;
4、 文件太大。
Core文件的许可权(假定该文件在此之前并不存在)一般是用户读/写。组读和其它读。
10.2各个信号的具体说明
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产生此信号。
10.3程序启动
运行一个程序时。全部信号的状态时系统默认/忽略。通常全部信号都被设置为系统默认动作,除非调用exec的进程忽略该信号。
须要注意的是,exec函数将原先设置为要捕捉的信号都更改为默认动作,其它信号的状态则不变(一个进程原先要捕捉的信号。在运行一个新程序后就自然地不能再捕捉了,由于信号捕捉函数的地址非常可能在所运行的新程序文件里已无意义)。
SHELL自己主动将后台进程中对中断和退出信号的处理方式设置为忽略。
(由于假设不这样设置的话当按下中断键时。它不但终止前台进程,也终止全部后台进程)
10.4 不可靠信号
在早期的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();”之间发生信号的话(并且该信号不会再次发生)。那么此进程就会一直睡眠下去了。于是这次发生的信号就丢失了。
10.5 SIGCLD
在上面的基础上,我们来看看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函数以确定发生了什么。
12. 高级I/O
非堵塞I/O、记录锁、系统V流机制、I/O多路转接(select和poll函数)、readv和writev函数,存储映照I/O(mmap)
12.1 非堵塞I/O
在《APUE》的10.5节中曾将系统调用分为两类:低速系统调用和其它。
可是须要注意:尽管读、写磁盘会使调用在短临时间内堵塞。但并不能将他们视为“低速”。
对于一个给定描写叙述符有两种方法对其指定非堵塞I/O:
1。 假设是调用open以获得该描写叙述符。则可指定O_NONBLOCK标识。
2, 对已已经打开的一个描写叙述符,则可调用fcntl打开O_NONBLOCK文件状态标识。
12.2 记录锁
其功能是:一个进程正在读或者改动文件的某个部分时,能够组织其它进程改动同一个文件区。
12.2.1 锁的隐含继承和释放
关于记录锁的自己主动继承和释放有三条规则:
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后。新程序能够继承原程序的锁。
12.3 I/O多路转接
12.3.1 为什么须要I/O多路转接?
假设我们仅仅从一个描写叙述符读,那么一个read/fread完事。
可是假设从多个描写叙述符读呢?试想一下以下的情况:1、正在读的那个描写叙述符正在被写,而还有一个描写叙述符早准备好了,难道我们须要在这个描写叙述符上一直等下去? 2、假设某个描写叙述符相应的文件一直没有内容,难道我们要“不停地read然后发现是空返回,之后等待若干秒后在read”(此为轮询)这样浪费资源?当然不能,那么我们就想了,能不能是描写叙述符有内容了而且准备好了后通知我们让我们去读呢?当然能,而眼下来说。一种比較好的技术就是I/O多路转接。
12.3.2 I/O多路转接的思想是什么?
先构造一张有关描写叙述符的表,然后调用一个函数,它要到这些描写叙述符中的一个已准备好进行I/O时才返回。在返回时。它告诉进程哪一个描写叙述符已准备好能够进行I/O。
12.3.3 怎样实现I/O多路转接?
有两个函数:select和poll。(当然某些内核可能提供了更高级的实现,比方pselect等等)
12.3.3.1 select函数
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会指示一个异常条件。)
12.3.3.2 poll函数
与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毫秒。
12.3.3.3 select和poll的对照
假设须要操作的描写叙述符(如文件描写叙述符)少的话select和poll在性能上差点儿没有差异,并且由于select实现起来相对简单而成为了首选,只是若描写叙述符非常多的话那poll就快于select了,由于select须要遍历全部的描写叙述符而poll就跳过了这一步。
12.3.4 多线程和I/O多路转接
若线程数少的话多线程(每一个线程会申请8M的空间----当然不同的内核会有不同。因此假设线程过多那么内存是不够的)。
须要操作的文件太大(上百M)的话也多线程。
反正就I/O多路转接了。
14. 进程间通信
下表为不同实现所支持的不同形式的IPC。
上表中的前7种IPC通常限于同一台主机的各个进程间的IPC。
最后两种:套接口和流支持不同主机上各个进程间的IPC。
14.1 管道
管道有两种限制:
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。则数据可能会与其它写操作的数据相穿插。
14.2 popen和pclose
由于例如以下情况非经常见:创建一个连接到还有一个进程的管道,然后读其输出或向其发送输入。所以有了这两个函数。
这两个函数实现的操作是:创建一个管道。fork一个子进程,关闭管道的不适用端。exec一个shell以运行命令。等待命令终止。
函数例如以下:
FILE* popen(const char* cmdstring,const char type); //type : ”r”, “w”
Int pclose(FILE* fp);
14.3 管道,命名管道,协同进程之间的差别(转)
管道:仅仅能有一个进程创建,并写入值。
命名管道:能够支持不相关的进程之间使用这个管道。
协同进程:同意一个进程将其stdin及stdout这两个标准IO绑定到相应的进程。能够向同一个进程写入数据。并从里面读取数据。
14.4 FIFO(命名管道)
创建命名管道的函数:
#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)。
14.4.1 打开FIFO和O_NONBLOCK
注意:当打开一个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的最大数据量。
14.4.2 FIFO的两种用途
1、FIFO由shell命令使用一遍将数据从一条管道线传送到还有一条,为此无需创建中间暂时文件。
2、FIFO用于客户机-server应用程序中,以在客户机和server之间传递数据。
14.5 消息队列、信号量以及共享存储器
三种系统V IPC:消息队列、信号量以及共享存储器它们各自的功能以及特征。
14.5.1 标识符和keyword
都用一个非负整数的标识符加以引用。(如:为了对一个消息队列发送/或取消息,仅仅需知道其队列标识符。
)
于文件描写叙述符不同,IPC标识符不是小的整数。当一个IPC被创建,以后又被删除时。与这结构相关的标志连续加1。直至达到一个整数的最大正直,然后又转回到0。(即使在IPC结构被删除后也记住该值,每次使用此结构时则增1,该值被称为“槽使用顺序号”)
当创建IPC结构时,一定要制定一个keyword(key),其数据类型是key_t。内核会将keyword变换成标识符。
使客户机和server在同一IPC结构上会合的方法:
1、 server使用keywordIPC_PRIVATE创建IPC结构,然后将返回的标识符放在某处(如一个文件)以便客户机取用。这样的技术有个缺点:server要将整形标识符写到文件里。然后客户机在此后又要读文件取得此标识符。(keywordIPC_PRIVATE保证server创建一个新IPC结构,该keyword也可用于父、子关系进程。父进程制定IPC_PRIVATE创建一个新IPC结构。所返回的标识符在fork后可有子进程使用。子进程可将此标识符作为exec函数的一个參数传给一个新程序。)
2、 在一个公用头文件里定义一个客户机和server都认可的keyword。
然后server制定此keyword创建一个新的IPC结构。缺点是:假设该keyword已经和一个IPC结构相结合。那么运行get函数(msgget、semget、shmget)时会出错返回。所以server需注意处理这一错误:删除已经存在的IPC结构。然后试着再创建它。
3、 客户机和server认同一个路径名和课题ID(0~255之间的字符值),然后调用ftok将这两个值变换为一个keyword,然后再方法2中使用次keyword。
缺点是:ftok尽管能够生成一个特殊的keyword,可是该keyword生成什么样我们不知道,所以一般避免使用ftok,改为在头文件里存放一个大家都知道的keyword。
三个能够创建IPC的get函数
这三个函数式msgget、semget和shmget。
这三个get函数都有两个类似的參数key和一个整形的flag。
若满足下列条件。则创建一个新的IPC结构(通常由server创建):
1、 key是IPC_PRIVATE。
2、 key未和特定类型的IPC结构相结合,flag中制定了IPC_CREAT位。
(为訪问现存的队列—通常由客户机进行,key必须等于创建该队列时所指定的keyword。而且不应制定IPC_CREAT)
注意:
假设目的是訪问一个现存队列。那么决不能指定IPC_PRIVATE作为keyword。由于该keyword总是用于创建一个新队列。
(为了訪问一个用于IPC_PRIVATE作为keyword创建的现存队列,一定要知道与该队列相结合的标识符,然后再其它IPC调用中(如msgsnd、msgrcv)中使用该标识符)
假设希望创建一个新IPC结构,保证不是引用具有同一标识符的一个现行IPC结构。那么必须在flag中同一时候指定IPC_CREAT和IPC_EXCL位。这样假设IPC结构已经存在就会造成出错。返回EEXIST(这与指定了O_CREAT和O_EXCL标志的open相类似)。
14.5.2 ipc_perm结构(许可权结构)
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字段。
表14-2 ????
14.5.3 长处和缺点
缺点:
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结构非常难做到。(比如:没有某种形式的忙-等待循环。就不能使一个server等待一个消息放在两个消息队列的任一一个中)
长处:
1。可靠;2,受控制的;3。面向记录;4,能够用非先进先出方式处理。(流也具有这些长处)
14.6 消息队列
消息队列是消息的连接表。存放在内核中并由消息队列标识符标识。
以下介绍下和消息队列有关的函数:
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);
附1:带缓存的I/O和不带缓存的I/O的差别
《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”等流函数来说明的,由于后者的会先将数据在流缓存中进行操作,前者则无此步骤而直接和内核提供的块缓存进行交互,所以称前者是“不带缓存”的。事实上对于内核来说,它还是进行了缓存的。
附2:流
先总结下关于流的一些翻译:
1,流是与磁盘或其它外围设备关联的数据的源或目的地。
2,流是(表达)读写数据的一种可移植的方法,它为一般的I/O操作提供了灵活有效的手段。一个流是一个由指针操作的文件或者是一个物理设备。而这个指针正是指向了这个流。
3, 无论是交互与诸如终端盒磁带驱动器之类的物理设备,还是存取与由结构化存储设备支撑的文件。输入和输出(信息)都被映射为逻辑数据流,而流的属性却远不是诸多输入输出属性的统一。
4, ANSI C进一步对I/O的概念进行了抽象。就C程序而言,全部的I/O操作知识简单地从程序移进或移出字节的事情。
因此毫不惊奇的是,这样的字节流便被称为流。程序仅仅须要关心创建正确的输出字节数据,以及正确的解释从输入数据的字节数据。特定I/O设备的细节对程序猿是隐藏的。
定义大致如上。以下总结一下。
1。 流是一个抽象的概念,并非一个物理设备的概念。假设用某个看得见摸得着的物理设备做參考来理解流的话那就大错特错了。
2, 流是对I/O系统中的一种I/O机制和功能的抽象。就像运输工具是对一切运动载体的抽象一样。
3, 流是一种“动”的概念,精巧存储在介质上的信息仅仅有当他按一定的序列准备“运动”时才成为流。(精巧的信息具有流的潜力,但不一定是流,就像没有汽油的汽车一样,它具有成为运输工具的潜力。但还不是运输工具)。流有源头也有目的地(而且他将源头和目的地相关联)。而且一定带有某种信息(好像说了句废话)。
附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
可见,仅仅有一行的代码不见得就是“原子操作”。
附4:延迟写
传统的UNIX实如今内核中没有缓冲存储器。大多数磁盘I/O都通过缓存进行。
当将数据写到文件上时,通常该数据先由内核拷贝到缓存中。假设该该缓存尚未写满,则并不将其排入输出队列,而是等待其写满或者当内核须要重用该缓存以便存放其它磁盘块数据时,再将该缓存排入输出队列。然后待其达到对首时。才进行实际的I/O操作。
这样的输出方式就是延迟写。
延迟写降低了磁盘读写次数,只是降低了文件内容的更新速度,是的欲写到文件里的数据在一段时间内并没有写到磁盘上。
因此当系统发生问题时,这样的延迟可能造成文件更新内容的丢失。而对了防止这样的丢失,保证磁盘上实际文件系统与缓存中内容的一致性,UNIX系统提供了sync和fsync两个系统调用函数。
附5:exit()和_exit()
_exit()函数:直接使进程停止执行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;
exit()函数则在这些基础上作了一些包装,在运行退出之前加了若干道工序。
exit()函数与_exit()函数最大的差别就在于exit()函数在调用 exit 系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件。在Linux的标准函数库中。有一套称作“高级I/O”的函数,我们熟知 printf()、fopen()、fread()、fwrite()都在此列,它们也被称作“缓冲I/O(bufferedI/O)”,其特征是相应每个打开的文件,在内存中都有一片缓冲区,每次读文件时,会多读出若干条记录,这样下次读文件时就能够直接从内存的缓冲区中读取。每次写文件的时候。也不过写入内存中的缓冲区,等满足了一定的条件(达到一定数量,或遇到特定字符,如换行符\n和文件结束 EOF),再将缓冲区中的内容一次性写入文件,这样就大大添加了文件读写的速度,但也为我们编程带来了一点点麻烦。
假设有一些数据。我们觉得已经写入了文件。实际上由于没有满足特定的条件,它们还仅仅是保存在缓冲区内,这时我们用_exit()函数直接将进程关闭,缓冲区中的数据就会丢失。反之,假设想保证数据的完整性,就一定要使用exit()函数。
在一个进程调用了exit之后,该进程并不是立即就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构。
附6:孤儿进程和僵死进程(原+摘)
孤儿进程:一个进程结束时,内核对全部的活动进程逐个检查,假设某个进程是该进程的子进程,则将其父进程的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函数取得其终止状态。这样就防止了系统中有非常多僵死进程。
附7:(不)可重入函数
首先我们先看看两者的定义。
可重入函数:能够由多于一个任务并发使用,而不必操心数据错误。
不可重入函数:不能由超过一个任务所共享。除非能确保函数的相互排斥(或者使用信号量,或者在代码的关键部分禁用中断)。对于不可重入函数。事实上现不保证函数在多线程环境下死正确的。
看完定义来让我们看一个样例:
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”。
附7.1 有哪些(不)可重入函数
假设理解了什么是可重入和不可重入函数的话。那么都有哪些函数时可重入的呢?请看下图。
没有在上表中的大多数函数是不可再入的,其原由于:它们使用静态数据结构。或它们调用malloc或free。或它们是标准I/O函数(标准I/O库的非常多实现都以不可再入方式使用全局数据结构)。
附7.2 关于不可重入函数须要注意的地方
我们须要知道:每一个进程仅仅有一个errno变量。而这就伴随着一个问题,信号处理程序中即使调用上述列表的值,但最后的errno却不一定是我们想要的。考虑下么的情况:有一个信号处理程序。它恰好在main刚设置errno之后调用。假设该信号处理程序调用read,则它可能更改errno的值从而代替了刚由main设置的值(就拿SIGCHLD信号来说,由于其信号处理程序要调用一种wait函数,而各种wait函数都能改变errno)。
因此。作为一个通用规则,当在信号处理程序中调用上表中列出的函数时,应当在其前保存errno。在其后恢复errno。
那么在信号处理程序中调用一个不可重入函数会出现什么情况呢?
附8:信号未决和信号堵塞
当产生信号时,内核通常在进程表中设置某种形式的一个标志。当对信号做了这样的动作时,我们说向一个进程递送了一个信号。
(即信号被处理)
而信号未决就是在信号产生到递送之间的这段时间间隔。
关于信号堵塞:这里要注意一点,这里的堵塞不是堵塞其产生,而是说在信号产生后堵塞其发生作用(假设一个信号被堵塞了。那么在其被堵塞的这段时间也是信号未决)。
这里顺便在说一点:假设在进程解除对某个信号的堵塞之前。这样的信号发生了多次。那么会怎样?POSIX.1同意系统递送该信号一次或多次。假设递送该信号多次,则称这些信号排了队。可是大多数的UNIX并不正确信号排队。代之以。UNIX内核仅仅递送这样的信号一次。
附9:低俗系统调用和其它
在《APUE》的10.5节(中断的系统调用)中将系统调用分为两类:低速系统调用和其它。
关于“低速系统调用”我没有查找到其定义,在《APUE》中对其的解释是:低速系统调用是可能会使进程永远堵塞的一类系统调用。
以下是其情况:
1, 假设数据并不存在。则读文件可能会使调用者永远堵塞(比如堵管道。终端设备和网络设备)。
2, 假设数据不能马上被接受,则写这些相同的文件也会使调用者永远堵塞。
3, 在某些条件发生之前,打开文件会被堵塞(比如打开一个终端设备可能需等到与之连接的调制解调器应答;又比如若仅仅以写方式打开FIFO,那么在没有其它进程以用读方式打开FIFO时也要等待)。
4, 对已经加上强制性记录锁的文件进行读、写。
5。 某些ioctl操作。
6, 某些进程间通信函数。
有一点须要注意:非堵塞I/O使我们能够调用不会永远堵塞的I/O操作。比如open、read和write。假设这样的操作不能完毕,则立马出错返回。表示该操作假设继续运行将继续堵塞下去。
附10:同步、异步; 堵塞、非堵塞
这两组概念均涉及到IO处理。
首先我们先说同步、异步:
这两个概念均与消息的通知机制有关。
首先我们说说我们的大学生活,大学中最让人印象深刻的事情之中的一个应该是吃饭,为什么这么说呢?应为在吃饭时我们会为自己去吃还是让别人带而苦恼。假设选择自己去吃,那么我们就得去食堂排队买饭。假设让别人带饭的话,那么我们就不用去排队。直到别人把饭带过来。
这时吃饭就相当于程序中消息触发后我们要做的动作,前者(排队买饭。买到饭了在開始吃----即排队等候)就是同步,后者(该干什么干什么。等别人带饭过来了在開始吃----即等待通知)就是异步。
然后我们再说堵塞、非堵塞:
这两个概念均与程序等待消息时的状态有关(无所谓同步或异步)。
无论我们是排队买饭还是等别人带饭。假设在这个过程中除了等待外不能做其它事情,那么就是堵塞。反之,假设在等待的时候,我们做些其它事情,那么就是非堵塞。
附11:强制性锁和建议性锁
这两个都和12.2记录锁有关系。
在说这个之前要说一个概念,合作进程。
所谓合作进程是指:假设该库中的全部函数都以一致的方法处理记录锁。则称使用这些函数存取数据库的不论什么进程集为合作进程。
什么叫一致的方法处理记录锁?举个样例:我有几个进程(不一定有亲缘关系)都通过fcntl机制来操作文件。这就叫一致的方法。假设有一个进程,不使用fcntl机制而是直接open。write文件,那这个进程和之前的进程就不是一致的方法。
好了,回归正题。
建议性锁的规定:每一个使用上锁文件的进程都要检查是否有锁存在,当然还得尊重已有的锁。和系统整体上都坚持不使用建议性锁,它们依靠程序猿遵守这个规定(Linux默认是採用建议性锁)
强制性锁:由内核运行。
当文件被上锁来进行写入操作时,在锁定文件的进程释放该锁之前,内核会阻止不论什么对该文件的读或写訪问。每次读或写訪问都得检查锁是否存在。
lock()用于对文件施加建议性锁
fcntl()用于对文件施加建议性锁和强制性锁都行。同一时候还能够对文件某一记录进行上锁。即记录锁。
附12:协同进程(属于IPC的知识)
UNIX过滤程序从标准输入读取数据,对其进行适当处理后写到标准输出。这几个过滤进程通常在shell管道中线性的连接。
而协同进程就是:假设一程序产生某个过滤程序的输入。同一时候又读取该过滤程序的输出时。那该过滤程序就成为协同进程。
(FIFO、SOCKET的对照)
版权声明:本文博主原创文章,博客,未经同意不得转载。