第1章 unix基础知识
1. char *strerror(int errnum)
该函数将errnum(就是errno值)映射为一个出错信息字符串,返回该字符串指针。声明在string.h文件中。
2.void perror(const char *s)
该函数基于当前的errno值,在标准出错文件中输出一条出错消息,然后返回。声明在stdio.h文件中。它首先输出由s指向的字符串,然后是一个冒号,一个空格,接着是errno值对应的出错信息,最后是一个换行符。
第2章 UNIX标准化及实现
1.long sysconf(int name)
long fpathconf(int fd, int name)
long pathconf(char *path, int name)
第一个函数用来获取系统运行时的配置信息。后两个函数用来获取文件的name选项的配置信息,它们的区别在于一个提供文件描述符,一个提供文件路径。声明在unistd.h文件中。这些name参数的常量值参考《apue》。
2.系统基本数据类型。这些类型在不同系统上被声明为不同的c基本类型。因此使用它们可以增强代码的可移植性。
caddr_t 核心地址 clock_t 时钟滴答计数器(进程时间) comp_t 压缩的时钟时间
dev_t 设备号(主和次) fd_set 文件描述符集 fpos_t 文件位置
gid_t 用户组ID ino_t i节点编号 mode_t 文件类型,文件创建模式
nlink_t 目录项的链接技术 off_t 文件大小和偏移量 pid_t 进程ID和进程组ID
ptrdiff_t 两个指针相减的结果 rlim_t 资源限制 sig_atomic_t 能原子访问的数据类型
sigset_t 信号集 size_t 对象大小(例如字符串),无符号 ssize_t 返回的字节计数,带符号的(如read,write返回值,需要错误值)
time_t 日历时间的秒计数器 uid_t 用户ID wchar_t 能表示所有不同的字符码
第3章 文件IO
1.int open(const char *pathname, int flags)
int open(const char *pathname, int flags, node_t mode)
该函数用来打开文件,声明在fcntl.h中。flags标志有如下:O_RDONLY,O_WRONLY,O_RDWR(这三个标志必须要包含其中之一),还有一些文件创建标志和状态标志,可以包含0个或多个,O_CLOEXEC,O_CREAT,O_DIRECTORY,O_EXEL,O_NOCITY,O_NOFOLLOW,O_TRUNC,O_TTY_INIT,O_NONBLOCK,O_SYNC。(这些都是打开的时候才设置,属于某个打开的文件,inode中并不存在)
另外,当使用到O_CREAT标志来创建文件时,mode中需要包含下列符号:S_IRWXU,S_IRUSR,S_IWUSR,S_IXUSR,S_IRWXG,S_IRGRP,S_IWGRP,S_IXGRP,
S_IRWXO,S_IROTH,S_IWOTH,S_IXOTH。
2.int creat(const char *pathname, mode_t mode)
该函数用来创建一个文件,声明在fcntl.h中。mode中的值同上。当open中的flags包含O_CREAT时,open和creat功能是一样的。
3.int close(int fd)
该函数用来关闭有open/creat打开的文件描述符,声明在unistd.h中。
4.off_t lseek(int fd, off_t offset, int whence)
该函数用来对已打开文件fd进行定位,声明在unistd.h中。whence中可以包含如下值:SEEK_SET,SEEK_CUR,SEEK_END,分别表示从文件开头,文件当前位置,文件结尾作为偏移量的开始。
5.ssize_t read(int fd, void *buf, size_t count)
该函数用来从文件中读数据,声明在unistd.h中。
6.ssize_t write(int fd, const void *buf, size_t count)
该函数用来向文件中写数据,声明在unistd.h中。
7.ssize_t pread(int fd, void *buf, size_t count, off_t offset)
该函数类似于read函数,区别在于该函数是原子执行,并且不更新文件指针位置,offset用来设置在文件中读取的位置。声明在unistd.h中。
8.ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset)
该函数也类似于pread和write。声明在unistd.h中。
注意:7,8两个函数主要用在多线程对同一文件进行读写的场合。
9.int dup(int oldfd)
int dup2(int oldfd, int newfd)
这两个函数用来复制文件描述符,声明在unistd.h中。dup使用最小的空闲描述符作为新描述符,dup2的newfd提供了新描述符。
10.void sync(void)
int fsync(int fd)
int fdatasync(int fd)
这三个函数用来将文件缓冲区中的数据写入磁盘文件中。声明在unistd.h中。区别在于,sync将块缓冲区放入写队列就返回,并不等待实际写入磁盘中;fsync将fd文件缓冲块
立即写入磁盘中,并等待写入后才返回,同时更新文件属性;fdatasync类似于syncfs,不过只影响文件的部分数据。
11. int fcntl(int fd, int cmd, ... /* arg */ )
该函数用来更改已打开文件fd的属性,声明在fcntl.h中。该函数实现了5中功能,体现在不同的cmd所包含的标志中:
a.复制一个现有描述符(cmd=F_DUPFD)
b.获得/设置文件描述符标志(cmd=F_GETFD/F_SETFD)
c.获得/设置文件状态标志(cmd=F_GETFL/F_SETFL)
d.获得/设置异步I/O所有权(cmd=F_GETOWN/cmd=F_SETOWN)
e.获得/设置记录锁(cmd=G_GETLK/G_SETLK/F_SETLKW)
当cmd为F_DUPFD,fcntl函数相当于dup/dup2函数,返回复制后的新描述符,该描述符是空闲描述符中大于或等于第三个参数的值;当cmd为F_GETFD,用来获取文件描述符的所有标志(当前只有一个标志FD_CLOEXEC),当cmd为F_SETFD,用来设置文件描述父的标志(由第三个参数提供);当cmd为F_GETFL,用来获取文件的状态标志,文件状态标志如下所示。当cmd为F_SETFL,用来设置文件的状态标志。当cmd为F_GETOWN,用来取当前接收SIGIO和SIGURG信号的进程ID和进程组ID;当cmd为F_SETOWN,设置接收SIGIO和SIGURG信号的进程ID和进程组ID。
文件状态标志:
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读写打开
O_APPEND 追加打开
O_NONBLOCK 非阻塞模式
O_SYNC 等待写完成(数据和属性)
O_DSYNC 等待写完成(仅数据)
O_RSYNC 同步读写
O_FSYNC 等待写完成(仅FreeBSD和Mac OS X)
O_ASYNC 异步I/O(仅FreeBSD和Mac OS X)
12.int ioctl(int d, int request, ...)
该函数进行I/O设置,不能用本章中其他函数设置的I/O操作都可用该函数完成。比如终端I/O等等。声明在sys/ioctl.h中。
第4章 文件和目录
1.int stat(const char *path, struct stat *buf)
int fstat(int fd, struct stat *buf)
int lstat(const char *path, struct stat *buf)
这几个函数用来获取文件状态(从inode节点中取出,除了st_ino之外),将文件状态保存在buf指向的struct stat类型结构体中。声明在sys/stat.h中。区别在于,stat和lstat中文件以路径名形式提供,fstat中文件以文件描述符形式提供,另外,lstat用来获得软连接的文件信息,而不是软连接所指向文件的信息。struct stat结构体信息如下:
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* protection */ //该域中保存了文件的访问权限以及文件类型的信息,还包括了设置用户ID位和设置组ID位
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for filesystem I/O */
blkcnt_t st_blocks; /* number of 512B blocks allocated */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last status change */
};
2.S_ISDIR
S_ISREG
S_ISCHR
S_ISBLK
S_ISFIFO
S_ISLNK
S_ISSOCK
这几个都是宏定义,依据struct stat中st_mode成员的值进行判断(eg: S_ISDIR(buf.st_mode)),当前文件是否是目录文件,普通文件,字符设备,块设备,FIFO,软连接,套接字。声明在sys/stat.h中。
3.S_ISUID
S_ISGID
这两个宏依据struct stat中st_mode成员的值,来测试该文件是否设置了“设置用户ID位”和“设置组ID位”。
4.int access(const char *pathname, int mode)
该函数用来检测当前进程的实际用户和实际组是否有访问由pathname所指示的文件。声明在unistd.h文件中。mode可取以下值:
R_OK 测试读权限:
W_OK 测试写权限
X_OK 测试执行权限
F_OK 测试文件是否存在
5.mode_t umask(mode_t mask)
该函数用来设置当前进程的文件模式创建屏蔽字,这会屏蔽掉当前进程中创建的文件的那些权限。声明在sys/stat.h文件中。该函数不改变shell的屏蔽字。mask取值集合和open函数的mode参数一致。注意:umask中mask包含的权限会被屏蔽掉,这和open用法相反。
6.int chmod(const char *path, mode_t mode)
int fchmod(int fd, mode_t mode)
这两个函数用来改变文件的访问权限。声明在sys/stat.h文件中。区别在于一个提供了文件的路径,另一个提供了文件描述符。此外,该函数的mode中除了能包含open的mode取值集合,还可以包含如下三个:S_ISUID,S_ISGID,S_ISVTX,其中前两个分别是设置用户ID位和设置组ID位,第三个是粘住位。
7.int chown(const char *path, uid_t owner, gid_t group)
int fchown(int fd, uid_t owner, gid_t group)
int lchown(const char *path, uid_t owner, gid_t group)
这几个函数用来设置文件的用户ID和组ID。声明在unistd.h文件中。第一三个通过文件路径来提供文件,第二个用fd提供文件。第三个函数只是修改符号链接的用户ID和组ID,没有修改符号链接指向的文件。只有调用这些函数的进程的有效用户为文件的拥有者时才能完成修改,超级用户可以修改任何文件。
8.int truncate(const char *path, off_t length)
int ftruncate(int fd, off_t length)
这两个函数用来将文件截短为length字节(即将length字节以后的部分去掉)。声明在unistd.h文件中。
9.int link(const char *oldpath, const char *newpath)
该函数为oldpath文件创建一个硬链接newpath(实际上就是个目录项)。声明在unistd.h文件中。最后oldpath和newpath指向了同一个inode节点。inode中的链接计数加1。
创建硬链接一般需要:1.超级用户权限才能创建目录的硬链接,2.硬链接和文件位于同一文件系统中。
10.int unlink(const char *pathname)
该函数删除一个目录项(也就是删除一个硬链接),该目录项指向的inode中的链接计数减1。声明在unistd.h文件中。
11.int remove(const char *pathname)
这是c的标准库函数,声明在stdio.h中。用来删除文件或目录的链接。删除文件时相当于unlink,删除目录时相当于rmdir。
12.int rename(const char *oldpath, const char *newpath)
该函数是c的标准库函数,声明在stdio.h中。用来为文件或者目录重命名。
13.int symlink(const char *oldpath, const char *newpath)
该函数用来为oldpath文件创建一个符号链接newpath。声明在unistd.h文件中。
14.ssize_t readlink(const char *path, char *buf, size_t bufsiz)
该函数用来读一个符号链接文件中的值(而不是所指向的文件)。声明在unistd.h文件中。虽然open函数可以打开文件,但是由于其跟随符号链接(打开符号链接所指向的文件),故不能打开符号链接文件本身。
15.int utime(const char *filename, const struct utimbuf *times)
该函数用来更改一个文件的访问和修改时间。声明在utime.h文件中。struct utimbuf结构体如下:
struct utimbuf {
time_t actime; /* access time */
time_t modtime; /* modification time */
};
16.int mkdir(const char *pathname, mode_t mode)
该函数用来创建一个新目录。声明在sys/stat.h文件中。创建目录时,至少需要设置一个执行权限位,以允许访问该目录的文件名。
17.int rmdir(const char *pathname)
该函数用来删除一个空目录。声明在unistd.h文件中。
18.DIR *opendir(const char *name)
DIR *fdopendir(int fd)
这两个函数用来打开目录文件。声明在dirent.h文件中。返回一个指向目录流的指针,目录流不用关心。
19.struct dirent *readdir(DIR *dirp)
该函数从打开的目录文件中读取目录项,保存到struct dirent结构中,并返回指向该结构的指针。声明在dirent.h文件中。该结构体如下:
struct dirent {
ino_t d_ino; /* inode number */
off_t d_off; /* not an offset; see NOTES */
unsigned short d_reclen; /* length of this record */
unsigned char d_type; /* type of file; not supported
by all filesystem types */
char d_name[256]; /* filename */
};
20.void rewinddir(DIR *dirp)
该函数用来重置目录流内的指针位置,使之指向目录的开头。声明在dirent.h文件中。
21.int closedir(DIR *dirp)
该函数关闭dirp所指向的目录流。声明在dirent.h文件中。
22.long telldir(DIR *dirp)
该函数返回目录流内指针的当前位置。声明在dirent.h文件中。
23.void seekdir(DIR *dirp, long loc)
该函数设置目录流内的指针位置,下一次readdir将从设置好的位置上读取。声明在dirent.h文件中。
24.int chdir(const char *path)
int fchdir(int fd)
这两个函数更改当前进程的当前工作目录。声明在unistd.h文件中。
25.char *getcwd(char *buf, size_t size)
该函数获取当前进程的当前工作目录。声明在unistd.h文件中。
第5章 标准I/O库
1.int fwide(FILE *stream, int mode)
该函数设置文件流stream的定向(字节定向或宽定向)。但是不设置已经定向的流的定向。该函数无出错返回(返回负值说明是字节定向,返回正值是宽定向,返回0无定向)。声明在wchar.h文件中。根据mode取值不同,函数功能不同:
mode值为负,将流设置为字节定向。
mode值为正,将流设置为宽定向。
mode值为0,不设置流的定向,但返回该流定向值。
2.void setbuf(FILE *stream, char *buf)
int setvbuf(FILE *stream, char *buf, int mode, size_t size)
这两个函数改变给定流stream的缓冲类型。声明在stdio.h文件中。对于setbuf而言,当buf为NULL,则stream流被设置为无缓冲;否则,buf的长度应该定义为BUFSIZE(在stdio.h中定义),将根据stream流是磁盘文件或者终端设备而将其设置为全缓冲或者行缓冲。对setvbuf而言,根据mode取值不同,设置的缓冲区类型也不同:
mode取值_IOFBF,设置为全缓冲,buf为非空,全缓冲为用户的buf,大小为size;buf为NULL,系统自动分配合适大小的全缓冲区。
mode取值_IOLBF,设置为行缓冲,同上,全缓冲改为行缓冲即可。
mode取值_IONBF,设置为无缓冲。(忽略buf和size)
此外,setvbuf函数要在所有对流进行操作的函数之前被调用(打开流以后就立即调用该函数)。
3. int fflush(FILE *stream)
该函数强制冲洗一个流,将stream流的所有未写数据传送到内核(然后写入磁盘/终端设备中)。若stream为NULL,强制冲洗所有输出流。
4.FILE *fopen(const char *path, const char *mode)
FILE *freopen(const char *path, const char *mode, FILE *stream)
FILE *fdopen(int fd, const char *mode)
这几个函数打开一个标准I/O流。声明在stdio.h文件中。fopen打开指定的文件;freopen在指定的流上打开一个指定的文件,如果指定流已打开,那么先关闭该流,如果指定流已定向,那么清除该定向,该函数一般用来将一个指定的文件打开到标准输入/输出/出错流上;fdopen为一个打开的文件描述符关联一个标准I/O流。这几个函数的mode取值如下:
r/rb 为读而打开
w/wb 为写而打开或为写而创建,并将文件截短为0
a/ab 为追加而打开或为追加而创建
r+/r+b/rb+ 为读写而打开
w+/w+b/wb+ 为读写而打开,并将文件截短为0
a+/a+b/ab+ 为读或追加打开或创建
(b仅代表二进制文件,不影响其他)
fdopen的mode取值有些特殊,取各种“写”的时候,并不截短文件(因为fd表明该文件已经打开,而不是由标准I/O打开的,所以标准I/O没资格截短)。而且取各种“写”的时候不能没有创建功能,因为fd文件已存在,无须创建。
另外,如果多个进程使用标准I/O打开一个文件,那么并发去写文件都可以正确将数据写入文件,标准I/O流的强大之处。。还有,如果同时以读和写打开一个文件,那么:1.如果中间没有fflush,fseek,fsetpos或rewind,则在输出后不能直接跟随输入;2.如果中间没有fseek,fsetpos或rewind,或者一个输入操作没有达到文件尾部,则在输入操作之后不能直接跟随输出。
最后,用标准I/O创建的文件无法说明文件的访问权限。
5.int fclose(FILE *fp)
该函数关闭标准I/O流fp。声明在stdio.h文件中。关闭之前冲洗缓冲区的输出数据,丢弃缓冲区中的输入数据。
6.int getc(FILE *stream)
int fgetc(FILE *stream)
int getchar(void)
这些函数从标准I/O流stream中每次读一个字符。声明在stdio.h文件中。其中getc是宏函数,getchar相当于fgetc(stdin)。返回值为读到的字符,字符类型本为unsigned char,在这里强制转化成int类型,是为了当出错或者到达文件末尾的时候能够返回-1。在stdio.h中,EOF定义为-1,因此将这些函数返回值和EOF比较就可知道是否出错或者是否到达文件末尾。
7.int ferror(FILE *stream)
int feof(FILE *stream)
void clearerr(FILE *stream)
前两个函数是为了判断流是否出错。每个流在FILE对象中维持了两个标志:出错标志和文件结束标志(用以区分出错和到达文件末尾)。它们都声明在stdio.h文件中。第三个函数清除这两个标志。
8.int ungetc(int c, FILE *stream)
该函数将字符c回送到stream流中,而不是文件中。下次从流中读取时,将读到该字符。声明在stdio.h文件中。
9.int putc(int c, FILE *stream)
int fputc(int c, FILE *stream)
int putchar(int c)
这三个函数将字符c输出到输出流中。和6中的三个函数类似,putc是宏函数,putchar相当于fputc(c,stdout)。声明在stdio.h文件中。
10.char *gets(char *s)
char *fgets(char *s, int size, FILE *stream)
gets函数从stdin中读取一行字符存入s中(在stdin中遇到换行符或者EOF结束标志就算一行),不推荐使用该函数,容易造成缓冲区溢出。fgets从指定的流stream中读取,并且指定了缓冲区大小为size,也是每次读取一行,读取的字符数最多为size-1,最后加‘ ’。对于这两个函数而言,读取到一行后,会将' '或者EOF替换为‘ ’。它们都声明在stdio.h文件中。
11.int fputs(const char *s, FILE *stream)
int puts(const char *s)
fputs函数将以‘ ’结尾的串s输出到stream流中。puts函数将以‘ ’结尾的串s输出到stdout中。‘ ’都不写入流中。puts函数最后会将一个‘ ’输出到stdout流中。它们都声明在stdio.h文件中。
12.size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
这两个函数是二进制I/O函数,可以进行块的读写操作。声明在stdio.h文件中。
13.long ftell(FILE *stream)
int fseek(FILE *stream, long offset, int whence)
void rewind(FILE *stream)
这几个函数用来对标准I/O流进行定位(偏移量都以字节为单位)。ftell获取流内指针的当前位置, fseek将流内指针偏移量设为offset,whence提供了偏移量的相对起始位置,这和第三章的第4个函数lseek的该参数取值相同。rewind函数将流内指针设置到文件的开头。对于fseek函数而言,定位文本文件时,whence只能取SEEK_SET,offset只能取两种值:0或ftell的返回值。它们都声明在stdio.h文件中。
14.int fseeko(FILE *stream, off_t offset, int whence)
off_t ftello(FILE *stream)
这两个函数和13中前两个函数用法相同,区别是这两个函数的偏移类型为off_t。声明在stdio.h文件中。
15.int fgetpos(FILE *stream, fpos_t *pos)
int fsetpos(FILE *stream, fpos_t *pos)
第一个函数将定位的流内指针位置保存到pos中,第二个函数可以使用pos将流内指针设置为该位置。声明在stdio.h文件中。
16.int printf(const char *format, ...)
int fprintf(FILE *stream, const char *format, ...)
int sprintf(char *str, const char *format, ...)
int snprintf(char *str, size_t size, const char *format, ...)
这几个函数用于格式化输出。声明在stdio.h文件中。第一个函数输出到标准输出流stdout。第二个函数输出到stream流中。后两个函数输出到str缓冲区中。sprintf可能会造成缓冲区溢出,因此不建议使用。snprintf规定了缓冲区大小为size,因此比较安全。
17.int vprintf(const char *format, va_list ap)
int vfprintf(FILE *stream, const char *format, va_list ap)
int vsprintf(char *str, const char *format, va_list ap)
int vsnprintf(char *str, size_t size, const char *format, va_list ap)
这四个函数和16中的函数类似,区别是将可变参数替换成了ap参数。声明在stdarg.h文件中。
18.int scanf(const char *format, ...)
int fscanf(FILE *stream, const char *format, ...)
int sscanf(const char *str, const char *format, ...)
这几个函数用于格式化输入。声明在stdio.h文件中。第一个函数从标准输入流stdin中读取,第二个函数从stream流中读取,第三个函数从str缓冲区中读取。
19.int vscanf(const char *format, va_list ap)
int vsscanf(const char *str, const char *format, va_list ap)
int vfscanf(FILE *stream, const char *format, va_list ap)
这几个函数类似于18中的函数,也是将可变参数替换成了ap参数。声明在stdarg.h文件中。
20.int fileno(FILE *stream)
该函数将stream流所关联的文件描述符返回。声明在stdio.h文件中。
21.char *tmpnam(char *s)
第一个函数用来产生一个与现有文件名不同的有效路径名字符串。如果s为NULL,函数自动申请空间来存放有效路径名字符串,并返回空间地址,如果s不为NULL,则将有效路径名字符串存放于s中,并将s地址返回,然后就可用该路径名在程序中手动创建文件了。声明在stdio.h文件中。该函数有个问题,就是在产生出一个有效文件名和使用该文件名创建文件中间一般会有时间差,在该时间差内可能别的进程会使用该文件名创建文件,这样就会出现问题。
22.FILE *tmpfile(void)
char *tempnam(const char *dir, const char *pfx)
第一个函数创建一个临时二进制文件,在关闭该文件或程序结束时将自动删除该文件。第二个函数也是用来创建一个临时文件。关闭该文件或者程序运行完后文件会被自动删除。声明在stdio.h文件中。其中,dir规定了创建临时文件所在的目录,pfx如果非NULL的话,是一个最多包含5个字符的字符串,作为文件名的头几个字符。第二个函数也存在21函数的时间差问题。dir有如下要求:
a.如果定义了环境变量TMPDIR,则用其作为目录。
b.如果dir非空,则用dir作为目录。
c.将stdio.h中的字符串P_tmpdir用作目录。
d.将本地目录(通常是/tmp)用作目录。
该函数自动分配空间来存放临时的文件名,不接受用户自己的buf。
23. int mkstemp(char *template)
该函数也是用来创建一个临时文件,返回文件描述符。与上边的函数不同的是,它所创建的文件不会被自动删除。临时文件名由template参数提供,该串的最后6个字符为XXXXXX,然后该函数用不同字符代替XXXXXX,以创建唯一路径名。声明在stdio.h文件中。
第6章 系统数据文件和信息
1.struct passwd *getpwuid(uid_t uid)
struct passwd *getpwnam(const char *name)
这两个函数通过口令文件(/etc/passwd,该文件中存放了用户的所有信息除了用户密码),将inode中的用户ID或者登录时输入的登录名在口令文件中的项找出来,填入struct passwd结构中,并返回该结构指针,该结构是函数中定义的静态结构,以后的函数调用会不断覆盖该结构的内容。该结构中包含了用户的各种信息。声明在pwd.h文件中。
2.struct passwd *getpwent(void)
void setpwent(void)
void endpwent(void)
第一个函数在循环中,可以逐个取出口令文件中的项。第二个函数用于返绕口令文件,即设置口令文件指针到文件的开头。第三个函数用于关闭由第一个函数打开的口令文件。它们都声明在pwd.h文件中。
3.struct spwd *getspnam(const char *name)
struct spwd *getspent(void)
void setspent(void)
void endspent(void)
这几个函数用来读取阴影文件(/etc/shadow,保存着用户名和用户密码)的项。声明在shadow.h文件中。这些函数用法和1,2函数类似。第一个函数根据用户名name获得对应的项,第二个函数在循环中可以逐个取出阴影文件中的项,最后两个函数用来返绕和关闭阴影文件。
4.struct group *getgrgid(gid_t gid)
struct group *getgrnam(const char *name)
这两个函数通过组文件(/etc/group,该文件中存放了所有用户组的信息),将组ID或者组名对应的项读取出来,写入struct group结构,并返回该结构的指针。类似于1中的函数。声明在grp.h文件中。struct group结构体如下:
struct group {
char *gr_name; /* group name */
char *gr_passwd; /* group password */
gid_t gr_gid; /* group ID */
char **gr_mem; /* group members */ //该组的所有用户名构成的数组
};
5.struct group *getgrent(void)
void setgrent(void)
void endgrent(void)
这些函数用来读取组文件中的项,类似于2中的函数。声明在grp.h文件中。
6.int getgroups(int size, gid_t list[])
该函数用来获取当前用户的附加组ID。声明在unistd.h文件中。每个用户除了可以加入口令文件中显示的那个组外,同时可以加入若干个附加组。对于该函数,list数组用来存放所有的附加组ID数值,size说明了数组的元素个数。当size为0是,该函数返回当前用户的附加组数(可以用来分配list的长度)。
7.int setgroups(size_t size, const gid_t *list)
该函数为当前用户设置附加组ID表。声明在grp.h文件中。该函数一般只在initgroups函数中进行调用。
8.int initgroups(const char *user, gid_t group)
该函数用来为当前用户初始化附加组ID表。声明在grp.h文件中。该函数会读整个组文件,找到user所在的所有组,构成list数组后,调用setgroups函数来设置。并将当前用户所在组的ID号group也存入附加组ID表。
9.struct utmp
/run/utmp文件中保存了当前所有登录到系统中的用户。通过该结构体可以对文件中的项进行读取和写入。
10.int uname(struct utsname *buf)
该函数可以获取当前主机和操作系统的有关信息。声明在sys/utsname.h文件中。struct utsname结构体如下:
struct utsname {
char sysname[]; /* Operating system name (e.g., "Linux") */
char nodename[]; /* Name within "some implementation-defined
network" */
char release[]; /* Operating system release (e.g., "2.6.28") */
char version[]; /* Operating system version */
char machine[]; /* Hardware identifier */
#ifdef _GNU_SOURCE
char domainname[]; /* NIS or YP domain name */
#endif
};
11.int gethostname(char *name, size_t len)
该函数获取主机的名字存入name数组中,len指定了数组长度。声明在unistd.h文件中。
12.time_t time(time_t *t)
该函数返回当前时间和日期(从1970.1.1 0:0:0开始至今的秒数),存入t所指向的变量中。声明在time.h文件中。
13.int gettimeofday(struct timeval *tv, struct timezone *tz)
该函数也是返回当前时间,将时间保存在struct timeval结构体中。声明在sys/time.h文件中。该函数比time函数有更高分辨率(微秒级),tz一般为NULL。struct timeval结构体如下:
struct timeval {
time_t tv_sec; /* seconds */ //秒,也是从1970.1.1 0:0:0开始至今的秒数,和time函数获取数值的相同
suseconds_t tv_usec; /* microseconds */ //微秒
};
14.struct tm *gmtime(const time_t *timep)
struct tm *localtime(const time_t *timep)
第一个函数将timep的秒数转换成国际标准时间(年月日时分秒星期),第二个函数将timep的秒数转换成本地时间。转换后的时间保存在struct tm静态结构体中,然后返回结构体指针。声明在time.h文件中。
15.time_t mktime(struct tm *tm)
该函数将本地时间转换成time_t类型的值(秒数)。声明在time.h文件中。
16.char *asctime(const struct tm *tm)
char *ctime(const time_t *timep)
这两个函数分别将struct tm类型的时间和time_t类型的秒数转换成26字节的字符串。eg:Sun Sep 7 22:27:44 CST 2014。声明在time.h文件中。
17.size_t strftime(char *s, size_t max, const char *format, const struct tm *tm)
该函数将tm中的时间格式化输出到长度为max的s数组中。若max大小不够,则函数返回0。否则返回存入的字符数。声明在time.h文件中。format格式和printf的format类似,但有一些不同,相见《APUE》p145页。
第7章 进程环境
18.void exit(int status)
void _Exit(int status)
这两个函数用来正常终止一个进程。第二个函数立即进入内核,第一个函数执行完清理工作(关闭标准IO)然后进入内核。status是进程的终止状态。声明在stdlib.h文件中。
在main函数中调用exit等价于return语句。
19.void _exit(int status)
该函数也是用来正常终止一个进程。并且直接进入内核。声明在unistd.h文件中。status是进程的终止状态。
注:在我的ubuntu14.04上测试,main函数的结束处未显式的写出返回语句(18,19函数或return),或者返回语句中没有明确的返回状态(数字),则进程终止状态均为6,而不是《apue》中提到的0。
在这里总结下进程的5种正常终止方式和3种异常终止方式:
a.在main函数中执行return。(等价于执行exit,也就是在主线程中结束进程,如果仅想结束主线程而不是进程,则在main最后调用pthread_exit即可)
b.调用exit函数(主线程中或者任意子线程中调用)。该函数将调用由atexit登记过的终止处理函数,并关闭所有的标准I/O流。
c.调用_exit或_Exit函数。这两个函数直接陷入内核,因此就不会执行终止处理程序或者信号处理程序。
d.进程的最后一个线程从其启动例程(就是线程的执行函数)中返回。线程的返回值不会做为进程的返回值,进程将以终止状态0返回。
e.进程的最后一个线程调用pthread_exit函数。这种情况下,进程的终止状态为0,而不是pthread_exit的参数。
3种异常终止方式:
a.调用abort。
b.当进程接收到某些信号。
c.最后一个线程对“取消”请求做出响应。
无论进程以何种方式结束,最终会执行内核中的同一段代码,来关闭所有打开的文件描述符,释放它所使用的存储器等。如果子进程希望父进程知道它是如何结束的,通过调用exit,_exit,_Exit函数,并把退出状态作为参数传递给它们,而在异常终止情况下,内核会产生一个指示其异常终止原因的终止状态,最终父进程通过调用wait或waitpid函数就可以获取到终止状态。exit和_Exit最终还是调用_exit函数,将退出状态转换成进程的终止状态。exit,_exit,_Exit三个函数的参数(退出状态)必然等于进程的终止状态,但是线程的结束函数pthread_exit的参数(退出状态)不会等于进程的终止状态。
20.int atexit(void (*function)(void))
由该函数登记的function函数,会被exit(或者主线程的return/最后一个线程的return)自动调用,也就说只有在进程正常结束时才调用,线程结束不会调用,就算是在某个非主线程中用atexit设置的清理函数,也得等到整个进程结束时才调用。声明在stdlib.h文件中。
21.void *malloc(size_t size)
void *calloc(size_t nmemb, size_t size)
void *realloc(void *ptr, size_t size)
void free(void *ptr)
前三个函数在堆中为进程分配空间,最后一个函数释放这些空间。其中,malloc函数不对分配的空间进行初始化,calloc函数将分配的空间初始化为 0,realloc用来调整(变大或减小)由前两个函数非配空间的大小,其中size参数是新存储区的大小,不是新旧存储区大小之差,当扩大空间时,如果原存储区周围的空闲空间不足,则在其他地方重新开辟空间,再将 原存储区中的值拷贝到新空间;如果原存储区周围空间足够,则在原存储区位置上向高地址方向扩充。声明在stdlib.h文件中。
22.char *getenv(const char *name)
该函数在进程的环境表(环境表是一个指针数组,每个元素指向了“name=value”形式的串)中查找name的环境变量值。返回一个字符串指针。声明在stdlib.h文件中。
23.int putenv(char *string)
int setenv(const char *name, const char *value, int overwrite)
int unsetenv(const char *name)
这几个函数用来设置进程环境表中的环境变量值。声明在stdlib.h文件中。对于putenv函数,会将string指向的串(形式为“name=value”)放到环境表name的项中,如果name已存在则删除name的定义。对于setenv函数,name和value值分别由两个参数提供,如果overwrite非0,则会删除环境表中已存在的name定义,如果overwrite为0,则不会删除删除环境表中已存在的name定义,也不设置新的value值,也不出错。对于unsetenv函数,将删除环境表中已存在的name定义,若环境表中不存在name的定义也不出错。
在改变或者增加环境变量的时候,有如下规定:
a.修改一个现有name,如果新value长度小于或等于现有value长度,则就在原空间中写入新字符串。
b.修改一个现有name,如果新value长度大于现有value长度,则用malloc在堆中为新value分配空间,然后将该空间地址放入环境表中原来的name元素中。
c.新增一个name,如果是第一次新增,调用malloc分配一个新的环境表,并将原来环境表中的值复制过来,将新的value串的指针放在环境表的表尾,再在其后放入一个空指针。此时环境表中的其他大多数指针仍指向了栈顶之上的各个value串。
d.新增一个name,如果这不是第一次新增,说明之前调用过malloc,则使用realloc改变环境表的大小,之后操作类似于c。
24.int setjmp(jmp_buf env)
void longjmp(jmp_buf env, int val)
第一个函数将调用时的堆栈内容保存在env中,以供第二个函数使用。第二个函数使用env值,会跳到设置env的函数中(即setjmp),由于setjmp可以对应多个longjmp函数,因此val变量传递的数字会对多个longjmp函数加以区分。
《apue》上说,对于无优化的编译,使用longjmp函数跳回后,全局变量,静态局部变量,自动变量,寄存器变量,volatile声明的变量都不会回滚到以前的值。对于优化后的编译,自动变量和寄存器变量会回滚到以前的值。笔者未进行过测试。
25.int getrlimit(int resource, struct rlimit *rlim)
int setrlimit(int resource, const struct rlimit *rlim)
每个进程都有一组资源限制,这两个函数可以获取和设置每个资源限制。声明在sys/resource.h文件中。resource代表资源限制的类型,struct rlimit结构体中装有该资源的软限制值和硬限制值。该结构体如下:
struct rlimit {
rlim_t rlim_cur; /* Soft limit */
rlim_t rlim_max; /* Hard limit (ceiling for rlim_cur) */
};
a.任何一个进程都可将一个软限制值更改为小于或等于其硬限制值。
b.任何一个进程都可降低其硬限制值,但它必须大于或等于其软限制值,这种降低对普通用户而言是不可逆的。
c.只有超级用户进程可以提高硬限制值。
第8章 进程控制
1.pid_t getpid(void)
pid_t getppid(void)
uid_t getuid(void)
uid_t geteuid(void)
gid_t getgid(void)
gid_t getegid(void)
这些函数用来返回当前进程的进程ID,父进程ID,实际用户ID,有效用户ID,实际组ID,有效组ID。它们均无出错返回。声明在unistd.h文件中。注意:当前进程的保存的设置用户ID无法返回。
2.pid_t fork(void)
该函数为调用进程创建一个子进程。声明在unistd.h文件中。由该函数创建的子进程和其父进程使用了”写时复制“技术。在父进程中,该函数返回一个正数;在子进程中,该函数返回0;出错的话该函数返回-1。
子进程会继承父进程的以下属性或资源:
a.实际用户ID,实际组ID,有效用户ID,有效组ID。
b.附加组ID。 c.进程组ID。 d.会话ID。 e.控制终端
f.设置用户ID标志和设置组ID标志。 g.当前共组目录。
h.根目录 i.文件模式创建屏蔽字。
j.信号屏蔽和安排。
k.针对任一打开文件描述符的在执行时关闭(close-on-exec)标志。
l.环境。 m.链接的共享存储段。 n.存储映射 o.资源限制。
p.父进程所打开的文件描述符的文件表项(父子进程共享由父进程打开的文件)。
父子进程有如下区别:
a.fork返回值不同。 b.进程ID不同。
c.两个进程具有不同的父进程ID。
d.子进程的tms_utime,tms_stime,tms_cutime,tms_ustime均被设置为0。
e.父进程设置的文件锁不会被子进程继承。
f.子进程未处理的闹钟(alarm)被清除。
g.子进程的未处理信号集设置为空集。
fork失败的两个主要原因:
a.系统中的进程数太多。
b.该实际用户拥有的进程总数超过了系统限制。
fork有下面两种用法:
a.父进程希望子进程复制父进程的代码,使得父,子进程执行不同的代码段。网络服务器进程一般这么用。
b.子进程执行其他的程序。子进程的fork返回后立即调用exec。
3.pid_t vfork(void)
该函数和fork类似,也是用来为当前进程创建一个子进程。区别是:该函数不使用“写时复制”技术。也就是说,该函数创建的子进程目的就是要用exec执行一个新程序。子进程如果不执行exec函数,就永远运行在父进程的地址空间中,就算执行写操作,也不会复制父进程的地址空间到子进程。此外,vfork会保证子进程先运行,子进程调用exec或exit之后父进程才可能被调度。(如果在调用者两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁)
4.pid_t wait(int *status)
pid_t waitpid(pid_t pid, int *status, int options)
在父进程中调用这两个函数来等待子进程的结束并收回子进程所占资源。声明在sys/wait.h文件中。二者区别是:wait函数等待第一个结束的子进程,如果还没有子进程结束的话父进程会阻塞;waitpid进程则可以等待由pid指定的进程,并且option提供了若干选项,可以选择该函数非阻塞。有若干个宏可以用来检测这两个函数返回的子进程状态status,详见man手册。下面说下waitpid函数的pid和options参数的用法:
pid == -1 等待任一子进程,相当于wait函数
waitpid > 0 等待进程ID为pid的子进程
pid == 0 等待进程组ID等于调用进程组ID的任一子进程
pid < -1 等待进程组ID等于pid绝对值的任一子进程
options选项:
WCONTINUED 若系统支持作业控制,那么由pid指定的任一子进程在暂停后已经继续,但其状态尚未报告,返回其状态
WNOHANG 若由pid指定的子进程还没有结束,则waitpid函数不阻塞,立即返回
WUNTRACED 若系统支持作业控制,那么由pid指定的任一子进程处于暂停状态,并且自暂停状态以来还未报告过,返回其状态
5. int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options)
该函数和waitpid函数类似,也是等待指定pid的子进程返回。声明在sys/wait.h文件中。但区别是:该函数把要等待的子进程ID和子进程所在组ID区分开了,分别表示。infop中包含了引起子进程状态改变的信号的详细信息。idtype提供了要等待的ID类型:
P_PID 等待进程号为id的子进程
P_PGID 等待进程组号为id的子进程
P_ALL 等待任一子进程,忽略id
options选项:
WCONTINUED 同上
WEXITED 等待已退出的进程
WNOHANG 同上
WNOWAIT 不破坏子进程的退出状态,该退出状态可有后续的wait,waitid,waitpid调用取得
WSTOPPED 等待一个已暂停但是状态还未报告的子进程
6.pid_t wait3(int *status, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage)
这两个函数和上面函数类似,只是多了一个功能:rusage可以收集到子进程的资源统计信息(用户CPU时间总量,系统CPU时间总量,页面出错次数,接收到信号的次数等)。声明在sys/wait.h文件中。
7.int execl(const char *path, const char *arg, ...)
int execlp(const char *file, const char *arg, ...)
int execle(const char *path, const char *arg, ..., char * const envp[])
int execv(const char *path, char *const argv[])
int execvp(const char *file, char *const argv[])
int execvpe(const char *file, char *const argv[], char *const envp[])
这些函数将调用进程所执行的程序替换为新程序,一般在子进程中使用。声明在unistd.h文件中。它们之间的区别:函数名中包含p的,以文件名(file)为参数提供可执行文件(若file串中包含/,将它视为路径名;否则安PATH环境变量来找),函数名不包含p的,是以路径名(path)为参数来提供可执行文件;函数名中包含l的(前三个函数),命令行参数为可变参数(除了第0个参数,它已经用arg给出来了),其他函数是以指针数组形式提供命令行参数;函数名以e结尾的,最后一个参数是环境表,其他函数则没有环境表参数。
子进程中通过exec函数执行新程序之后,2中所列出的a-o那些属性不会改变,外加tms_utime,tms_stime,tms_cutime,tms_ustime这些值也不会改变,还要注意实际用户ID和实际组ID是否改变根据新可执行文件的设置用户ID位和设置组ID位是否设置而定;p的话,要根据所打开文件描述符是否设置FD_CLOEXEC标志(执行时关闭close-on-exec)有关,若设置了就关闭,否则不关闭。
8.int setuid(uid_t uid)
int setgid(gid_t gid)
这两个函数分别用来设置当前进程的实际用户ID/有效用户ID和实际组ID/有效组ID。声明在unistd.h文件中。有如下设置规则:
a.如果进程具有超级用户权限,可将进程的实际用户ID,有效用户ID,保存的设置用户ID设置为uid。
b.如果进程没有超级用户权限,但是uid等于进程的实际用户ID或保存的设置用户ID,则只将有效用户ID设置为uid,不改变实际用户ID和保存的设置用户ID。
c.如果a和b都不满足,则将error设置为EPERM,并返回-1。
还要注意以下三点:
a.只有超级用户进程可以更改实际用户ID。
b.仅当对程序文件设置了设置用户ID位时,exec函数才会设置有效用户ID为程序文件的拥护者ID。否则exec函数不改变有效用户ID,维持原先值。上边的b表明了任何时候都可调用setuid将有效用户ID设置为实际用户ID或保存的设置用户ID。
c.保存的设置用户ID是有exec复制有效用户ID而得来的。
9.int setreuid(uid_t ruid, uid_t euid)
int setregid(gid_t rgid, gid_t egid)
这两个函数用来交换实际用户ID和有效用户ID,实际组ID和有效组ID。声明在unistd.h文件中。任一参数值为-1,则相应的ID应当保持不变。
10.int seteuid(uid_t euid)
int setegid(gid_t egid)
这两个函数类似于8中的函数,但是只改变当前进程的有效用户ID,无论是当前是特权用户还是非特权用户。声明在unistd.h文件中。特权用户进程可将有效用户ID设置为euid,非特权用户进程可将有效用户ID设置为实际用户ID或保存的设置用户ID。
注意:上述所有的组ID设置和用户ID设置类似,不再赘述。附加组ID不受这些函数影响。
11.解释器文件:第一行包含“#!pathname”的文件就是解释器文件。shell会创建一个子进程来执行pathname所指示的命令,然后该命令再去解释执行解释器文件。解释器文件不一定非得是shell脚本,也可能是其他脚本(比如awk程序脚本)。每一种类型的解释器文件都会有自己的命令解释器。内核在执行pathname命令时,传递给该命令的参数如下:argv[0]:pathname路径;argv[1]:解释器文件#!pathname后边跟的参数;argv[2]:exec的第0个参数(解释器文件的绝对路径);argv[3]及以后的参数:exec的第2个及以后的参数。
eg:execl(“home/sar/bin/testinterp”, "testinterp", "myarg1", "MY ARG2", (char *)0);
其中home/sar/bin/testinterp是解释器文件。查看该解释器内容:cat home/sar/bin/testinterp ===>#!/home/sar/bin/echoarg foo。该解释器文件第一行包含了解释器命令为/home/sar/bin/echoarg,foo为该命令的参数。execl将执行/home/sar/bin/echoarg程序,将“/home/sar/bin/echoarg”,“foo”,“home/sar/bin/testinterp”,"myarg1","MY ARG2"作为参数传递进去。
12.int system(const char *command)
该函数用来执行一个shell命令。声明在stdlib.h文件中。在该函数内部调用了fork,exec,waitpid。command字符串中包含了命令以及参数等(包括由管道连接的多个命令,以及输入输出重定向),shell会对其进行语法分析,将其分成命令行参数。一般情况下,fork和exec之间会有安全漏洞问题,需要在fork之后,exec之前将子进程的有效用户改为普通用户,然后再让子进程执行exec。该函数中fork出的子进程去执行/bin/sh程序,然后shell子进程再创建子进程去执行command。由于该函数中调用了fork,exec和waitpid,因此该函数的返回值较为特殊,需要注意,有三种返回值:
a.如果fork失败或者waitpid返回除EINTR之外的出错,则system返回-1,并且errno中设置了错误类型。
b.如果exec失败(表示不能执行shell),则system返回值如同shell执行了exit(127)一样。
c.否则三个函数都执行成功,则system返回值是shell的终止状态。
注:该函数对信号的处理有要求。该函数在fork子进程前,会先屏蔽掉SIG_INT和SIGQUIT信号,同时屏蔽掉调用进程的SIGCHLD信号(防止用户在该信号处理函数中调用了wait等函数回收子进程,这样system函数中的wait函数就获取不到子进程的终止状态),函数返回时会恢复一切。
第10章 信号
1.typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler)
该函数用来设置进程的信号处理函数。一般不推荐使用该函数,而应该使用sigaction函数替代它。声明在signal.h文件中。signum参数是要设置的信号名,handler参数是一个函数指针变量,该变量也可以取以下三种值值:SIG_IGN,SIG_DFL,信号处理函数的地址。第一个表示忽略此信号,第二个表示采用系统的默认动作处理此信号,第三个表示此信号发生时,调用该处理函数来处理此信号。函数返回值是前一个信号处理函数的指针。一般情况下,执行一个程序时,如果没有设置信号的处理函数,则系统会把信号设置为默认的处理动作。当进程执行exec函数后,也会把之前设置的信号处理动作更改为信号的默认处理动作。当进程创建了一个子进程时,子进程会继承父进程的信号处理方式。
注:信号的三种处理方式:忽略,捕获,默认方式(默认方式一般是终止接收进程)。
2.int kill(pid_t pid, int sig)
int raise(int sig)
第一个函数将新号发送给进程或者进程组。第二个函数向本进程发送信号。声明在signal.h文件中。第二个函数等价为kill(getpid(),sig)。kill函数的pid有以下四种情况:
a.pid > 0 将该信号发送给进程ID为pid的进程
b.pid == 0 将该信号发送给和发送进程属于同一个进程组的所有进程(不包括系统进程),而且发送进程具有向这些进程发送信号的权限。
c.pid < 0 将信号发送给进程组ID等于|pid|的所有进程(不包括系统进程)。并且也要具有发送权限,类似于b
d.pid == -1 将信号发送给系统上的所有进程(不包括系统进程)。并且也要具有发送权限,类似于b和c
上边提到的发送权限是这样的:超级用户可将信号发送给任意进程;非超级用户,是否有权限的依据是发送者的实际或者有效用户ID是否等于接收者的实际或者有效用户ID,相等的话就具有权限,否则不具有权限。另外,普通进程可以将SIGCONT信号发送给同一个会话中的所有进程,包括系统进程。
如果kill的sig参数为0,用来测试pid号进程是否存在,不存在的话kill返回-1并设置errno为ESRCH,这种测试一般没有什么价值。另外,kill函数向本进程发送信号时,会在函数返回前将信号传递至该进程。
3.unsigned int alarm(unsigned int seconds)
该函数设置一个闹钟(计时器),当闹钟时间到之后会投递一个SIGALRM信号至调用进程。该信号的默认处理是终止调用进程。每个进程只能有一个闹钟时间,如果调用alarm函数设置了一个闹钟时间,在该时间到之前又调用alarm函数设置了一个闹钟时间,则第二次调用alarm函数返回值为第一次alarm调用设置时间的剩余值,然后按照第二次设置的时间重新计时;如果第二次alarm调用参数为0,则取消闹钟时间,并将第一次alarm调用设置时间的剩余值返回。声明在unistd.h文件中。
4.int pause(void)
该函数将进程挂起直到捕捉到一个信号。声明在unistd.h文件中。当该挂起进程接收到一个信号,执行完信号处理函数并从其返回时,pause函数才返回,返回值为-1,并将errno设置为EINTR。
5.int sigemptyset(sigset_t *set)
int sigfillset(sigset_t *set)
int sigaddset(sigset_t *set, int signum)
int sigdelset(sigset_t *set, int signum)
int sigismember(const sigset_t *set, int signum)
这五个函数用来初始化信号集。声明在signal.h文件中。sigemptyset函数会清空set所指向的信号集。sigfillset函数会将所有信号放入set指向的信号集中。所有信号集在使用之前,都要调用这两个函数的其中之一来初始化信号集。一旦初始化了一个信号集,就可使用其余的函数在信号集中增加或删除特定的信号。
如果实现的信号数目少于一个整型所包含的位数,则可以用整型变量作为信号集,整型的每一位代表一个信号。此时,sigemptyset函数将整型量设置为0,sigfillset函数将整型量的各个位都设置为1。这两个函数被实现为宏函数:
#define sigemptyset(ptr) {*(ptr) = 0}
#define sigfillset(ptr) {*(ptr) = ~(sigset_t)0, 0}
可看出,这两个宏函数返回值均为0。
sigaddset函数打开一位(将该位置为1),sigdelset函数关闭一位(将该位置为0),sigismember函数测试一个指定位。
6.int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
该函数设置进程的信号屏蔽字。声明在signal.h文件中。如果oldset参数非空,则进程的当前信号屏蔽字通过oldset返回;如果set参数非空,这时how指定当前信号屏蔽字的修改方法。how的取值如下:
SIG_BLOCK 该进程新的信号屏蔽字是其当前的信号屏蔽字和set指向信号集的并集。set包含了我们希望阻塞的附加信号。
SIG_UNBLOCK 该进程新的信号屏蔽字是其当前的信号屏蔽字和set指向信号集的交集。set包含了我们希望解除阻塞的信号。
SIG_SETMASK 该进程新的信号屏蔽字将被set指向的信号集所替代。
如果set为空,则不改变该进程的信号屏蔽字,how值也无意义。另外,调用sigprocmask函数后如果有任何没被阻塞的未决信号,会在该函数返回前被递送给该进程。
注意:sigprocmask函数仅能在单线程的进程中使用。多线程中需要使用pthread_sigmask函数。
7.int sigpending(sigset_t *set)
该函数用来查看当前进程的未决信号(实际上就是被阻塞的信号)集。该信号集通过set参数返回。声明在signal.h文件中。
8. int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
该函数类似于signal函数,用来查看或者设置信号的处理函数。声明在signal.h文件中。一般推荐使用该函数,去替代signal函数。参数signum是要查看或者修改的信号编号,若参数act非空,则要修改信号处理动作,若参数oldact非空,返回该信号的上一个处理动作。struct 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);
};
sa_handler字段指向了信号处理函数的地址;sa_mask字段指定了一个信号集,调用上述信号处理函数之前会屏蔽掉该信号集,从信号处理函数中返回时恢复信号屏蔽字(这样就保证了在处理一个给定信号时,若这种信号再次发生,它将被阻塞到对前一个信号的处理结束为止,如果同一种信号发生多次,通常部将它们排队,也就是说当解除信号阻塞后,该信号只会被投递一次)。sa_flags字段指定对信号进行处理的各个选项:
SA_INTERRUPT 由此信号中断的系统调用不会自动重启。
SA_NOCLDSTOP 若signum参数是SIGCHLD,当子进程暂停(作业控制)时或者由暂停继续运行时,不产生此信号;当子进程终止时产生此信号。
SA_NOCLDWAIT 若signum参数是SIGCHLD,则当调用进程的子进程终止时,不产生将死进程;若调用进程依然wait子进程,则调用进程阻塞知道所有子进程都终止,才 会返回,返回值为-1,并将errno设置为ECHILD。
SA_NODEFER 当捕捉到该信号并执行其信号处理函数时,系统不自动阻塞此信号(除非sa_mask中包括了此信号)。此种类型操作对应于早期的不可靠信号。
SA_ONSTACK 若用sigaltstack函数声明了一个替换栈,则将此信号递送给替换栈上的进程。
SA_RESETHAND 在此信号函数的入口处,将此信号的处理方式复位为SIG_DFL,并清除SA_SIGINFO标志。此种类型操作对应于早期的不可靠信号。此标志不能自动复 位SIGILL和SIGTRAP这个信号。设置了此标志也会有设置SA_NODEFER标志后的效果。
SA_RESTART 由此信号中断的系统调用会自动重启动。
SA_SIGINFO 此选项会用sa_sigaction指向的信号处理函数来处理此信号,而不用sa_handler所指向的信号处理函数。
sa_sigaction指针指向的信号处理函数类型为:void * function(int, siginfo_t *, void *)。siginfo_t结构体类型如下:
该结构体将包含信号产生原因的有关信息。
9.int sigsetjmp(sigjmp_buf env, int savesigs)
void siglongjmp(sigjmp_buf env, int val)
如果要在信号处理函数中实现远跳转,应该使用这两个函数进行堆栈保存和恢复,而避免使用setjmp和longjmp。因为执行信号处理程序时系统自动设置当前信号的屏蔽字,信号处理程序结束时系统自动恢复屏蔽字,而setjmp和longjmp函数不对屏蔽字进行任何操作,因此使用longjmp远跳转之后,当前信号的屏蔽字就无法恢复了(以后无法处理当前信号了)。因此需要使用9中这两个函数。sigsetjmp函数比setjmp多出了一个savesigs参数,该参数非0时,该函数会保存当前进程的信号屏蔽字,然后siglongjmp函数会将保存的信号屏蔽字恢复,这样当从信号处理函数中远跳转后,进程的信号屏蔽字仍然会恢复到以前的状态。声明在setjmp.h文件中。
10.int sigsuspend(const sigset_t *mask)
进程中调用该函数用来等待一个信号。声明在signal.h文件中。mask参数用来设置进程的信号屏蔽字,也就是说,该函数将等待除了被屏蔽掉的信号以外的任意一个信号到来,在这一信号到来之前当前进程处于阻塞状态。信号到来之后并执行完信号处理函数后,才从该函数返回。该函数返回时,会自动将信号屏蔽字恢复为该函数被调用之前的状态。该函数的返回值总是-1,并将errno设置为EINTR,表示一个被中断的系统调用。
11.void abort(void)
该函数发送SIGABRT信号至当前进程,使当前进程非正常终止。无论程序中在调用abort之前,对SIGABRT信号的处理进行何种设置,abort都会终止该进程(abort内部会重新将SIGABRT信号的处理设置为默认方式)。声明在stdlib.h文件中。
12.unsigned int sleep(unsigned int seconds)
该函数使调用线程睡眠seconds秒。该函数一般使用alarm实现,因此该函数和用户调用的alarm的之间有交互作用,需要注意。该函数是调用线程睡眠,并不是整个进程睡眠,当然,如果进程只有一个主线程,那么主线程睡眠就相当于进程睡眠。
注意:信号这一章节有个重要的知识,就是中断的系统调用。一般情况下,系统调用将进程阻塞后,如果进程收到了任意可以处理的信号,就会激活进程,若设置了该信号的处理函数,则去执行该处理函数并且返回后也会从该引起进程阻塞的系统调用处返回。也有一部分系统调用会重新启动。
13.与作业控制有关的信号:
SIGCHLD 子进程已暂停或终止
SIGCONT 使暂停的进程继续运行
SIGSTOP 使进程暂停(不能捕获或忽略)
SIGTSTP 交互式暂停信号(用终端命令使进程暂停,比如ctrl+z)
SIGTTIN 后台进程组成员读控制终端
SIGTTOU 后台进程组成员读控制终端
除了SIGCHLD以外,其它信号应用程序不去处理。当按ctrl+z时,SIGTSTP信号被送至前台进程组的所有进程,使之暂停(并转入后台)。在命令行下使用fg %n或bg %n时,SIGCONT信号被发给相应进程,使之转向前台继续运行或者在后台继续运行。对一个进程产生四种停止信号(SIGTSTP,SIGSTOP,STGTTIN,SIGTTOU)中的任意一种时,对该进程的任一未决SIGCONT信号就会被丢弃。类似,当对一个进程产生SIGCONT信号时,对同一进程的任一未决的停止信号将被丢弃。当对一个暂停的进程产生SIGCONT信号时,无论SIGCONT信号是被阻塞或是忽略,进程都将继续运行。
14. void psignal(int sig, const char *s)
该函数将字符串s输出到标准出错文件,后接一个冒号和空格,再接着是对sig信号的说明,最后是换行符。声明在signal.h文件中。类似于perror函数。
15.char *strsignal(int sig)
给出一个信号sig,函数会返回说明该信号的字符串。应用程序可用该字符串打印关于接收到信号的出错消息。声明在string.h文件中。类似于strerror函数。
第11章 线程概念
1.int pthread_equal(pthread_t t1, pthread_t t2)
该函数用来比较两个线程IDti和t2是否相等。相等返回非0值,不相等返回0。声明在pthread.h文件中。由于每个平台的pthread_t类型不一定相同,因此不能直接比较,只能通过该函数比较。
2.pthread_t pthread_self(void)
该函数获取当前线程的线程ID。声明在pthread.h文件中。1,2函数一般可以配合来使用。
3.int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg)
该函数用来创建一个新线程。声明在pthread.h文件中。thread中包含了新线程的线程ID。attr结构可以为新线程设置线程属性。start_routine是线程的启动例程(线程要执行的函数)。arg为线程传递参数,一般是结构类型指针。线程创建后并不保证哪个线程先运行,所以不能做任何假设。新线程继承了调用线程的浮点环境和信号屏蔽字,新线程的未决信号集被清除。并且每个线程都有errno的副本,当线程出错后,错误码会保存在各自的errno中。新创建的线程一般通过2中函数来获取自己的线程ID,而不能使用thread参数来获取,因为新线程可能在创建函数返回前就运行了,而此时也许thread还未设置成线程ID。其他平台同一个进程中的多个线程,它们的进程号是相同的,线程号是不同的;但是Linux系统,由于它的线程和进程实现机制是一样的,都用clone系统调用来完成创建,因此Linux系统中的线程实际上是其创建进程的子进程,该子进程可以共享父进程一定数量的执行环境。
4.void pthread_exit(void *retval)
该函数用来结束调用线程。声明在pthread.h文件中。参数retval是线程的返回码。其他线程可以通过pthread_join函数获得该返回码。线程的三种退出方式如下:
a.线程从启动例程中返回,返回值是线程的退出码
b.线程可以被同一进程中的其他线程取消
c.线程调用pthread_exit返回
5.int pthread_join(pthread_t thread, void **retval)
该函数等待一个线程ID号为thread的线程结束。类似于waitpid。该函数会一直阻塞直到等待的线程调用pthread_exit,或从启动例程返回,或者被其他线程取消。声明在pthread.h文件中。该函数可以获得4中函数的返回码,保存到retval指向的单元,如果对等待线程的返回码不感兴趣,可将retval置为NULL,则该函数不会获取等待线程的返回码。如果等待线程是被其他线程取消的,则retval指向的内存单元被置为PTHREAD_CANCELED。
4,5两个函数的retval参数可以传递结构体指针,来包含更多的数值。该结构体一般要用全局结构或者是malloc分配的单元。
6.int pthread_cancel(pthread_t thread)
线程通过调用该函数来取消同一进程中的其他线程。声明在pthread.h文件中。默认情况下,被取消的线程会表现得跟调用了pthread_exit(PTHREAD_CANCELED)函数一样。线程可以选择忽略取消方式或者控制取消方式。此外,pthread_cancel并不等待线程终止,它仅提出请求。
7.void pthread_cleanup_push(void (*routine)(void *)
void pthread_cleanup_pop(int execute)
第一个函数用来注册线程结束时要执行的清理函数。类似于进程中的atexit函数。清理函数的执行顺序与注册顺序相反。这两个函数需要配对使用,调用了几次pthread_cleanup_push函数,也需要对应的调用几次pthread_cleanup_pop函数。它们均声明在pthread.h文件中。当线程执行以下动作时会调用清理函数:
a.调用pthread_exit时
b.响应取消请求时
c.用非0的execute参数赖掉用pthread_cleanup_pop时
当使用参数execute=0来调用pthread_cleanup_pop函数后,无论在上边那种情况下,清理函数都不会被调用,而会被清理掉。此外,还要注意,当线程从它的启动例程中返回的话(比如使用return),即使注册了清理函数,清理函数也不会被调用。
8.int pthread_detach(pthread_t thread)
该函数用来分离一个线程。声明在pthread.h文件中。一个线程被分离后,它的底层存储资源在线程终止时立即被收回。而不用等待其他线程调用pthread_join函数或者整个进程结束。注意,一个线程分离后,不能再调用pthread_join函数来等待该分离线程。
9.线程同步
a.互斥量
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr)
int pthread_mutex_destroy(pthread_mutex_t *mutex)
这两个函数用来初始化互斥量(动态初始化)和清理互斥量资源。声明在pthread.h文件中。如果想静态初始化互斥量,定义一个全局的pthread_mutex_t 类型变量,并给其赋值为PTHREAD_MUTEX_INITIALIZER。如果互斥量单元是用malloc等进行分配的,那么在释放内存之前需要调用pthread_mutex_destroy对资源进行清理。用默认属性初始化互斥量时只需把attr置为NULL。
int pthread_mutex_lock(pthread_mutex_t *mutex)
int pthread_mutex_trylock(pthread_mutex_t *mutex)
int pthread_mutex_unlock(pthread_mutex_t *mutex)
使用第一个函数对互斥量进行加锁,使用第三个函数对互斥量解锁。它们均声明在pthread.h文件中。如果互斥量已经上锁,那么调用进程将阻塞直到互斥量被解锁。第二个函数也是用来对互斥量进行加锁,如果互斥量已经上锁,它不会阻塞进程,返回EBUSY,如果成功加锁则返回0。
b.读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr)
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)
这两个函数用来初始化读写锁和清理读写锁。它们和a中的函数类似。声明在pthread.h文件中。如果希望读写锁有默认属性,将attr置为NULL。在释放动态分配的内存前,需要先调用第二个函数释放资源。
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock)
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock)
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock)
这几个函数分别在读模式下或者写模式下锁定读写锁,无论哪种模式下锁定读写锁,都可用第三个函数来解锁。它们均声明在pthread.h文件中。读写锁实现的时候可能会对共享模式下可获取的锁数量进行限制,所以需要检查pthread_rwlock_rdlock函数返回值。
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock)
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock)
这两个函数也类似于a中的函数,获得锁时返回0,锁被占时返回EBUSY,也不会阻塞线程。声明在pthread.h文件中。
c.条件变量
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr)
int pthread_cond_destroy(pthread_cond_t *cond)
这两个函数和上边的函数是类似的。声明在pthread.h文件中。第一个函数用来对条件变量进行初始化。第二个函数用来对条件变量进行资源清理。如果条件变量是由malloc等手动分配的,那么在释放该空间前,要先调用第二个函数进行资源清理。如果使用默认属性进行初始化,attr传递NULL即可。
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex)
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime)
这两个函数用来等待一个条件变量cond为真。声明在pthread.h文件中。如果cond不为真,会将调用线程放到等待条件的线程列表上,并阻塞当前线程,释放互斥锁mutex。第二个函数仅仅等待abstime时长,若cond还不为真则返回错误码,同时获取互斥锁。这两个函数的条件变量cond均受到互斥锁mutex的保护,当函数将线程放入等待条件列表上后,会释放该互斥锁。该互斥锁mutex将会保护两个资源:一个是消息队列,另一个是条件变量cond。具体的使用例子参考《apue》。第二个函数的struct timespec参数类型如下:
struct timespec {
time_t tv_sec, /*seconds*/
long tm_nsec; /*nanoseconds*/
};
第二个函数的等待时间是个绝对数而不是相对数,也就是说,如果需要等待3分钟,就要在当前时间上加3分钟然后转换到struct timespec结构中。
int pthread_cond_signal(pthread_cond_t *cond)
int pthread_cond_broadcast(pthread_cond_t *cond)
这两个函数用于通知线程条件已经满足。第一个函数将唤醒等待该条件的某个线程,第二个函数会唤醒等待该条件的所有线程。
第12章 线程控制
1.int pthread_attr_init(pthread_attr_t *attr)
int pthread_attr_destroy(pthread_attr_t *attr)
这两个函数用于对线程的属性结构体进行初始化和清理。声明在pthread.h文件中。Linux系统支持的线程属性有下边四个:
detachstate 线程的分离属性
guardsize 线程栈末尾的警戒缓冲区大小(字节数)
stackaddr 线程栈的最低地址
stacksize 线程栈的大小(字节数)
可取消状态
可取消类型
并发度
2.int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate)
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)
这两个函数用来获取和设置线程的分离属性。声明在pthread.h文件中。detachstate参数代表了属性值,有两个:PTHREAD_CREATE_DETACHED(以分离状态启动线程),PTHREAD_CREATE_JOINABLE(正常启动线程)。如果一开始就知道不需要获取线程的返回码,那就可以分离属性来创建线程,这样线程就会以分离状态来启动,线程结束时系统就会收回线程资源。
3.int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize)
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize)
这两个函数用来获取和设置线程的线程栈地址属性和线程栈大小属性。声明在pthread.h文件中。一个进程中的所有线程共用该进程的栈地址空间,当线程数太多时,可能会消耗完进程的栈地址空间,于是就需要使用malloc或者mmap为线程栈重新分配空间。分配好之后需要用第二个函数来设置栈。stackaddr是线程栈的最低地址,因此它可能是线程的开始或者结尾(栈从高地址向低地址方向伸展)。
4.int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize)
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize)
这两个函数用来专门获取和设置线程栈的大小属性。声明在pthread.h文件中。如果希望改变线程栈的默认大小,但又不想自己处理栈的分配问题,就可以用该函数。
5.int pthread_attr_getguardsize(pthread_attr_t *attr, size_t *guardsize)
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize)
这两个函数用来获取和设置线程栈的警戒缓冲区属性。声明在pthread.h文件中。参数guardsize控制着线程栈末尾之后用意避免栈溢出的扩展内存(警戒缓冲区)的大小。该缓冲区默认大小是PAGESIZE字节。当把guardsize置为0时,则不为线程栈提供警戒缓冲区。另外,如果对线程属性stackaddr做了修改,也会使线程栈缓冲区机制无效,等同于把guardsize置为0。当线程的栈指针溢出到警戒区域,应用程序就会收到出错信号。
6.int pthread_getconcurrency(void)
int pthread_setconcurrency(int new_level)
这两个函数用来获取和设置线程的并发度。声明在pthread.h文件中。如果系统当前正控制着并发度,那么第一个函数会返回0。用第二个设置函数设置并发度时,只是对系统做一个提示,系统并不保证一定采用new_level参数指定的并发度。使用new_level参数为0来调用设置函数,会撤消在这之前用非0的new_level参数设置的并发度。
7.同步属性
a.互斥量属性
int pthread_mutexattr_init(pthread_mutexattr_t *attr)
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr)
这两个函数用来初始化和清理互斥量的属性。声明在pthread.h文件中。第一个函数会用默认的互斥量属性初始化pthread_mutexattr_t结构。有两个互斥量属性值得注意:互斥量共享属性和类型属性。
int pthread_mutexattr_getpshared(const pthread_mutexattr_t * restrict attr, int *restrict pshared)
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared)
这两个函数用来获取和设置互斥量的共享属性。在一个进程中,多个线程默认可以访问同一个同步对象(指互斥量,读写变量,条件变量),因此线程共用的同步对象属性需要设置为PTHREAD_PROCESS_PRIVATE,这也是默认的属性值;而多个进程可以把同一个内存区映射到它们各自独立的地址空间中,多个进程访问同一个内存区的数据时也需要同步,那么在共享内存区域中分配的互斥量就可用于这些进程的同步,这时需要将互斥量的共享属性设置为PTHREAD_PROCESS_SHARED。
int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type)
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type)
这两个函数用来获取和设置互斥量的类型属性。互斥量类型属性值如下:
PTHREAD_MUTEX_NORMAL 递归加锁:死锁 解别人的锁:未定义 已解锁时解锁:未定义
PTHREAD_MUTEX_ERRORCHECK 递归加锁:返回错误 解别人的锁:返回错误 已解锁时解锁:返回错误
PTHREAD_MUTEX_RECURSIVE 递归加锁:允许 解别人的锁:返回错误 已解锁时解锁:返回错误
PTHREAD_MUTEX_DEFAULT 递归加锁:未定义 解别人的锁:未定义 已解锁时解锁:未定义
在一个函数的加锁,解锁调用之间需要调用另外的函数,而另外的函数内部也要获取同一个锁,这时就需要设置递归锁属性,否则就会死锁。此外,使用递归锁时,需要一定的编程技巧。
b.读写锁属性
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr)
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr)
这两个函数用来初始化和清理读写锁的属性。声明在pthread.h文件中。类似于互斥量的该函数。
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t * restrict attr, int *restrict pshared)
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared)
这两个函数用来获取和设置读写锁的共享属性。声明在pthread.h文件中。类似于互斥量的该函数。读写锁仅支持共享属性,没有类型属性。
c.条件变量属性
int pthread_condattr_init(pthread_condattr_t *attr)
int pthread_condattr_destroy(pthread_condattr_t *attr)
这两个函数用来初始化和清理条件变量的属性。声明在pthread.h文件中。类似于互斥量和读写锁的该函数。
int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr, int *restrict pshared)
int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared)
这两个函数用来获取和设置条件变量的共享属性。声明在pthread.h文件中。类似于互斥量和读写锁的该函数。读写锁仅支持共享属性,没有类型属性。
8.void flockfile(FILE *filehandle)
int ftrylockfile(FILE *filehandle)
void funlockfile(FILE *filehandle)
这几个函数用来为FILE对象加锁和解锁。声明在stdio.h文件中。其中第二个函数不会阻塞当前线程。标准IO函数都是线程可重入的函数(所谓一个函数是可重入的,指的是该函数可以被并发的调用),因为它们会对标准IO流FILE进行加锁访问。并不是说标准IO函数内部调用了这几个函数,而是表现出来的特性就像是调用了这几个函数。这几个函数主要是留给应用程序来调用,对没有加锁的FILE流进行加锁保护。、
注:可重入函数一般分为对线程可重入(线程安全)或者对信号处理程序可重入(异步-信号安全),前者的话就是上边的定义,后者的话指的是在该函数中发生信号后函数仍然是安全的(执行完信号处理函数,返回到该函数中)。
9.int getc_unlocked(FILE *stream)
int getchar_unlocked(void)
int putc_unlocked(int c, FILE *stream)
int putchar_unlocked(int c)
这几个函数是标准IO函数的不加锁版本。声明在stdio.h文件中。在使用这几个函数的时候就需要用到8中的函数。
10.int pthread_key_create(pthread_key_t *key, void (*destructor)(void*))
在分配线程的私有数据之前,需要调用该函数创建与该数据关联的键。这个键用于获取对线程私有数据的访问权。声明在pthread.h文件中。该函数的一般用法:在主线程中调用完成键的创建,然后在其他线程中使用该键去绑定私有数据即可(因此键的变量要定义为全局变量)。创建好的键存放在key所指向的内存单元中,这个键可以被进程中的所有线程使用,而每个线程把这个键与不同的线程私有数据地址进行关联。创建新键时,每个线程的数据地址设为NULL。destructor参数用来为该键关联析构函数,当线程正常退出时,如果传递的destructor参数非NULL,就会调用该析构函数。而当线程调用exit,_exit,_Exit函数或者以非正常方式退出时,就不会调用该析构函数。通常会使用mallocl为线程私有数据分配内存空间,在析构函数中释放这些空间,否则,就可能使线程所属的进程出现内存泄漏。线程可以为私有数据分配多个键,每个键的析构函数可以相同也可以不同。当所有析构函数都调用完后,系统会检查是否还有非NULL的线程私有数据与键关联,有的话继续执行相应析构函数,直到所有私有数据为NULL。
11.int pthread_key_delete(pthread_key_t key)
该函数用来取消键与私有数据值之间的关联关系。声明在pthread.h文件中。该函数在释放键时,并不会激活键的析构函数,因此需要在程序中手动释放由malloc函数分配的私有数据空间。
12.pthread_once_t once_control = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void))
当多个线程中同时调用10中函数创建键的时候,很可能会发生冲突(其实键只需要被创建一次就行)。该函数用来避免此冲突。因为该函数只要在线程中被调用一次后,init_routine参数指向的函数将只会被调用一次,因此在多个线程中都调用一次pthread_once函数,系统就能保证init_routine函数只会被调用一次,然后再在init_routine函数中调用10中的函数来完成键的创建,这样多个线程之间就不会发生冲突。另外,该函数的once_control参数必须是全局或者静态变量,而且被初始化为PTHREAD_ONCE_INIT。具体例子可以参考《apue》。
13.void *pthread_getspecific(pthread_key_t key)
int pthread_setspecific(pthread_key_t key, const void *value)
这两个函数用来用来获取和设置key键所对应的线程私有数据。声明在pthread.h文件中。如果key还没有对应的私有数据,第一个函数返回NULL,否则返回私有数据空间地址,第二个函数可以根据第一个函数的返回值进行判断,进而选择是否要为键设置私有数据空间。另外,只有键所对应的私有数据非空时,析构函数才会被调用。
14.int pthread_setcancelstate(int state, int *oldstate)
该函数用来设置当前线程的可取消状态。声明在pthread.h文件中。该函数把当前的可取消状态设置为state,把原来的可取消状态保存到oldstate指向的单元中。pthread_cancel调用并不等待线程终止,该调用执行后,被取消线程继续运行,当运行到某个取消点时,才被检查自己是否要被取消。《apue》书中列举了POSIX.1所定义的取消点函数。当线程长时间都执行不到这些取消点函数时,可以调用15中的函数,它是专门的取消点函数。线程的可取消状态state有两个值:PTHREAD_CANCEL_ENABLE和PTHREAD_CANCEL_DISABLE,线程启动时默认取值是前者。如果线程的可取消状态值设为前者,则线程在取消点可以被取消;否则设为后者的话,在取消点也不会杀死线程,这时,取消请求对该线程而言处于未决状态,当该线程的可取消状态再次被设为PTHREAD_CANCEL_ENABLE时,该线程将在下一个取消点上对所有的未决请求进行处理。
15.void pthread_testcancel(void)
该函数用来为当前线程添加取消点。声明在pthread.h文件中。如果当前线程的可取消状态被设为PTHREAD_CANCEL_DISABLE,则该函数没有任何效果。
16.int pthread_setcanceltype(int type, int *oldtype)
该函数用来用当前线程设置可取消类型。声明在pthread.h文件中。type是新类型,oldtype存放的是之前的旧类型。线程的可取消类型值有两个:PTHREAD_CANCEL_DEFERRED和PTHREAD_CANCEL_ASYNCHRONOUS。默认情况下,线程的可取消类型是前者,即延迟取消(就是执行完pthread_cancel调用后,被请求线程并不马上被取消)。后者是异步取消,也就是说线程可以在任意时间取消,而不是非得遇到取消点才能被取消。
17.int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset)
该信号用来屏蔽当前线程的某些信号。声明在pthread.h文件中。信号是属于一个进程的,因此可以被进程的所有线程共享,一般情况下,信号到达进程时,哪个线程正在运行,就在该线程的上下文中执行信号处理函数。但是,每个线程都有自己的信号屏蔽字,也就是说每个线程都可以屏蔽自己不想处理的信号,但这不影响其他线程的信号屏蔽字。另外,由于信号是属于进程的,所以任何线程中设置或修改了信号的处理函数,整个进程的信号处理函数也就随之改变了。
18.int sigwait(const sigset_t *set, int *sig)
线程通过调用该函数等待一个或多个信号发生。声明在pthread.h文件中。函数的返回值存放在sig指向的变量中,表明信号发送的数量。如果信号集set中的某个信号在sigwait调用的时候处于未决状态,那么sigwait函数将无阻塞的返回,返回前sigwait将从进程中移除那些处于未决状态的信号。线程在调用sigwait函数之时,必须阻塞它所等待的信号,从而避免错误发生(在函数返回之前的时间内,又会有新的等待信号产生)。sigwait函数在返回前会取消信号集的阻塞状态(即恢复线程的屏蔽字)。
使用该函数使得线程简化了信号处理,将异步产生的信号用同步的方式来处理,说白了就是当信号产生时,不去执行进程的信号处理函数,而是被等待信号的线程所拦截(通过sigwait函数)而从sigwait函数返回,这样就相当于将等待信号的线程环境(线程中sigwait以后的代码)作为了信号处理程序,而不是进程的信号处理函数。当有多个线程调用sigwait函数来等待同一个信号,信号发生时只会有一个线程从sigwait返回。这块讨论的信号到底是由线程的sigwait捕获还是进程的信号处理函数捕获,是由操作系统实现的,操作系统要么实现前者,要么实现后者,而不会二者皆可。
19.int pthread_kill(pthread_t thread, int sig)
线程通过调用该函数向其他线程发送信号,thread参数是其他线程的线程id,sig是信号名。声明在pthread.h文件中。当传递sig=0的参数到函数中时,可以用来检查thread线程是否存在。另外,如果信号的默认处理动作是终止进程,那么把信号传递给某个线程也仍然会杀死整个进程。
20.int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void))
该函数用于清除子进程中的锁状态。声明在pthread.h文件中。当在一个线程中使用fork创建子进程时,子进程通过继承父进程的整个地址空间,从而也继承了父进程的所有互斥量,读写锁和条件变量状态。但是,在子进程中只存在一个线程,那就是父进程中调用fork的那个线程的副本。由于子进程不包含其他线程副本,于是子进程就无法感知到其他线程中的锁,因此需要借助该函数来清理锁状态。如果子进程创建好后,立即调用了exec函数,就不需要清理那些锁了,因为整个旧的地址空间会被抛弃。
该函数的作用是来帮助fork后的子进程把其所有继承而来的锁打开。该函数的原理:该函数在fork函数之前进行调用,来注册三个处理函数prepare,parent和child。注册成功后,prepare函数会在fork函数创建子进程之前调用,作用是获取父进程定义的所有锁;parent会在fork创建完子进程,将要返回父进程环境时调用,作用是打开父进程的所有锁;child函数会在fork创建完子进程,将要返回子进程环境时调用,作用是打开子进程的所有锁。至此,子进程中所有继承而来的锁全部被打开了。注意:如果不想用哪个处理函数,注册时该函数的参数传递为NULL。该函数可以在fork之前多次调用,或者在多个模块中调用,详细例子参考《apue》。
第14章 高级I/O
.......
........
从I/O多路转接开始。。。。。
1.int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
该函数具有进行同步I/O多路转接功能(确定多个文件描述符fd的状态)。声明在sys/select.h文件中。nfns参数告诉内核需要确定的描述符范围为0~nfds-1,内核将来只会返回该范围的文件描述符状态。readfds,writefds,excpetfds三个参数用来设置需要了解其状态的描述符,同时用来返回已准备好的可读,可写,有异常消息的描述符。timeout参数用来设置select的阻塞时间。
先来说明最后一个参数,它指定了select函数愿意等待的时间,struct timeval结构如下:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
当timeout == NULL时,select函数永远等待。当所指定的文件描述符之一准备好时或者函数捕捉到一个信号时,等待结束。如果捕捉到一个信号函数返回-1并设置errno值。
当timeout->tv_sec == 0 && timeout->tv_usec == 0时,select函数完全不阻塞。测试完所指定的描述符后立即返回。这种方式适合用轮询来获得描述符状态。
当timeout->tv_sec != 0 || timeout->tv_usec != 0时,select函数等待所指定的秒数或微秒数。当指定的描述符之一准备好时或者指定的时间已超过时立即返回。
中间的三个参数readfds,writefds,excpetfds用下面的函数来设置。(这三个参数类型是fd_set,也就是描述符集,每个描述符在其中占据一位,下面的函数使用描述符fd来指定是哪一位)
void FD_CLR(int fd, fd_set *set)
int FD_ISSET(int fd, fd_set *set)
void FD_SET(int fd, fd_set *set)
void FD_ZERO(fd_set *set)
第一个函数将fd指定的一位清除。第二个函数测试一个指定位是否设置。第三个函数用来设置一个指定位。最后一个函数将fd_set变量的所有位设置为0。一般在定一个了一个描述符集变量后,必须先用FD_ZERO清除其所有位,然后再设置我们关心的各个位。
select函数的中间三个参数readfds,writefds,excpetfds中的任意一个或者全部都可以为空指针,这表示对相应状态并不关心。如果这三个参数都是空指针,select函数就相当于sleep函数,不过睡眠的时间更加精确。select有三个可能的返回值:
a.返回值为-1表示出错。比如所有指定的描述符都没有准备好时(select正阻塞着),进程接收到了一个其它的信号。
b.返回值为0表示没有描述符准备好。这种情况出现在,要么select不阻塞立即返回,要么阻塞的时间已到,描述符都没有准备好。这是readfds,writefds,excpetfds三个描述符集的所有位均为0。
c.返回值为正值表示已经有描述符准备好了。如果有同一个描述符的读和写都准备好了,返回值为2。这时,readfds,writefds,excpetfds三个描述符集中打开的位(为1)对应于准备好的描述符。
另外,对于普通文件描述符,获取其读,写,异常状态时总是返回准备好了。如果在一个描述符上碰到了文件结尾处,select指示该描述符是可读状态,而不指示其为异常状态。
2.int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask)
该函数域select函数类似。声明在sys/select.h文件中。有以下区别:
a.select的超时值用timeval结构指定,但pselect使用timespec结构指定。该结构如下所示:
struct timespec {
long tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
b.pselect的超时值被设置为const,这保证了函数调用期间不会改变该该结构值。
c.pselect函数可以设置信号屏蔽字,如果sigmask为null,那么在信号有关方面该函数和select运行状况相同。该函数以原子操作安装信号屏蔽字,在函数返回时恢复屏蔽字。
3.int poll(struct pollfd *fds, nfds_t nfds, int timeout)
该函数和select类似,不过接口不同。声明在poll.h文件中。该函数使用了一个结构体数组fds,为每个要获取其状态的描述符分配了一个结构体(结构体中包含了描述符的各种状态),而不是为每个状态构造一个描述符集。stuct pollfd结构体如下:
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
结构体中的events成员用来告诉内核我们关心该描述符的什么状态。函数返回时,内核设置revents成员,以说明对于该描述符已经发生了什么事件。events和revents取值如下:
events:
POLLIN 不阻塞地可读除高优先级外的数据(等效于POLLRDNORM | POLLRDBAND)
POLLRDNORM 不阻塞地可读普通数据(优先级波段为0)
POLLRDBAND 不阻塞地可读非0优先级波段数据
POLLPRI 不阻塞地可读高优先级数据
前四行用来测试可读性
POLLOUT 不阻塞地可写普通数据
POLLWRNORM 与POLLOUT 相同
POLLWRBAND 不阻塞地可写非0优先级波段数据
这三行用来测试可写性
revents:
POLLERR 已出错
POLLHUP 已挂断
POLLNVAL 描述符不引用一打开文件
当描述符被挂断后,就不能再写向该描述符,但仍可能从该描述符中读到数据。应该区分文件结束和挂断之间的区别:如果正从终端输入数据,并键入了文件结束字符,POLLIN被打开,于是就可读文件结束标志(read返回0)。如果测试的正在读调制解调器,并且电话线已挂断,则在revents中将街道POLLHUP通知(之前POLLHUP在revents中没有打开)。
4.ssize_t readv(int fd, const struct iovec *iov, int iovcnt)
ssize_t writev(int fd, const struct iovec *iov, int iovcnt)
这两个函数在一次函数调用中读,写多个非连续缓冲区。声明在sys/uio.h文件中。第一个函数将fd文件中的数据读到iov数组中,第二个函数将iov数组中的数据聚集起来写入fd文件中。struct iovec结构如下:
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
iov_base成员指向缓冲区的首地址,iov_len成员代表该缓冲区大小。writev以顺序iov[0],iov[1]至iov[iovcnt-1]从缓冲区中聚集输出数据。函数返回输出的字节总数,通常,它应该等于所有缓冲区长度之和。readv将读到的数据按照上述的顺序散步到缓冲区中。readv函数总是填满一个缓冲区,再填写下一个缓冲区。readv返回读到的字节数,遇到文件结尾返回0。
5.void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)
该函数将一个给定文件映射到一个存储区域中。声明在sys/mman.h文件中。addr用于指定映射存储区的起始地址,通常将其设置为0,表示由系统选择该映射区的起始地址(系统一般选择堆区和栈区中间的共享区)。该函数返回值为映射区的起始地址。fd参数指定要被映射文件的描述符,在映射之前,先要打开该文件。length参数指示映射区的长度(字节为单位)。offset参数指示要映射字节在文件中的起始偏移量。prot参数说明对映射存储区的保护要求:
PROT_READ 映射区可读
PROT_WRITE 映射区可写
PROT_EXEC 映射区可执行
PROT_NONE 映射区不可访问
对映射存储区的保护要求不能超过文件open模式访问权限。例如,若该文件是只读打开的,那么对映射存储区就不能指定PROT_WRITE。
flags参数影响映射存储区的多种属性:
MAP_FIXED 返回值必须等于addr。不推荐使用该标志,因为不利于可移植性。如果未指定此标志,而且addr非0,则内核只把addr视为一种建议值,并不保证会采用
该地址。将addr指定为0可获得最大可移植性。
MAP_SHARED 该标志说明本进程对该映射区做的修改对其他进程是可见的。说白了,就是本进程对该映射区做的修改都会真正修改磁盘上的该文件。这样,其它使用该
标志映射该文件的进程就会和本进程共享该文件,对该区域的修改对于这些进程都是可见的,最终也会修改磁盘上的文件。
MAP_PRIVATE 使用了该标志,只是将文件的一个副本映射到存储区中,进程对存储区的修改,只是在副本上进行修改,不会影响到磁盘上的文件,当进程终止后,磁盘
上的该文件没有变化。当然,对映射区的修改对于多个进程都是不可见的。
还有许多MAP_类标志,详情参考man手册。
需要注意的是,一般映射区的长度会是内存页长的整数倍,那么需要映射的文件如果比内存的页长小,那么系统会把映射区内除了文件以外的多余部分设置为0,对这部分的操作不会影响到文件。例如,文件长度为12字节,系统页长为512字节,因此多出的500字节系统会设置为0,并且操作这500字节对文件无影响(如果想让文件映射满一页,可以先对打开的文件进行lseek操作,将文件指针移动到距离文件开头一页大小的位置,再随意向文件中写一个字符即可,实际上该操作将文件变大了再映射)。此外,与映射区相关信号有两个:SIGSEGV和SIGBUS。第一个信号通常用于指示进程试图访问对它不可用的存储区,如果进程企图存数据到mmap指定为只读的映射区,那么也会产生该信号。如果访问映射区的一部分,该部分已经不存在了,则会产生第二个信号。例如进程映射了一个文件后,另外一个进程又将该文件截短了,那么该进程如果访问被截去的部分时就会接收到第二个信号。最后,用fork产生子进程后,子进程会继承存储映射区(因为存储映射区也是父进程地址空间的组成部分)。子进程调用exec执行新程序后,不再继承存储映射区。
6.int mprotect(void *addr, size_t len, int prot)
该函数用来更改一个已映射存储区的权限。声明在sys/mman.h文件中。prot许可值与mmap函数的该参数一样。addr地址必须是系统页边界对齐。
7.int msync(void *addr, size_t length, int flags)
如果修改了共享映射存储区中的页,就可以使用该函数将页冲洗到被映射的文件中。该函数类似于fsync,但作用于映射存储区。如果映射的存储区是私有的,就不会修改被映射的文件。addr地址也必须和页边界对其。flags参数用来控制冲洗的方式,必须要指定MS_ASYNC和MS_SYNC二者中的一个。MS_ASYNC表示不等待冲洗完成,MS_SYNC表示等待冲洗完成后函数才返回(冲洗完成之前进程一直阻塞)。MS_INVALIDATE标志是可选标志,用来丢弃与磁盘没有同步的任何页(会将函数参数表示的地址范围的所有页丢弃),这一般不是期望的操作。声明在sys/mman.h文件中。
8.int munmap(void *addr, size_t length)
当进程终止时,或者调用了该函数后,映射的存储区会被自动解除。关闭文件描述符不会解除映射。声明在sys/mman.h文件中。该函数调用时,不会将映射区的内容写到磁盘文件中。对于MAP_SHARED标志后的映射区,内容同步的工作由内核虚存算法自动进程,和该函数是没有关系的。对于MAP_PRIVATE标志的映射区,调用该函数解除后将被丢弃。
第15章 进程间通信
1.int pipe(int pipefd[2])
该函数用来创建管道。声明在unistd.h文件中。pipefd数组将返回两个文件描述符:pipefd[0]为读而打开,pipefd[1]为写而打开。向pipefd[1]描述符中写入的数据,可以从pipefd[0]描述符中读出。一般不在同一个进程中使用管道,因为没有意义,而是在父进程和子进程之间使用管道。当用fork创建子进程后,子进程会继承父进程已打开的文件描述符,因此也就继承了管道。这时候父进程和子进程中总共有四个管道端口,它们之间都是通的,也就是说,向父进程的pipefd[1]写入的数据既可以从父进程的pipefd[0]中读出也可以从子进程的pipefd[0]中读出;同理,向子进程的pipefd[1]写入的数据既可以从子进程的pipefd[0]中读出也可以从父进程的pipefd[0]中读出。但是管道中的数据被读一次后,就会被清除,不会保留。向管道中连续写入的数据,第二次的数据回追加到第一次数据后面,而不会覆盖第一次的数据。多个进程对同一个管道进行写操作时,如果写入的数据长度大于管道的尺寸PIPE_BUF时,可入的数据可能会穿插,如果写入的数据小于PIPE_BUF,则没有穿插问题(可用pathconf/fpathconf函数获取PIPE_BUF值的大小)。一般在父子进程间使用管道,会关闭父进程的读(或写)端描述符,同时关闭子进程的写(或读)端描述符,使父子进程半双工通信。当子进程执行exec后,文件描述符会丢失,因此一般将管道端口描述符先重定向到标准输入输出描述符中,再使子进程执行exec,这样在子进程中就可继续使用管道。当管道的一端被关闭后,下面两个规则起作用:
a.当读一个写端已被关闭的管道时,在所有数据被读取后,read返回0,表示到达了文件末尾。
b.如果写一个读端已被关闭的管道时,会产生SIGPIPE信号。如果忽略该信号或者捕捉该信号并从信号处理函数返回后,write函数返回-1,errno设置为EPIPE。
最后,a.历史上的管道是半双工的,b.管道只能在具有公共祖先的进程之间使用。
2.FILE *popen(const char *command, const char *type)
int pclose(FILE *stream)
第一个函数用来创建一个子进程,同时,创建父子进程之间的管道(这和fork/system是不同的),返回管道的一个端口至父进程。该子进程执行shell程序,然后再创建子进程执行commad命令(在这点上类似于system函数)。type参数可以是“r”或者“w”,如果是r,管道的写端将连接到commad进程的标准输出,此时函数返回管道的读端;如果w,管道的读端将连接到commad进程的标准输入,此时函数返回管道的写端。
pclose函数用来关闭标准I/O流,等待命令执行结束,然后返回shell的终止状态。函数的返回值类似于system函数的返回值。由于pclose中会调用waitpid等待它所创建的所有子进程,因此如果在程序中,用户自行设置了SIGCHILD的处理函数,并且函数中调用了wait等函数,则可能会影响pclose中的waitpid函数。他们均声明在stdio.h文件中。
3.协同进程(coprocess):当一个进程产生某个过滤进程的输入,同时又读取该过滤进程的输出,则称该过滤进程为协同进程。实际上协同进程就是帮其它进程处理数据,当然要从其它进程中获取要处理的数据,然后再将处理以后的数据返回给其它进程。
4.int mkfifo(const char *pathname, mode_t mode)
该函数用来创建一个FIFO文件。声明在sys/stat.h文件中。创建一个FIFO类似与创建一个文件,因此该函数类似于open/creat函数。mode参数取值和open/creat的mode参数相同。创建FIFO文件的用户和组权限规则同创建普通文件是相同的。创建好的FIFO文件可以使用一般的I/O函数(open,close,read,write,unlink等)来操作它。
当打开一个FIFO时:
a.如果没有指定O_NONBLOCK,那么只读open要阻塞到某个其它进程为写而打开它;类似的,只写open要阻塞到某个其它进程为读而打开它。
b.如果指定了O_NONBLOCK,只读open函数会立即返回(如果没有其它进程写FIFO);只写open将返回-1,并设置errno为ENXIO(如果没有其它进程为读而打开FIFO)。
和管道类似,若用write写一个尚无进程为读而打开的FIFO,则产生信号SIGPIPE;若某个FIFO的最后一个写进程关闭了该FIFO,则将为该FIFO的读进程产生一个文件结束标志(如果以读写打开FIFO,最后一个写进程关闭FIFO时,不会产生文件结束标志)。如果有多个进程写同一个FIFO,如果不希望数据穿插,则需要考虑原子操作。和管道类似,常量PIPE_BUF说明了可被原子写入FIFO的最大数据量。
FIFO有两种用途:
a.FIFO由shell命令使用以便将数据从一条管道线传送到另一条,为此无需创建中间临时文件。
b.FIFO用于客户进程---服务器进程应用程序中,以便在客户进程和服务器进程之间传递数据。
5.key_t ftok(const char *pathname, int proj_id)
该函数用来为XSI IPC创建一个键(key)。声明在sys/msg.h文件中。所谓XSI IPC,指的是消息队列,信号量,共享存储这三种进程间通信方式。不包括前边提到的管道和FIFO。XSI IPC创建的IPC结构,不会因为进程的终止而消失,不像管道和FIFO,进程终止后管道就不存在了,FIFO虽然存在,但其数据已被清空。使用该函数创建键时,pathname参数所指定的文件必须存在,该函数将使用指定文件的stat结构中st_dev和st_ino字段和proj_id参数的后8位组合起来生成键。生成的键可供下边的函数来创建XSI IPC结构。创建消息队列,信号量,共享存储所用的函数分别为msgget,semget,shmget,这三个函数都有两个类似的参数:一个key(就是由ftok生成的键)和一个整型msgflag。可以用这三个函数创建新的IPC结构或者打开已存在的IPC结构。如若要创建新结构,需满足下面两个条件之一:
a.key值为IPC_PRIVATE
b.key当前未与特定类型的IPC结构相结合,并且msgflag终止定了IPC_CREAT位
如若需要打开已存在的IPC结构,key值必须等于创建IPC结构时所指定的键,并且msgflag中不应指定IPC_CREAT。为了打开已存在的IPC结构,key值决不能为IPC_PRIVATE,该值比较特殊,它总是用来创建一个新IPC结构。如果需要访问用IPC_PRIVATE键创建的IPC结构,一定要知道该结构的标识符,然后用其他IPC调用(msgsnd和msgrcv)使用该标识符,而不能用msgget,semget,shmget三个函数来打开。如果需要创建一个新的IPC结构,而且要确保不是引用具有同一标识符的一个已存在IPC结构,需要在msgflag中同时指定IPC_CREAT和IPC_EXCL位,这样的话,如果IPC结构已经存在就会造成出错,返回EEXIST(这与指定了O_CREAT和O_EXCL标志的open函数类似)。消息队列,信号量,共享存储三者有各自的IPC结构,后边会列出。这些IPC结构中包含了一个相同结构struct ipc_perm结构,装有该IPC结构的权限和所有者。如下所示:
struct ipc_perm
{
__key_t __key; /* Key. */
__uid_t uid; /* Owner's user ID. */
__gid_t gid; /* Owner's group ID. */
__uid_t cuid; /* Creator's user ID. */
__gid_t cgid; /* Creator's group ID. */
unsigned short int mode; /* Read/write permission. */
............
...........
};
uid,gid,cuid,cgid分别指定了拥有者ID,拥有者组ID,创建者ID,创建者组ID。mode字段指定了访问者权限,如下所示:(IPC结构不需要执行,因此没有执行权限)
用户读 0400
用户写(更改) 0200
组读 0040
组写(更改) 0020
其他读 0004
其他写(更改) 0002
6.int msgget(key_t key, int msgflg)
该函数用来创建一个新的消息队列或者打开一个现存消息队列,返回消息队列标识符。后面对消息队列进行操作的函数可以使用该标识符。该队列ID可以声明在sys/msg.h文件中。该函数会创建如下所示的结构体,并初始化一部分成员:msg_perm中的各个值;msg_qnum,msg_lspid,msg_lrpid,msg_stime,msg_ctime都设置为0;msg_ctime设置为当前时间;msg_qbytes设置为系统限制值。
struct msqid_ds
{
struct ipc_perm msg_perm; /* structure describing operation permission */
__time_t msg_stime; /* time of last msgsnd command */
.......
__time_t msg_rtime;
.......
__time_t msg_ctime;
.......
__syscall_ulong_t __msg_cbytes;
msgqnum_t msg_qnum; /* number of messages currently on queue */
msglen_t msg_qbytes; /* max number of bytes allowed on queue */
__pid_t msg_lspid; /* pid of last msgsnd() */
__pid_t msg_lrpid; /* pid of last msgrcv() */
.......
.......
};
函数执行成功,返回一个非负的队列ID。该队列可被用于其他三个消息队列函数。
7.int msgctl(int msqid, int cmd, struct msqid_ds *buf)
该函数可以对消息队列进行各种控制操作。声明在sys/msg.h文件中。该函数是XSI IPC中类似于ioctl的函数(垃圾桶函数)。msqid是消息队列ID(就是msgget函数返回的队列描述符),cmd参数指定了队列要执行的命令:
a.IPC_STAT 取此队列的msqid_ds结构,并将它存放在buf指向的结构中
b.IPC_SET 按由buf指向结构中的值,设置与此队列相关结构中的下列四个字段:msg_perm.uid,msg_perm.gid,msg_perm.mode和msg_qbytes。此命令只能由下 面两种进程执行:一种是其有效用户ID等于msg_perm.cuid或msg_perm.uid,另一种是具有超级用户权限的进程。此外,只有超级用户才能增加 msg_qbytes的值。
c.IPC_RMID 从系统中删除该消息队列以及仍在该对列中的所有数据。这种删除立即生效。仍在使用这一消息队列的其他进程在它们下一次试图对此队列进行操作时, 将出错返回EIDRM。此命令只能由下列两种进程执行:一种是其有效用户ID等于msg_perm.cuid或msg_perm.uid,另一种是具有超级用户权限的进程。
8.int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)
调用该函数将数据放到消息队列中。声明在sys/msg.h文件中。每个消息都有三部分组成,它们是:正长整型类型字段,非负长度(msgsz)以及实际数据字节(对应于长度)。消息总是放在队列尾端。msgp参数指向一个长整型数,它包含了症的整型消息类型,紧跟其后的是消息数据。(若msgsz是0,则无消息数据)若发送的最长消息是512字节,则可定义下列结构:
struct mymesg {
long mtype;
char mtext[512];
};
然后,ptr就是一个指向该结构的指针。接收者可以使用消息类型以非先进先出的次序取消息。参数msgflg的值可以指定为IPC_NOWAIT,这类似于文件I/O的非阻塞标志。若消息队列已满(或者是队列中的消息总数等于系统限制值,或对列中的字节总数等于系统限制值),则指定IPC_NOWAIT的话,msgsnd立即出错并返回EAGAIN;没有指定IPC_NOWAIT的话,进程将阻塞到下述情况出现为止:有空间可以容纳要发送的消息;从系统中删除了此队列,或捕捉到一个信号,并从信号处理程序返回。第二种情况下,返回EIDRM(“标识符被删除”)。第三种情况则返回EINTR。
需要注意的是,对删除消息队列的处理不是很完善,因为对每个消息队列并没有设置一个引用计数器(对打开文件则有这种计数器),所以删除一个队列会造成仍在使用这一队列的进程在下次对队列进行操作时出错返回。信号量机制也是如此。而文件删除就比较完善,当使用某个文件的最后一个进程关闭了它的文件描述符后,才能删除文件内容。
当msgsnd成功返回后,与消息队列相关的struct msqid_ds结构将得到更新,以标明发出该消息的进程ID(msg_lspid),进行该调用的时间(msg_stime),并指示队列中增加了一条消息(msg_qnum增加1)。
9.ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg)
该函数从队列中取出消息。声明在sys/msg.h文件中。该函数的msqid参数和msgp参数和9中的函数类似。msgsz参数表示要接收的长度。如果返回的消息长度大于msgsz,并且msgflg中设置了MSG_NOERROR,则接收到的消息会被截短。(这种情况下,不会通知我们消息被截短了,消息截去的那一部分被丢弃)如果没有设置MSG_NOERROR标志,而消息有太长,则函数出错返回E2BIG(消息仍留在队列中)。type参数用于指定想要取哪一种消息:
type == 0 返回队列中的第一个消息
type > 0 返回队列中消息类型为type的第一个消息
type < 0 返回队列中消息类型值小于或等于|type|的消息,如果这种消息有若干个,则取类型值最小的消息。
type值非0用于以非先进先出次序读消息。例如,若应用程序对消息赋优先权,那么type就可以是优先权值。如果一个消息队列由若干个客户进程和一个服务器进程使用,那么type字段可以用来包含客户进程的进程ID(只要进程ID可以存放在长整型中)。
可以指定flag值为IPC_NOWAIT,使操作不阻塞。这使得如果没有指定类型的消息,则msgrcv函数返回-1,errno设置为ENOMSG;如果没有指定IPC_NOWAIT,则进程阻塞到如下情况出现才终止:有了指定类型的消息;从系统中删除了此队列(出错则返回-1且errno置为EIDRM);或捕捉到一个信号并从信号处理程序返回(msgrcv函数返回-1,errno设置为EINTR)。
msgrcv函数成功执行时,内核更新与该消息队列相关联的struct msqid_ds结构,已指示调用者的进程ID(msg_lrpid)和调用时间(msg_rtime),并将队列中的消息数减1(msg_qnum)。
注意:当初实施消息队列时,是因为其他形式IPC都是半双工管道。而现在有了全双工管道。因此,在新的应用程序中不应该再使用消息队列。
10.
11
12
13.int shmget(key_t key, size_t size, int shmflg)
该函数用来创建或者打开一个共享存储区,返回存储区标识符。声明在sys/shm.h文件中。后边对共享区操作的函数就可以使用该标识符。该函数的创建或者打开规则和msgget函数相同。当创建一个新段时,会创建一个相关的结构体struct shmid_ds,并初始化下列成员:shm_perm中的各个值;shm_lpid,shm_nattch,shm_atime,shm_dtime成员都设置为0;shm_ctime成员设置为当前时间;shm_segsz设置为请求长度size。
struct shmid_ds
{
struct ipc_perm shm_perm; /* operation permission struct */
size_t shm_segsz; /* size of segment in bytes */
__time_t shm_atime; /* time of last shmat() */
........
__time_t shm_dtime; /* time of last shmdt() */
........
__time_t shm_ctime; /* time of last change by shmctl() */
........
__pid_t shm_cpid; /* pid of creator */
__pid_t shm_lpid; /* pid of last shmop */
shmatt_t shm_nattch; /* number of current attaches */
.........
};
size参数用来指定该共享存储段的长度,实现通常将其向上取为系统页长的整数倍。但是,如果应用指定的size值并非系统页长的整数倍,则最后一页的余下部分不可使用。如果正在创建一个新段,那么必须指定size值;如果正在引用一个现存段,则将size指定为0。当创建一个新段时,段内的内容初始化为0。
14.int shmctl(int shmid, int cmd, struct shmid_ds *buf)
该函数用来对指定的共享存储段进行控制。声明在sys/shm.h文件中。cmd指定了下面5中命令中的一种:
IPC_STAT 取此段的shmid_ds结构,并将它存放在由buf指向的结构中。
IPC_SET 按buf指向结构中的值设置与此段相关结构中的下列三个字段:shm_perm.uid,shm_perm.gid和shm_perm.mode。此命令只能由下 面两种进程执行:一种是其有效用户ID等于shm_perm.cuid或shm_perm.uid,另一种是具有超级用户权限的进程。
IPC_RMID 从系统中删除共享存储段,因为每个共享存储段有一个连接计数(shmid_ds结构中的shm_nattch字段),所以除非该段最后一个进程终止或与该段分离,否 则不会实际上删除该存储段。不管此段是否仍在使用,该段标识符立即被删除,所以不能再用shmat函数与该段连接。此命令只能由下 面两种进程执行:一种是其有效用户ID等于shm_perm.cuid或shm_perm.uid,另一种是具有超级用户权限的进程。
IPC_LOCK 将共享存储段锁定在内存中,此命令只能由超级用户执行。
IPC_UNLOCK 解锁共享存储段。此命令只能由超级用户执行。
15.void *shmat(int shmid, const void *shmaddr, int shmflg)
一旦创建了共享存储段,就可以使用该函数将其连接到进程的地址空间中。声明在sys/shm.h文件中。共享存储段连接到调用进程的哪个地址上与shmaddr参数以及在flag中是否指定SHM_RND位有关:
a.如果shmaddr为0,则此段连接到由内核选择的第一个可用地址上。推荐使用这种方式。(一般位于堆区和栈区中间的共享区)
b.如果shmaddr非0,并且没有指定SHM_RND,则此段连接到shmaddr所指定的地址上。
c.如果shmaddr非0,并且指定了SHM_RND,则此段连接到shmaddr mod ulus SHMLBA所表示的地址上。
如果在shmflag中指定了SHM_RDONLY位,则以只读方式连接此段,否则以读写方式连接此段。shmat函数的返回值是该段所连接的实际地址,如果函数出错返回-1。如果执行成功那么内核将使该共享存储段shmid_ds结构中的shm_nattch计数器值加1。
16.int shmdt(const void *shmaddr)
当今成使用完存储共享区后,调用该函数是共享区与进程分离。声明在sys/shm.h文件中。该操作并不从系统中删除共享区标识符及其数据结构。该标志符让然存在,直至某个进程调用shmctl(带命令IPC_RMID)删除它。shmaddr参数是共享区在进程中的起始地址。如果函数执行成功,shmid_ds结构中的shm_nattch计数器值减1。
小结:掌握管道和FIFO,因为在大量应用程序中仍可有效地使用这两种基本技术。在新的应用程序中,尽量避免使用消息队列以及信号量,而用全双工管道和记录锁取代。共享存储段有其应用场合,而mmap函数也能提供同样的功能。
第16章 网络IPC:套接字
1.int socket(int domain, int type, int protocol)
该函数用来创建一个套接字。声明在sys/socket.h文件中。参数domain用来指定套接字使用的地址族,取值如下:
AF_INET ipv4因特网域
AF_INET6 ipv6因特网域
AF_UNIX unix域
AF_UNSPEC 未指定
参数type指定套接字的类型,取值如下:
SOCK_DGRAM 长度固定的,无连接的不可靠报文传递(UDP)
SOCK_RAW IP协议的数据包接口
SOCK_SEQPACKET 长度固定的,有序,可靠的面向连接报文传递
SOCK_STREAM 有序,可靠,双向的面向连接字节流(TCP)
参数protocol通常为零,表示按给定的域和套接字类型选择默认的协议。当对同一域和套接字类型支持多个协议时,可以使用protocol参数选择一个特定协议。在AF_INET通信域中套接字类型SOCK_STREAM的默认协议是TCP,在AF_INET通信域中套接字类型SOCK_DGRAM的默认协议是UDP。
socket函数创建的套接字描述符可用普通文件操作函数来操作,但并适用所有的文件操作函数。
2.int shutdown(int sockfd, int how)
该函数用来关闭套接字的输入/输出端口。声明在sys/socket.h文件中。如果参数how是SHUT_RD(关闭读端),那么无法从套接字读取数据(但仍可以读);如果参数how是SHUT_WR(关闭写端),那么无法使用套接字发送数据(但仍可以写);使用SHUT_RDWR则将无法同时读取和发送数据。需要注意的是,close函数虽然可以关闭套接字,但是如果使用dup2复制了套接字描述符,那么直到最后一个套接字描述符被关闭后,套接字才会被释放;而shutdown函数是一个套接字处于不活动状态,无论它的描述符有多少。
3.uint32_t htonl(uint32_t hostlong)
uint16_t htons(uint16_t hostshort)
uint32_t ntohl(uint32_t netlong)
uint16_t ntohs(uint16_t netshort)
这四个函数用来完成本地字节序和网络字节序之间的转换。声明在arpa/inet.h文件中。n代表network,h代表host,l代表long,s代表short。奔腾处理器上的Linux系统采用小端字节序。
4.地址格式
为了是不同格式地址能够被传入到套接字函数,地址会被强制转换成通用的地址结构struct sockaddr。
Linux对该通用地址结构的实现如下(/usr/include/i386/bits/socket.h):
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */ /* #define __SOCKADDR_COMMON(sa_) sa_family_t sa_prefix##family */
char sa_data[14]; /* Address data. */
};
Linux中IPV4的套接字地址sockaddr_in格式如下:
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */ /* typedef uint16_t in_port_t; */
struct in_addr sin_addr; /* Internet address. */ /* typedef uint32_t in_addr_t; */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
struct in_addr
{
in_addr_t s_addr;
};
Linux中IPV6的套接字地址sockaddr_in6格式如下:
struct sockaddr_in6
{
__SOCKADDR_COMMON (sin6_);
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};
struct in6_addr
{
union
{
uint8_t __u6_addr8[16];
#if defined __USE_MISC || defined __USE_GNU
uint16_t __u6_addr16[8];
uint32_t __u6_addr32[4];
#endif
} __in6_u;
#define s6_addr __in6_u.__u6_addr8
#if defined __USE_MISC || defined __USE_GNU
# define s6_addr16 __in6_u.__u6_addr16
# define s6_addr32 __in6_u.__u6_addr32
#endif
};
在使用时,无论是ipv4还是ipv6,均要被强制转换成struct sockaddr_in结构传入到套接字例程中。
5.in_addr_t inet_addr(const char *cp)
char *inet_ntoa(struct in_addr in)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size)
int inet_pton(int af, const char *src, void *dst)
这些函数用于二进制地址格式和点分十进制字符串格式之间的相互转换。它们均声明在arpa/inet.h文件中。前两个函数只能用于IPV4地址。后两个新函数支持IPV4和IPV6地址。第一和第三个函数将点分十进制字符串转换成二进制地址,第二和第四个函数刚好相反。后两个函数的af参数取值为:AF_INET和AF_INET6,分别代表ipv4域和ipv6域。第三个函数的size参数指定了保存文本字符串缓冲区(dst)的大小,有两个常数用于简化工作:INET_ADDRSTRLEN和INET6_ADDRSTRLEN,前者可放ipv4地址,后者可放ipv6地址。对于第四个函数,dst参数指向的地址空间必须足够大,以便能够存入32位地址(当af指定为AF_INET)和128位地址(当af指定为AF_INET6)。
6.struct hostent *gethostent(void)
void sethostent(int stayopen)
void endhostent(void)
调用这些函数可以获取计算机的主机信息。均声明在netdb.h文件中。第一个函数会打开数据库文件(如果该文件没有打开),如果文件是打开的,则该函数返回文件的下一条目;第二个函数也可以打开数据库文件,如果文件是打开的,则将其回绕。第三个函数关闭数据库文件。第一个函数返回的struct hostent结构如下:
struct hostent {
char *h_name; /* official name of host */
char **h_aliases; /* alias list */
int h_addrtype; /* host address type */
int h_length; /* length of address */
char **h_addr_list; /* list of addresses */
}
7.struct netent *getnetbyaddr(uint32_t net, int type)
struct netent *getnetbyname(const char *name)
struct netent *getnetent(void)
void setnetent(int stayopen)
void endnetent(void)
这几个函数用来获取网络名字和网络号。均声明在netdb.h文件中。将网络名字和网络号包含在struct netent结构体中,该结构体如下:
struct netent {
char *n_name; /* official network name */
char **n_aliases; /* alias list */
int n_addrtype; /* net address type */
uint32_t n_net; /* network number */ //返回的地址采用网络字节序。
}
8.struct protoent *getprotobyname(const char *name)
struct protoent *getprotobynumber(int proto)
struct protoent *getprotoent(void)
void setprotoent(int stayopen)
void endprotoent(void)
这几个函数用来获取协议名字和协议号。均声明在netdb.h文件中。将协议名字和协议号包含在struct protoent结构体中,该结构体如下:
struct protoent {
char *p_name; /* official protocol name */
char **p_aliases; /* alias list */
int p_proto; /* protocol number */
}
9.struct servent *getservbyname(const char *name, const char *proto)
struct servent *getservbyport(int port, const char *proto)
struct servent *getservent(void)
void setservent(int stayopen)
void endservent(void)
这几个函数用来获取服务名字和服务的端口号。均声明在netdb.h文件中。返回的struct servent结构如下:
struct servent {
char *s_name; /* official service name */
char **s_aliases; /* alias list */
int s_port; /* port number */
char *s_proto; /* protocol to use */
}
10.int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res)
const char *gai_strerror(int errcode)
void freeaddrinfo(struct addrinfo *res)
第一个函数通过主机名字或服务名字来获取主机的地址信息。声明在netdb.h文件中。主机可能有多个地址,因此返回一个节点类型为struct addrinfo的链表res。struct addrinfo结构如下:
struct addrinfo {
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
socklen_t ai_addrlen;
struct sockaddr *ai_addr;
char *ai_canonname;
struct addrinfo *ai_next;
};
hints参数是一个过滤地址的模板,用来选择特定的地址。它仅使用ai_family,ai_flags,ai_protocol和ai_socktype字段。剩余的整数字段必须为0,指针字段必须为空。ai_flags的使用的标志参考《apue》。如果该函数失败,不能使用perror或strerror函数来生成错误信息,而要使用第二个的函数。第二个函数用来释放这些链表节点。
11.int getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *serv, size_t servlen, int flags)
该函数将套接字地址转换成主机名或服务器名。声明在netdb.h文件中。如果host非空,它指向一个长度为hostlen字节的缓冲区用于存储返回的主机名;如果serv非空,它指向一个长度为servlen字节的缓冲区用于存储返回的服务名。flags参数指定一些转换的控制方式,取值参考《apue》。
12. int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
该函数将地址绑定到一个套接字上。声明在sys/socket.h文件中。参数addr有以下规则:
a.在进程所运行的机器上,指定的地址必须有效,不能指定一个其他机器上的地址。
b.地址必须和创建套接字时的地址族所支持的格式相匹配。
c.端口号必须不小于1024,除非该进程具有超级用户特权。
对于因特网域(AF_INET/AF_INET6),如果指定IP地址为INADDR_ANY,套接字端点可以被绑定到系统的所有网络接口上,这意味着可以收到这个系统所安装的所有网卡的数据。一般情况下,与客户端套接字关联的地址没有太大意义,可以让系统选择一个默认地址;服务器端需要绑定地址一个众所周知的地址,使得客户端可以进行连接。另外,如果没有使用bind绑定地址,那么当调用connect或listen时,系统会自动将一个地址绑定到套接字上。
13.int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
调用该函数来获得绑定到套接字上的一个地址。声明在sys/socket.h文件中。调用该函数时,addrlen参数指定了addr缓冲区的大小;函数返回时,该参数被设置成缓冲区实际被使用的大小。如果提供的缓冲区和地址的实际大小不匹配,则将其截断而不报错。如果套接字当前没有绑定地址,函数结果没有意义。
14.int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
如果套接字已经和对方连接,调用该函数可以获得对方的地址。声明在sys/socket.h文件中。该函数和13中的函数类似。
15.int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
该函数在sockfd套接字和addr指定的服务器之间建立一个连接。声明在sys/socket.h文件中。使用该函数建立连接时,可能会失败,因此应用程序必须能够处理该函数返回的错误,这些错误可能由一些瞬时变化条件引起(这在一些负载很重的服务器上很可能发生)。关于函数返回的错误值类型和原因,参考man手册。另外,对于UDP套接字调用该函数的话,所有发送报文的目标地址被设置为该函数中指定的地址,这样每次传送报文的时候就不许要再提供地址,此外,也仅能接收来自指定地址的报文。
16.int listen(int sockfd, int backlog)
服务器端调用该函数监听套接字,来宣告可以接受客户端的连接请求。声明在sys/socket.h文件中。backlog参数指定了该套接字能连接的最大数量。一旦超过了最大值,服务器会拒绝多余连接请求。
17.int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
一旦服务器调用了listen,就可调用该函数获得连接请求并建立连接。声明在sys/socket.h文件中。该函数返回一个套接字描述符,该套接字连接到了客户端。返回的套接字和原始套接字sockfd具有相同的套接字类型和地址族。而原始套接字sockfd并没有关联到客户端的套接字,而是继续保持可用状态并接受其他连接请求。如果不关心客户端的地址,则可将addr参数和addrlen置为null。否则,addr设为足够大的缓冲区来存放客户端的地址信息,len指定了该缓冲区的大小。返回时,函数会在缓冲区填充客户端地址信息并更新len的大小。
如果没有连接请求,accept函数会阻塞到直到一个请求到来。如果sockfd设置为非阻塞模式,accept函数会返回-1并将errno设置为EAGAIN或EWOULDBLOCK。此外,服务器端可以使用poll或者select函数来等待一个请求的到来,这时,一个请求套接字会以可读方式出现。
18.ssize_t send(int sockfd, const void *buf, size_t len, int flags)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen)
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags)
这三个函数用来发送数据。声明在sys/socket.h文件中。第一个函数的用法和write类似,用于TCP套接字发送数据(UDP套接字不能使用它来发送),flags标志取值如下:
MSG_DONTROUTE 勿将数据路由出本地网络
MSG_DONTWAIT 允许非阻塞操作(等价于使用了O_NONBLOCK)
MSG_EOR 如果协议支持,此为记录结束
MSG_OOB 如果协议支持,发送带外数据
如果send成功返回,并不代表连接的另一端接收到了数据,所保证的仅是当send成功返回时,数据已经无错误的发送到网络上。
UDP套接字可以使用第二个函数来发送数据,dest_addr参数指定了目标地址。TCP套接字如果使用第二个函数来发送数据的话,会忽略目标地址。
第三个函数用来发送由struct msghdr结构体指定的多重缓冲区的数据,该函数和writev函数类似。struct msghdr结构体如下:
struct msghdr {
void *msg_name; /* optional address */
socklen_t msg_namelen; /* size of address */
struct iovec *msg_iov; /* scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* ancillary data, see below */
size_t msg_controllen; /* ancillary data buffer len */
int msg_flags; /* flags on received message */
};
19.ssize_t recv(int sockfd, void *buf, size_t len, int flags)
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen)
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags)
这三个函数用来接收数据。声明在sys/socket.h文件中。第一个函数的用法和read函数类似,flags标志取值如下:
MSG_OOB 如果协议支持,接收带外数据
MSG_PEEK 返回报文内容而不真正取走报文
MSG_TRUNC 即使报文被截断,要求返回的是报文的实际长度
MSG_WAITALL 等待直到接收完len指定的数据长度(仅SOCK_STREAM)
当指定为MSG_PEEK标志时,可以查看下一个要读的数据但不会真正取走,当再次调用read或recv函数时会返回刚才查看到的数据。
对于SOCK_STREAM套接字(TCP套接字),默认情况下接收到数据可以比请求的少,但是如果设置了MSG_WAITALL标志,则直到接收完len指定的数据长度,recv函数才返回。对于SOCK_DRAM和SOCK_SEQPACKET套接字(它们是基于报文而不是字节流的),MSG_WAITALL标志没有改变什么行为,因为这些套接字本身就是一次取走整个报文。如果发送者调用shudown来结束传输,或者发送端已经关闭,那么当所有数据接收完毕后,recv返回0。
第二个函数可以获得发送者的套接字端点地址(src_addr和addrlen必须非空),该函数通常用于UDP套接字,也可以用于TCP套接字(这时等同于recv)。
第三个函数将接收到的数据送入多个缓冲区(类似于readv),也可以接收辅助数据。msg参数指定了用于接收数据的缓冲区。可以设置参数flags来改变recvmsg的默认行为,返回时,msghdr结构中的msg_flags字段被设置为所接收数据的各种特征(进入recvmsg时的msg_flags被忽略),这些返回的msg_flags标志如下:
MSG_CTRUNC 控制数据被截断
MSG_DONTWAIT recvmsg处于非阻塞模式
MSG_EOR 接收到记录结束符
MSG_OOB 接收到带外数据
MSG_TRUNC 一般数据被截断
20.int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen)
该函数用来设置套接字选项。声明在sys/socket.h文件中。套接字有三个层次的选项(XSI仅定义了前两个选项):
a.通用选项,工作在所有套接字类型上。
b.在套接字层次管理的选项,但是依赖于下层协议的支持。
c.特定于某协议的选项,为每个协议所独有。
参数level标识了选项的层次,如果是通用层次的选项,level设置成SOL_SOCKET;否则,level设置成套接字层次的选项,例如,对于TCP选项,level是IPPROTO_TCP,对于IP选项,level是IPPROTO_TCP。参数optname代表通用层次和套接字层次的选项,参考《apue》。参数optval根据选项的不同指向一个数据结构或者一个整数。一些选项是on/off开关,当valopt整数非0,选项被启用,当optval整数为0,选项被禁止。参数optlen指定了optval指向对象的大小。
21.int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen)
该函数用来获取套接字当前的选项。声明在sys/socket.h文件中。该函数调用时,optlen设置成optval缓冲区大小,函数返回时,optlen被更新为实际尺寸。如果实际尺寸大于optlen初始设置值,选项会被截断而不报错。
22.带外数据:带外数据又叫做紧急数据,TCP协议支持一个字节的带外数据,UDP不支持带外数据。在三个send函数中指定了标志MSG_OOB,就可以传输待外数据,传送的若干个字节中最后一个字节就是带外数据。此外,接收进程可以调用fcntl(sockfd,F_SETOWN,pid)函数来设置pid进程对套接字信号进行处理。当接收进程接收到带外数据时,pid指定的进程就会收到一个套接字信号。调用owner = fcntl(sockfd,F_GETOWN,0)可以获得处理套接字信号的进程ID。
23.int sockatmark(int sockfd)
TCP协议支持紧急标记(在普通数据流中紧急数据所处的位置),如果采用套接字选项SO_OOBINLINE,那么可以在普通数据中接收紧急数据。当系一个要读的字节是紧急数据的话,该函数返回1。
此外,当带外数据出现在套接字读取队列时,select函数会返回一个文件描述符并且拥有一个异常状态。可以在普通数据流上接受紧急数据,或者在某个recv函数中采用MSG_OOB标志在其他队列数据之前接收紧急数据。TCP队列仅有一字节的紧急数据,如果在接收当前的紧急数据之前又有新的紧急数据到来,那么当前的字节就成了普通字节(新的最后一个字节是紧急数据)。
24.套接字非阻塞和基于套接字的异步I/O
通常,recv函数没有数据可用时会阻塞等待。同样地,当套接字输出队列没有足够空间来发送消息send函数也会阻塞。在套接字非阻塞模式下,这些函数不会阻塞而是失败,设置errno为EWOULDBLOCK或者EAGAIN。非阻塞模式下,可以使用poll或select来判断套接字何时能接收或者传输数据。
在基于套接字的异步I/O中,当能够从套接字中读取数据,或者套接字写队列中的空间变得可用时,可以安排发送信号SIGIO。通过两个步骤来使用异步I/O:
a.建立套接字拥有者关系,信号可以被传送到合适的进程
b.设置套接字选项(使用fcntl),使得套接字可用时(可读或可写)发信号告知
有三种方式完成步骤a:
aa.在fcntl中使用F_SETOWN命令
bb.在ioctl中使用FIOSETOWN命令
cc.在ioctl中使用SIOCSPGRP命令
有两种方式完成步骤b:
aa.在fcntl中使用F_SETFL命令并且启用文件标志O_ASYNC。
bb.在ioctl中使用FIOASYNC。