UNIX环境高级编程(APUE)读书笔记
- 未完待续
第一章 基础
1.1 引言
- 所有操作系统都为它们所运行的程序提供服务。典型服务包括:执行新程序、打开文件、写入/读取文件、分配存储区以及获得当前时间等。
1.2 UNIX体系结构
- 内核:从严格意义上来说,可将操作系统定义为一种软件,它控制计算机硬件资源,提供程序运行环境。
- UNIX体系结构(从里到外):内核->系统调用->shell/公用函数库->应用程序
- 内核的接口称为系统调用(system call)
- shell是一个特殊的应用程序,为运行其他应用程序提供了一个接口。
- 从广义上说,操作系统包括了内核和一些其他软件,其他软件包括系统实用程序(system utility)、应用程序、shell以及公用函数库等。
1.3 登录
- 登录名
- 登录UNIX时,需要键入用户名及口令(密码)
- 在/etc/passwd文件中可查看登录名
- 文件中登录项由7个以冒号分隔的字段组成,依次是:登录名、加密口令、数字用户ID(205)、数字组ID(105)、注释字段、起始目录(/home/sar)以及shell(/bin/ksh)
- 例如,sar: x:205:105:Stephen Rago:/home/sar:/bin/ksh
- shell
- shell是一个命令行解释器,它读取用户输入,然后执行命令。它的用户输入通常来自于终端(交互式shell),有时来自于文件(shell脚本)
- 当用户登录时,某些系统启动一个视窗管理程序,但最终总会有一个shell程序运行在一个视窗中。
- LINUX常见shell解释器:
- C shell: /bin/csh
- Korn Shell: /bin/ksh
- TENEX C shell:/bin/tcsh
- 系统从口令文件(/etc/passwd)中相应用户登录项的最后一个字段中了解到应该为该登录用户执行哪一个shell
- 其余shell见书P3
- LINUX常见shell解释器:
1.4 文件和目录
- 文件系统
- UNIX文件系统时目录和文件的一种层次结构,所有的起点成为根(root),即“/”
- 目录(directory)是一个包含目录项的文件。
- 每个目录项都包含一个文件名,同时还包含说明该文件属性的信息。
- 文件属性是指文件类型(普通文件/目录等)、文件大小、文件所有者、文件权限(其他用户能否访问该文件)以及文件最后的修改时间等。
- 目录项的逻辑视图与实际存放在磁盘上的方式是不同的,UNIX文件系统的大多数实现并不在目录项中存放属性,因为当一个文件具有多个硬链接时,很难保持多个属性副本之间的同步。
- 文件名
- 目录中各个文件成为文件名(filename)
- 斜杠(/)和空字符不能用于文件名。
- 斜杠(/)用于分隔构成路径名的各文件名
- 空字符用于终止一个路径名
- 斜杠(/)和空字符不能用于文件名。
- POSIX.1推荐将文件名限制在以下字符集内
- 字母(az、AZ)
- 数字(0~9)
- 句点(.)
- 短横线(-)
- 下划线(_)
- 创建新目录时会自动创建两个文件名
- . :指向当前目录
- … :指向父目录
- 在最高层次的根目录中,.与…相同
- 目录中各个文件成为文件名(filename)
- 路径名
- 由斜杠(/)分隔的一个或多个文件名组成的序列构成路径名
- 斜杠开头的路径为绝对路径名
- 否则为相对路径名
- 指向相对于当前目录的文件
- 由斜杠(/)分隔的一个或多个文件名组成的序列构成路径名
- 工作目录
- 每个进程都有一个工作目录(working directory),或称为当前工作目录(current working directory)
- 起始目录
- 登录时,工作目录设置为起始目录(home directory),起始目录从口令文件中相应用户的登录项中获得。
1.5 输入和输出
- 文件描述符
- 文件描述符(file descriptor)通常是一个小的非负整数,内核用之标识一个特定进程正在访问的文件。
- 当内核打开一个现有文件或创建一个新文件时,都返回一个文件描述符
- 在读写文件时,使用
- 文件描述符(file descriptor)通常是一个小的非负整数,内核用之标识一个特定进程正在访问的文件。
- 标准输入、输出及标准错误
- 每当运行一个新程序时,所有shell都为其打开3个文件描述符
- 标准输入(standard input)
- 标准输出(standard output)
- 标准错误(standard error)
- shell中提供重定向方法
- 如 ls > file.list ,执行ls命令,其标准输出重新定向到名为file.list文件
- 每当运行一个新程序时,所有shell都为其打开3个文件描述符
- 不带缓冲的I/O
- open、read、write、lseek及close都提供了不带缓冲的I/O
- 标准I/O
- 标准I/O函数为不带缓冲I/O函数提供了一个带缓冲的接口
- 使用标准I/O无需担心如何选取最佳缓冲区大小
- 使用标准I/O简化了对输入行的处理
1.6 程序和进程
- 程序
- 程序(program)是一个存储在磁盘上某个目录中的可执行文件
- 内核使用exec函数(7个),将程序读入内存,并执行程序。
- 程序(program)是一个存储在磁盘上某个目录中的可执行文件
- 进程和进程ID
- 程序执行的实例被称为进程(process)
- 某些操作系统通用任务(task)表示正在被执行的程序。
- UNIX系统确保每一个进程都有一个唯一的数字标识符,称为进程ID(procee ID,PID)
- 进程ID是一个非负整数。
- 程序执行的实例被称为进程(process)
- 进程控制
- 主要函数fork、exec及waitpid
- exec有七种变体
- 具体的函数用法及意义后面章节记录
- 主要函数fork、exec及waitpid
- 线程和线程ID
- 通常,一个进程只有一个控制线程(thread)
- 一个进程内的所有线程共享同一地址空间、文件描述符、栈及与进程相关的属性。
- 由于他们能够访问同一存储区,各线程在访问共享数据时需要采取同步措施以避免不一致性
- 线程ID只在它所属的进程内起作用,在另一个进程中是没有意义的
1.7 出错处理
- 当UNIX系统函数出错时,通常会返回一个负值,且整形变量errno通常被设置为具有特定信息的值。
- <errno.h>中定义了errno及可以赋予它的各种常量
- 这些常量都已字符E开头
- POSIX和ISO C将errno定义为一个符号,它扩展称为一个可修改的整形左值(lvalue)。可以是一个包含出错编号的整数,也可以是一个返回出错编号指针的函数。
- 多线程环境中,共享进程地址空间,每个线程都有属于它自己的具备errno,以避免一个线程干扰另一个线程。
- 如Linux支持多线程存取errno
- 定义:
- extern int *__errno_location(void);
- #define errno (*__errno_location())
- 定义:
- 两条应该注意的规则
- 如果没有出错,其值不会被例程清楚
- 仅当函数的返回值出错时,才检验其值
- 任何函数都不会将errno值设置为0,而且在<errno.h>中定义的所有常量都不为0
- 如果没有出错,其值不会被例程清楚
- <errno.h>中定义了errno及可以赋予它的各种常量
- 出错恢复
- 可将在<errno.h>中定义的各种出错分为致命性及非致命性
- 致命性:无法执行回复动作。最多能在用户屏幕上打印出一条错误消息或将一条出错消息写入日志文件中,然后退出
- 非致命性:大多数非致命性错误时暂时的(如资源不足),当系统中的活动较少时,这种出错很可能不会发生
- 可将在<errno.h>中定义的各种出错分为致命性及非致命性
1.8 用户标识
- 用户ID
- 口令文件登录项中的用户ID(user ID)是一个数值,他向系统标识各个不同的用户。
- 用户不能更改其用户ID。
- 通常每个用户拥有一个唯一的用户ID。
- 用户ID为0的用户为根用户(root)或超级用户(superuser)。
- 若一个进程具有超级用户特权,则大多数文件权限检查都不再进行。某些操作系统功能只向超级用户提供,超级用户对系统有自由的支配权。
- Mac OS X客户端版本交由用户使用时,禁用超级用户账户,服务器版本则可使用。
- 组ID
- 口令文件登录项也包括用户的组ID(group ID),它是一个数值。
- 组ID由系统管理员在指定用户登录名时分配。
- 一般来说,多个登录项具有相同的组ID。
- 同组各个成员之间由于此机制能够共享资源
- 组文件将组名映射为数值的组ID。组文件通常是/etc/group
- 对于磁盘上每个文件,文件系统存储该文件所有者的用户ID 和组ID
- 存储这两个值只需要4字节(假定每个都已双字节整形值存放)
- 若使用完整ASCII登录名和组名,则需要更多的磁盘空间。另外,在检验权限期间,比较字符串更耗费时间。
- 附属组ID
- 从4.2BSD开始,允许一个用户属于多至16个其他的组
- 登录时,读文件/etc/group,寻找列有该用户作为其成员的前16个记录项就可以得到该用户的附属组ID(supplementary group ID)
- POSIX要求系统至少应支持8个附属组。实际上,大多数系统支持16个。
1.9 信号
- 信号(signal)用于通知进程发生了某种情况。
- 进程由3种处理信号的方式
- 忽略信号
- 按系统默认方式处理
- 提供一个函数,信号发生时调用该函数(回调)
- 终端键盘上有2种产生信号的方法
- 中断键(interrupt key)及退出键(quit key):中断键,通常是Delete键或Ctrl+C;退出键,通常是Ctrl+\。用于中断当前运行的进程
- kill函数:调用此函数可向另一个进程发送中止信号
- 当向一个进程发送信号时,必须是那个进程的所有者或是超级用户
1.10 时间值
- UNIX系统使用过两种不同的时间值
- 日历时间
- UTC时间,早期手册称为格林尼治标准时间。该值自协调时间时(Coordinated Universal Time,UTC)1970年1月1日 00:00:00这个特定时间以来所经过的秒数累计值。
- 此时间值可用于记录文件最近一次的修改时间等。
- 系统基本数据类型time_t用于保存这种时间值。
- 进程时间
- 也可称为CPU时间,用以度量进程使用的中央处理器资源
- 进程时间以时钟滴答(Tick)计算
- 系统基本数据类型clock_t保存这种时间值。
- 当度量一个进程的执行时间时,UNIX为一个进程维护了3个进程时间值
- 时钟时间
- 又称为墙上时钟时间(wall clock time),它是进程运行的时间总量,与系统种同时运行的进程树有关。
- 用户CPU时间
- 执行用户指令所用的时间量
- 系统CPU时间
- 该进程执行内核程序所经历的时间
- 时钟时间
- 日历时间
1.11 系统调用和库函数
- 所有的操作系统都提供多种服务的入口点,由此程序向内核请求服务。
- 各种版本的UNIX实现都提供良好定义、数量有限、直接进入内核的入口点,这些入口点即系统调用(system call)
- UNIX所使用的技术是为每个系统调用在标准C库种设置一个具有同样名字的函数。
- 用户进程用标准C调用序列来调用这些函数,然后函数又用系统所要求的技术调用相应的内核服务。
- 两个差别
- 应用程序既可以调用系统调用也可以调用库函数
- 系统调用通常提供一种最小接口,而库函数通常提供比较复杂的功能
第二章 标准及实现
2.1 引言
2.2 UNIX标准化
- ISO C
- IEEE POSIX
- Single UNIX Specification
- FIPS
- 详见书P20~P26
2.3 UNIX系统实现
- SVR4
- 4.4BSD
- FreeBSD
- Linux
- Mac OS X
- Solaris
- 其他
- 详见书P26~P29
2.4 标准和实现的关系
- 详见书P29
2.5 限制
- 具体见书P29~P6043
- 为了解决不同系统限制不一的问题,提出三种限制:
- 编译时限制(头文件)
- 与文件或目录无关的运行时限制(sysconf函数)
- 与文件或目录有关的运行时限制(pathconf函数和fpathconf函数)
ISO C限制
- ISO C定义的所有编译时限制都列在头文件<limits.h>中。这些限制常量在一个给定系统中不会改变
- 在64位系统中,long与longlong整型的最大值相匹配
- 具体见书P30~P31
POSIX限制
- 数值限制
- 最小值
- 最大值
- 运行时可增加的值
- 运行时不变值
- 其他不变值
- 路径名可变值
XSI限制
- 最小值
- 运行时不变值
函数sysconf、pathconf和fpathconf
- 这三个函数能够在代码运行时根据最值表获取系统当前运行时所对应的最值。能够依系统运行状态变化而变化。
不确定的运行时限制
- 如果需要调用的最值限制不确定,可直接设为系统对应支持的最值。
- 在支持XSI扩展的系统中,可调用getrlimit函数,它能够返回一个进程可以同时打开的描述符的最多个数。这样就不需要强制设置上限。
- 路径名
- 最大文件打开数
2.6 选项
- POSIX.1定义了3种处理选项的方法
- 编译时选项定义在<unistd.h>中
- 与文件或目录无关的运行时选项用sysconf函数判断
- 与文件或目录有关的运行时选项通过调用pathconf或fpathconf函数判断
- 若符号常量未定义,则必须使用上述三种函数判断是否支持该选项
- 这种情况下,这三种函数的name参数前缀_POSIX必须替换 _SC或 _PC。对于 _XOPEN为前缀的常量,同上
2.7 功能测试宏
2.8 基本系统数据类型
- 头文件<sys/type.h>中定义了某些与实现有关的数据类型,它们被称为基本系统数据类型(primitive system data type)
2.9 标准之间的冲突
- 因为各系统支持的标准不同,在使用如clock_t的类型时,需要注意时间单位
第三章 文件I/O
3.1 引言
- UNIX系统中大多数文件I/O只需用到5个函数:open、read、write、close以及lseek
- 不带缓冲指的是每个read和write都调用内核中的一个系统调用
3.2 文件描述符
对于内核而言,所有打开的文件都通过文件描述符引用。
文件描述符是一个非负整数
当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符
按照惯例,UNIX系统shell把
- 文件描述符0规定为进程的标准输入,STDIN_FILENO
- 文件描述符1规定为进程的标准输出,STDOUT_FILENO
- 文件描述符2规定为进程的标准错误,STDERR_FILENO
- 在<unistd.h>中定义
范围为0~OPEN_MAX-1
- 早期UNIX系统采用的上限为19(每个进程允许最多打开20个文件)
- 现在很多系统上限为63
- FreeBSD8.0、Linux3.2.0、Mac OS X 10.6.8以及Solaris 10,文件描述符范围几乎无限,因为它们只受到系统配置的存储器总量、整型的字长及系统管理员所配置的软限制和硬限制约束
3.3 函数open和openat
- 在<fcntl.h>中定义
- int open(const char* path, int oflag, …/* mode_t mode */)
- int openat(int fd, const char* path, int oflag, …/* mode_t mode */)
- 具体选项见书P50~P51
- openat函数是POSIX.1最新版本中新增的一类函数之一,主要解决两个问题
- 让线程可以使用相对路径名打开目录中的文件,而不再和open一样只能打开当前工作目录
- 可以避免TOCTTOU(time-of-check-to-time-of-use),TOCTTOU指的是如果有两个基于文件的函数调用,其中第二个调用依赖于第一个调用的结果,那么程序是脆弱的。因为两个调用并不是原子操作,在两个函数调用之间文件可能改变了,这样也就造成了第一个调用的结果不再有效,使得程序最后的结果是错误的
- openat相较于open函数
- 多了参数fd
- path参数指定的是绝对路径名,在这种情况下,fd参数被忽略,openat相当于open
- path参数指定的是相对路径名,fd参数指出了相对路径名在文件系统中的开始地址。fd参数是通过打开相对路径名所在的目录获取
- path参数指定了相对路径名,fd参数具有特殊值AT_FDCWD。这种情况下,路径名在当前工作目录中获取,openat函数在操作上与open函数类似
- 文件名和路径名截断
- 若创建文件名超出NAME_MAX的文件,会发生
- 若是早期System V版本会将文件名截断至NAME_MAX,且不给出任何信息
- 若是Linux或BSD类的系统会返回出错状态,并将errno设置为NAME_MAX
- 大多数现代文件系统支持文件名的最大长度可以为255。因为文件名通常比这个限制要短,因此对大多数应用程序来说这个限制还未出现什么问题
- 若创建文件名超出NAME_MAX的文件,会发生
3.4 函数creat
- 在<fcntl.h>中定义
- int creat(const char* path, mode_t mode)
- 等效于open(path, O_WRONLY | O_CREAT | O_TRUNC, mode)
- 早期的UNIX系统版本中,open的第二个参数只能是0、1、2,无法打开一个尚未存在的文件,因此需要另一个系统调用creat以创建新文件
- 不足之处在于,只能以只写方式打开所创建文件。
3.5 函数close
- <fcntl.h>中定义
- int close(int fd)
- 关闭一个文件时会释放该进程加在该文件上的所有记录锁
- 当一个进程终止时,内核会自动关闭它所有的打开文件。很多程序利用了这一功能而不显式的用close关闭打开文件
3.6 函数lseek
- <fcntl.h>中定义
- off_t lseek(int fd, off_t offset, int whence)
- 关于offset与whence
- 若whence=SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个字节
- 若whence=SEEK_CUR,则将该文件的偏移量设置为其当前值加offset,offset可为正或负
- 若whence=SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可为正或负
- 上述三个参数是在System V中引入的
- 若lseek成功执行,则返回新的文件偏移量
- 关于offset与whence
- off_t lseek(int fd, off_t offset, int whence)
- 每个打开文件都有一个与其相关联的”当前文件偏移量“(current file offset)。通常是一个非负整数,用以度量从文件开始处计算的字节数
- 通常读、写操作都从当前文件偏移量开始,并使偏移量增加所读写的字节数
- 按系统默认情况,打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0
- 某些设备可能允许文件有负的偏移量,因此在比较lseek的返回值时应测试它是否等于-1 !!!!
- lseek仅将当前的文件偏移量记录在内核中,不引起任何I/O操作
- 文件偏移量可大于文件当前长度,那么文件的下一次写入将加长该文件并在文件中构成一个空洞,空洞中没有写过的字节都被读为0
- 文件中的空洞并不要求在磁盘上占用存储区,具体处理方式和文件系统的实现有关
- 只有新写入的数据需要分配磁盘块,对于原文件尾端和新开始写入位置之间的部分则不需要分配磁盘块
- 因为lseek使用的偏移量用off_t类型表示,所以允许具体实现根据各自特定的平台自行选择大小合适的数据类型!!!!
3.7 函数read
- <unistd.h>中定义
- ssize_t read(int fd, void *buf, size_t nbytes)
- 通常返回读到的字节数
- 返回0表示读到文件尾
- 返回-1表示出错
- ssize_t read(int fd, void *buf, size_t nbytes)
- 多种情况可使实际读到的字节数少于期望读到的字节数
- 读普通文件时,在读到期望字节数之前已到达了文件尾端
- 读终端设备时,通产一次最多读一行
- 读网络设备时,网络中的缓冲机制可能造成返回值小于期望读到的字节数
- 读管道或FIFO时,若管道包含的字节少于所需数量
- 读某些面向记录的设备(如磁带),一次最多返回一个记录
- 信号造成中断时,已经读了部分数据量时。
- POSIX.1从几个方面对read函数原型做了修改
- 为了与ISO C一致,第2个参数类型由char* 改为void* 。在ISO C中,void* 表示通用指针
- 返回值必须是一个带符号整形(ssize_t),以保证能够返回正整数字节数、0(表示文件尾端)或-1(出错)
- 第3个参数在历史上是一个无符号整型,允许一个16位的实现一次读或写的数据多达65534字节。ssize_t为有符号整型
3.8 函数write
- <fcntl.h>中定义
- ssize_t write(int fd, const void* buf, size_t nbytes)
- 返回值通常与nbytes相同,否则表示出错
- ssize_t write(int fd, const void* buf, size_t nbytes)
- 出错的一个常见原因为磁盘写满,或超过一个给定进程的文件长度限制
3.9 I/O效率
- 所有常用的UNIX系统shell都提供一种方法,它在标准输入上打开一个文件用于读,在标准输出上创建(或重写)一个文件。这使得程序不必打开输入和输出文件,并允许用户利用shell的I/O重定向功能
- 大多数系统为改善性能都采用某种预读(read ahead)技术。当检测到正在进行顺序读取时,系统就试图读入比应用所要求的更多数据,并假想应用很快就会读这些数据
3.10 文件共享
- UNIX系统支持在不同进程间共享打开文件
- 内核使用3种数据结构表示打开文件
- 每个进程表中都有一个记录项,记录项中包含一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
- 文件描述符标志
- 指向一个文件表项的指针
- 内核为所有打开文件维持一张文件表。每个文件表项包含
- 文件状态标志(读、写、读写、同步和非阻塞等)
- 当前文件偏移量
- 指向该文件v节点表项的指针
- 每个打开文件(或设备)都有一个v节点(v-node)结构
- v节点包含了文件类型和对此文件进行各种操作函数的指针
- 对于大多数文件,v节点还包含了该文件的i节点(i-node,索引节点)。这些信息在打开文件时从磁盘上读入内存的,所以,文件的所有相关信息都是随时可用的。
- 每个进程表中都有一个记录项,记录项中包含一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
- 创建v节点结构的目的时对在一个计算机系统上的多文件系统类型提供支持。
- Linux没有使用v节点,而是使用了通用i节点结构
- Linux没有将相关数据结构分为i节点和v节点,而是采用了一个与文件系统相关的i节点和一个与文件系统无关的i节点。
- 打开文件的每个进程都能获得各自的一个文件表项,但对于一个给定的文件只有一个v节点表项!!!
- 可能由多个文件描述符项指向同一文件表项
- 文件描述符标志和文件状态标志在作用范围的区别
- 前者只用于一个进程的一个描述符!!!
- 后者应用于指向该给定文件表项的任何进程中的所有描述符!!!
3.11 原子操作
- 任何要求多于一个函数调用的操作都不是原子操作,因为在两个函数之间,内核有可能会临时挂起进程。
- 在追加(写入)到一个文件时,可增加O_APPEND标志,使得内核在每次写操作之前,都将进程的当前偏移量设置到该文件的尾端处。
- 函数pread和pwrite
- <unistd.h>中定义
- ssize_t pread(int fd, void* buf, size_t nbytes, off_t offset)
- ssize_t pwrite(int fd, const void* buf, size_t nbytes, off_t offset)
- 调用pread时,无法中断其定位和读操作。并且不会更新当前文件偏移量
- <unistd.h>中定义
- 一般而言,原子操作(atomic operation)指的是由多步组成的一个操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行。
3.12 函数dup和dup2
- <unistd.h>中定义
- int dup(int fd)
- int dup2(int fd, int fd2)
- 返回值为新的文件描述符,出错返回-1
- 两个函数都可用于负值一个现有的文件描述符
- dup返回的新文件描述符一定是当前可用文件描述符中的最小数值
- dup2可用fd2参数指定新描述符的值。
- 若fd2已经打开,则先将其关闭
- 若fd等于fd2,则dup2返回fd2,不关闭它。否则fd2的FD_CLOSEXEC文件描述符标志被清除,fd2在进程调用exec时就是打开状态
- dup等效于fcntl(fd, F_DUPFD, 0),而dup2(fd, fd2)等效于close(fd2)及fcntl(fd, F_DUPFD, fd2),而dup2又不完全等效于后者
- 因为
- dup2是一个原子操作,而close和fcntl包括两个函数调用。有可能在close和fcntl之间调用了信号捕获函数而修改了文件描述符
- dup2和fcntl有一些不同的errno
- 因为
3.13 函数sync、fsync和fdatasync
- 传统的UNIX系统实现在内核中设有缓冲区告诉缓存或页高速缓存,大多数磁盘I/O都通过缓冲区进行
- 当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,之后空闲时再写入磁盘。这种方式被称为延迟写(delayed write)
- 通常,当内核需要重用缓冲区来存放其他磁盘块数据时,它会把所有延迟写数据块写入磁盘。
- <unistd.h>中定义
- int fsync(int fd)
- int fdatasync(int fd)
- void sync(void)
- 返回值为0,出错为-1
- sync只将所有修改过的块缓冲区排入写队列,然后返回,它并不等待实际写磁盘操作结束
- 通常称为update的系统守护进程周期性地调用(一般每隔30s)sync函数,保证定期冲洗(flush)内核的块缓冲区
- fsync只对由文件描述符fd指定的一个文件起作用,并且等待写磁盘操作结束才返回
- fsync可用于数据库这样的应用程序,这种应用程序需要确保修改过的块立即写入磁盘
- fdatasync类似于fsync,它之影响文件的数据部分,除数据外的部分,还会更新文件的属性
- FreeBSD8.0不支持fdatasync
3.14 函数fcntl
<fcntl.h>
- int fcntl(int fd, int cmd, … /* int arg */)
- 返回值依赖于cmd,出错返回-1
- int fcntl(int fd, int cmd, … /* int arg */)
fcntl有以下5种功能
- 复制一个已有的描述符(cmd = F_DUPFD 或 F_DUPFD_CLOSEXEC)
- 获取/设置文件描述符标志(cmd = F_GETFD 或 F_SETFD)
- 获取/设置文件状态标志(cmd = F_GETFL 或 F_SETFL)
- 获取/设置异步I/O所有权(cmd = F_GETOWN 或 F_SETOWN)
- 获取/设置记录锁(cmd = F_GETLK、F_SETLK或F_SETLKW)
- 选项具体见书P66~P67
3.15 函数ioctl
- 终端I/O是使用ioctl最多的地方
- System V <unistd.h>中定义
- BSD和Linux <sys/ioctl.h>中定义
- int ioctl(int fd, int request, …)
- 返回对应request值,出错返回-1
- int ioctl(int fd, int request, …)
- 对于ISO C原型,用省略号表示其余参数。但是通常只用另外一个参数,它常常是指向一个变量或结构的指针
- 通常除了POSIX.1所要求的基本操作外,还需要设备专用的头文件,如终端I/O<termios.h>
- 每个驱动设备程序可以定义专署的一组ioctl命令
3.16 函数/dev/fd
- 较新的系统都提供名为/dev/fd的目录,其目录项是名为0、1、2等的文件。
- 打开文件/dev/fd/n等效于复制描述符n(假定描述符n是打开的)
- 0、1、2等效于stdin、stdout、stderr
第四章 文件和目录
4.1 引言
4.2 函数stat、fstat、fstatat及lstat
- 返回目录结构信息
- <sys/stat.h>中定义
- int stat(const char* restrict pathname, struct stat* festrict buf)
- int fstat(int fd, struct stat* buf)
- int lstat(const char* restrict pathname, struct stat* restrict buf)
- int fstatat(int fd, const char* restrict pathname, struct stat* rextrict buf, int flag)
- 返回值0,出错-1
- <sys/stat.h>中定义
- lstat返回该符号链接的有关信息,而不是由该符号链接引用的文件的信息
- fstatat是一个相对于当前打开目录(由fd参数指向)的路径名返回文件统计信息
- fd参数为AT_FDCWD,并且pathname参数是一个相对路径名,fstatat会计算相对于当前目录的pathname参数
- 若pathname是一个绝对路径,fd参数会被忽略
- timespec结构类型按照秒和纳秒定义了时间
4.3 文件类型
- UNIX系统过的大多数文件是普通文件或路路,但是也有另外一些文件类型
- 普通文件(regular file)
- 对普通文件内容的解释由处理该文件的应用程序执行
- 目录文件(directory file)
- 包含了其他文件的名字及指向与这些文件有关信息的指针
- 对于一个目录文件具有读权限的任意进程都可以读该目录的内容,但只有内核可以直接写目录文件
- 块特殊文件(block special file)
- 提供对设备(如磁盘)带缓冲的访问,每次访问以固定长度为单位执行
- 字符特殊文件(character special file)
- 提供对设备不带缓冲的访问,每次访问长度可变
- FIFO
- 用于进程间通信,有时也被称为管道(pipe)
- 套接字(socket)
- 用于进程间网络通信
- 也可以用于在一台宿主机上进程之间的非网络通信
- 符号链接(symbolic link)
- 指向另一个文件
- 普通文件(regular file)
- 系统中的而所有设备要么是字符特殊文件,要么是块特殊文件
- 文件类型信息包含在stat结构的st_mode成员
- 具体宏见书P76
- POSIX.1允许实现将进程间通信(IPC)对象(如消息队列和信号量等)说明为文件
4.4 设置用户ID和设置组ID
- 实际用户ID和实际组ID表示我们究竟是谁。/etc/passwd
- 有效用户ID、有效组ID及附属组ID决定了我们的文件访问权限
- 保存的设置用户ID和保存的设置组ID在执行一个程序时包含了有效用户ID和有效组ID的副本
- 通常有效用户ID等于实际用户ID,有效组ID等于实际组ID
- 每个文件都有一个所有者和组所有者,所有者由stat结构中的st_uid指定,组所有者则由st_gid指定
4.5 文件访问权限
所有文件类型(目录、字符特别文件等)都有访问权限(access permission)
每个文件由9个访问权限位,分三类
- <sys/stat.h>
- 用户 u
- 用户读 S_IRUSR 4
- 用户写 S_IWUSR 2
- 用户执行 S_IXUSR 1
- 组 g
- 组读 S_IRGRP 4
- 组写 S_IWGRP 2
- 组执行 S_IXGRP 1
- 其他 o
- 其他读 S_IROTH 4
- 其他写 S_IWOTH 2
- 其他执行 S_IXOTH 1
执行权限位也叫搜索位,只有此位置位才可搜索打开
对于一个文件的读权限决定了我们是否能够打开现有文件进行写操作
对于一个文件的写权限决定了我们是否能够打开现有文件进行写操作
为了在open函数中对一个文件指定O_TRUNC标志,必须对该文件具有写权限
问了在一个目录中创建一个新文件,必须对目录具有写权限和执行权限
为了删除一个现有文件,必须对包含该文件的目录具有写权限和执行权限,对该文件本身不需要由读、写权限
如果用7个exec函数中的任何一个执行某个文件,都必须对该文件具有执行权限。该文件还必须是一个普通文件
进程每次打开、创建或删除一个文件时,内核就进行文件访问权限测试
- 若进程的有效用户ID时0(超级用户),则允许访问
- 若进程的有效用户ID等于文件的所有者ID(即进程拥有此文件),那么如果所有者适当的访问权限位被设置,则允许访问,否则拒绝访问
- 若进程的有效组ID或进程的附属组ID之一等于文件的组ID,那么如果组适当的访问权限位被设置,则允许访问,否则拒绝访问
- 若其他用户适当的访问权限位被设置,则允许访问,否则拒绝访问
4.6 新文件和目录的所有权
- 新文件的用户ID设置为进程的有效用户ID,关于组ID,POSIX.1允许实现
- 新文件的组ID可以是进程的有效组ID
- 新文件的组ID可以是它所在目录的组ID
- FreeBSD8.0和MAC OS X10.6.8系统默认设置组所有权的方法,但Linux3.2.0及Solaris10之下,必须使用设置组ID位起作用
4.7 函数access和faccessat
- 实际用户ID及实际组ID进行访问权限测试
- <unistd.h>中定义
- int access(const char* pathname, int mode)
- int faccessat(int fd, const char* pathname, int mode, int flag)
- 返回值0,出错-1
4.8 函数umask
- 为进程设置文件模式创建屏蔽字,并返回之前的值,用于确认访问权限是否置位
- 4、2、1
- <sys/stat.h>中定义
- mode_t umask(mode_t cmask)
- 返回之前的文件模式创建屏蔽字
- cmask为(S_IRUSR、S_IWUSR等)中的若干个按位或构成
- mode_t umask(mode_t cmask)
- 在进程创建一个新文件或新目录时,一定会使用文件模式创建屏蔽字
4.9 函数chmod、fchmod及fchmodat
更改现有文件的访问权限
<sys/stat.h>中定义
int chmod(const char* pathname, mode_t mode)
int fchmod(int fd, mode_t mode)
fchmodat(int fd, const char* pathname, mode_t mode , int flag)
- 返回0,出错-1
chmod在指定的文件上进行操作,fchmod则对已打开的文件进行操作
fchmodat pathname为绝对路径,当fd为AT_FDCWD,pathname为相对路径
- flag参数可用于改变fchmodat的行为,当设置了AT_SYMLINK_NOFOLLOW标志时,fchmodat并不会跟随符号链接
改变一个文件的权限位,进程的有效用户ID必须等于文件的所有者ID,或者该进程必须具有超级用户权限
chmod将在下列条件下自动清除两个权限位
- Solaris等系统对用于普通文件的粘着位赋予了特殊含义,在这些系统上如果我们试图设置普通文件的粘着位(S_ISVTX),而且没有超级用户权限,那么mode中的粘着位自动被关闭。防止恶意用户设置粘着位,由此影响系统性能(Linux无此意义)
- 新创建文件的组ID可能不是调用进程所属的组。特别的,如果新文件的组ID不等于进程的有效组ID或者进程附属组ID中的一个,且进程没有超级用户权限,则设置组ID位会被自动关闭。防止用户创建一个设置组ID的文件,而该文件是由并非该用户所属的组拥有的
4.10 粘着位
- 若一个可执行程序文件的这一位被设置了,则当该程序第一次被执行,在其终止时,程序正文部分的一个副本仍被保存在交换区(程序的正文部分是机器指令)。使得下次执行该程序时能较快地将其装载入内存
- 如果对一个目录设置了粘着位,只有对该目录具有写权限的用户且满足下列条件之一才能删除或重命名该目录下的文件
- 拥有此文件
- 拥有此目录
- 是超级用户
- 目录/tmp及/var/tmp是设置粘着位的典型候选者,任何用户都可在这两个目录中创建文件
4.11 函数chown、fchown、fchownat及lchown
- <unistd.h>中定义
- int chown(const char* pathname, uid_t owner, gid_t group)
- int fchown(int fd, uid_t owner, gid_t group)
- int fchownat(int fd, const char* pathname, uid_t owner, gid_t group, int flag)
- int lchown(const char* pathname, uid_t owner, gid_t group)
- 返回0,出错-1
4.12 文件长度
- stat结构成员st_size表示以字节为单位的文件的长度。此字段只对普通文件、目录文件和符号链接有意义
- 对于普通文件,其文件长度可以是0,在开始读这种文件时,将得到文件结束(END_OF_FILE,EOF)指示
- 对于目录,文件长度通常是一个数的整倍数
- 对于符号链接,文件长度是在文件名中的实际字节数
4.13 文件截断
- <unistd.h>中定义
- int truncate(const char* pathname, off_t length)
- int ftruncate(int fd, off_t length)
- 返回0,出错-1
- 两个函数将一个现有文件长度截断为length
- 若文件以前的长度大于length,则超过length外的数据不再能访问
- 若文件以前的长度小于length,则文件长度增加至length,在以前文件尾端和新文件尾端之间的数据读作0(即创建一个文件空洞)
4.14 文件系统
- 大部分UNIX文件系统支持大小写敏感的文件名
- 基本结构
- 一个磁盘包括
- n个分区,一个分区包括
- 一个文件系统(file system),包括
- 自举块
- 超级块
- n个柱面组,一个柱面组包括
- 超级块副本
- 配置信息
- i节点图
- 块位图
- 数据块
- i节点包括
- n个i节点
- 一个文件系统(file system),包括
- n个分区,一个分区包括
- 一个磁盘包括
- “解除对一个文件的链接并不总是意味着释放该文件占用的磁盘块”以及“删除一个目录项的函数名字为unlink而不是delete”的原因
- 每个i节点都有一个链接计数,其值是指向该i节点的目录项数。
- 只有当链接计数减少至0时,才可删除该文件(也就是可以释放该文件占用的数据块)
- 还有一种链接类型为符号链接(symbolic link)。符号链接文件的实际内容(在数据块中)包含了该符号链接所指向的文件名字。
- i节点包含了文件有关的所有信息:文件类型、文件访问权限位、文件长度及指向文件数据块的指针等。
- 因为目录项中的i节点编号指向同一文件系统中的相应i节点,一个目录项不能指向另一个文件系统的i节点。
- 当在不更换文件系统的情况下为一个文件重命名时,该文件的实际内容并未移动,只需构造一个指向现有i节点的新目录项,并删除老目录项。链接计数不会改变。
4.15 函数link、linkat、unlink、unlinkat及remove
- 创建一个指向现有文件的链接
- <unistd.h>
- int link(const char* existingpath, const char* newpath)
- int linkat(int efd, const char* existingpath, int nfd, const char* newpath, int flag)
- 返回0,出错-1
- linkat,默认情况下,如果两个路径名中的任一个是相对路径,那么它需要通过相对于对应的文件描述符进行计算。AT_FDCWD
- 创建新目录项和增加链接计数应当是一个原子操作
- POSIX.1允许实现支持跨越文件系统的链接
- 如果实现支持创建指向一个目录的硬链接,仅限于超级用户。
- 理由为这样做可能在文件系统中形成循环,大多数处理文件系统的实用程序都不能处理这种情况。
- 如果实现支持创建指向一个目录的硬链接,仅限于超级用户。
- <unistd.h>
- 删除一个现有的目录项
- <unistd.h>
- int unlink(const char* pathname)
- int unlinkat(int fd, const char* pathname, int flag)
- 返回值0,出错-1
- 两个函数删除目录项后,并将由pathname所引用文件的链接计数减1
- 只有当链接计数为0时,文件的内容才可被删除
- 无任何进程打开文件,文件内容才可被删除
- 先检查打开文件的进程数,若为0,则继续检查链接计数,若为0,则删除文件内容
- 当设置AT_REMOVEDIR时,unlinkat与rmdir一样能够删除目录。若清除,则执行与unlink一样的操作
- 若pathname为符号链接,则unlink删除该符号链接,而不是删除由该链接所引用的文件
- <unistd.h>
- 解除对一个文件或目录的链接或删除目录
- <stdio.h>
- int remove(const char* pathname)
- 返回0,出错-1
- int remove(const char* pathname)
- 对于文件,remove和unlink功能相同
- 对于目录,remove和rmdir功能相同
- ISO C指定remove删除一个文件,更改了UNIX历来使用的名字unlink,原因为实现C标准的大多数非UNIX系统并不支持文件链接。
- <stdio.h>
4.16 函数rename和renameat
- 文件或目录重命名
- <stdio.h>
- int rename(const char* oldname, const char* newname)
- int renameat(int oldfd, const char* oldname, int newfd, const char* newname)
- 返回0,出错-1
- 如果oldname指的是一个文件而不是目录,那么为该文件或符号链接重命名
- 若newname已存在,则它不能引用一个目录,若不是目录,则先将该目录项删除然后将oldname重命名为newname
- 如果oldname指的是一个目录,那么为该目录重命名。
- 若newname已存在,则它必须引用一个目录,而且该目录应当是空目录(即目录中只有.项和…项)
- 若newname已存在,且是一个空目录,则先将其删除,然后重命名
- 当为一个目录重命名时,newname不能包含oldname作为其路径前缀。
- 如果oldname或newname引用符号链接,则处理的是符号链接本身,而不是它所引用的文件。
- 不能对.和…重命名
- 如果oldname和newname引用同一文件,则函数不做任何更改而成功返回
- 若newname已经存在,则调用进程对它需要由写权限。若要删除oldname目录项,并创建newname目录项,需要由写和执行权限
- <stdio.h>
4.17 符号链接(软链接)
- 符号链接是对一个文件的间接指针(硬链接直接指向文件的i节点)。引入符号链接是为了避开硬链接的一些限制
- 硬链接通常要求链接和文件位于同一文件系统中
- 只有超级用户才能创建指向目录的硬链接(在底层文件系统支持的情况下)
- 对符号链接以及它指向何种对象并无任何文件系统限制,任何用户都可以创建指向目录的符号链接
- 符号链接一般用于将一个文件或整个目录结构移到系统中另一个位置
- 当使用以名字引用文件的函数时,应当了解该函数是否跟随符号链接到达它所链接的文件
- 若该函数具有处理符号链接的功能,则其路径名参数引用由符号链接指向的文件。否则,一个路径名参数引用链接本身,而不是由该链接指向的文件
- 见书P97 4-17
- 命令
- ln -s 。。。 。。。
- ls -l 若第一字符是1,则表示它是一个符号链接,->也表示它是一个符号链接
- ls -F 若文件名后有@,则表示它是一个符号链接
4.18 创建和读取符号链接
- <unistd.h>
- int symlink(const char* actualpath, const char* sympath)
- int symlinkat(const char* actualpath, int fd, const char* sympath)
- 返回0,出错-1
- 在创建此符号链接时,不要求actualpath已经存在,且actualpath即sympath不需要位于同一文件系统中
- 若sympath指定的是绝对路径或fd设置了AT_FDCWD值,那么symlinkat就等同于symlink
- 由于open函数跟随符号链接,因此需要有一种方法打开链接本身,并读链接中的名字
- <unistd.h>
- ssize_t readlink(const char* restrict pathname, char* restrict buf, size_t bufsize)
- ssize_t readlinkat(int fd, const char* restrict pathname, char* restrict buf, size_t bufsize)
- 返回读取的字节数,出错-1
- 在buf中返回的符号链接内容不以null字节终止
- AT_FDCWD同样效果
- <unistd.h>
4.19 文件的时间
- 每个文件属性所保存的实际精度依赖于文件系统的实现。
- 对于把时间戳记录在秒级的文件系统来说,纳秒这个字段就会被填充为0
- 对于时间戳的记录精度高于秒级的文件系统来说,不足秒的值被转换成纳秒并记录在纳秒这个字段中
- 时间字段
- st_atim 文件数据的最后访问时间,指令ls -u
- st_mtim 文件数据的最后修改时间,指令ls
- st_ctim i节点状态的最后修改时间, 指令ls -c
- st_mtim与st_ctim的区别:修改时间是文件内容最后一次被修改的时间,状态更改时间是该文件的i节点最后一次被修改的时间
- 系统不维护i节点的最后一次访问时间,即st_atim,通过access以及stat函数不更改3个时间中的任一个
- 时间字段
- 见书P100~P101表4-20
4.20 函数futimens、utimensat及utimes
文件的访问和修改
<sys/stat.h>
- int futimens(int fd, const struct timespec times[2])
- int utimensat(int fd, const char* path, const struct timespec times[2], int flag)
- 返回0,出错-1
- 可指定纳秒级精度的时间戳
- futimens函数需要打开文件来更改他的时间,支持AT_FDCWD
- utimensat的flag参数可用于进一步修改默认行为
- 若设置了AT_SYMLINK_NOFOLLOW标志,则符号链接本身的时间就会被修改。默认的行为是跟随符号链接,并把文件的时间改成符号链接的时间
- 两个函数都包含在POSIX.1种
时间戳指定的4种方式
- 若times参数是一个空指针,则访问时间和修改时间两者都设置为当前时间
- 若times参数指向两个timespec结构的数组,任一数组元素的tv_nsec字段的值为UTIME_NOW,相应的时间戳就设置为当前时间,忽略相应的tv_sec字段
- 若times参数指向两个timespec结构的数组,任一数组元素的tv_nsec字段的值为UTIME_OMIT,相应的时间戳保持不变,忽略相应的tv_sec字段
- 若times参数指向两个timespec结构的数组,且tv_nsec字段的值既不是UTIME_NOW也不是UTIME_OMIT,在这种情况下,相应的时间戳设置为相应的tv_sec及tv_nsec字段的值
- 执行这些函数所要求的优先权取决于times参数的值
- 若times是一个空指针,或者任一tv_nsec字段设为UTIME_NOW,则进程的有效用户ID必须等于该文件的所有者ID。进程对该文件必须具有写权限,或者进程是一个超级用户进程
- 若times是一个非空指针,并且任一tv_nsec字段的值既不是UTIME_NOW也不是UTIME_OMIT,则进程的有效用户ID必须等于该文件的所有者ID,或者进程必须是一个超级用户进程。对文件只具有写权限是不够的。
- 若times是非空指针,并且两个tv_nsec字段的值都为UTIME_OMIT,就不执行任何的权限检查
4.21 函数mkdir、mkdirat及rmdir
- 创建目录
- int mkdir(const char* pathname, mode_t mode)
- int mkdirat(int fd, const char* pathname, mode_t mode)
- 返回0,出错-1
- 这两个函数创建一个新目录,其中,.及…目录项是自动创建的
- 常见错误
- 指定与文件相同的mode(只指定读、写权限)。但是,对于目录通常至少需要设置一个执行权限位,以允许访问该目录中的文件名
- solaris10及Linux3.2.0也使新目录继承父目录的设置组ID位。这就使得在新目录中创建的文件将继承该目录的组ID。对于Linux,文件系统的实现决定是否支持此特征。如ext2、ext3及ext4文件系统用mount命令的一个选项来控制是否支持此特征。但是Linux的UFS文件系统实现则是不可选择的,新目录继承父目录的设置组ID位,仿效了历史上BSD的实现。在BSD系统中,新目录的组ID是从父目录继承的。
- 基于BSD的系统并不要求在目录间传递设置组ID位,因为不论设置组ID位如何,新创建的文件和目录总是继承父目录的组ID。因为FreeBSD8.0和Mac OS X 10.6.8是基于4.4BSD的,它们不要求继承设置组ID位。在这些平台上,新创建的文件和目录总是继承父目录的组ID,这与是否设置了设置组ID位无关。
- 早期的UNIX版本没有mkdir,它是由4.2BSD及SVR3引入的。进程要调用mknod创建一个新目录,但是只有超级用户进程才能使用mknod。为了避免这一点,创建目录的命令mkdir必须由根用户拥有,而且对它设置了设置用户ID位
- 删除空目录
- <uinistd.h>
- int rmdir(const char* pathname)
- 返回0,出错-1
- int rmdir(const char* pathname)
- 若调用此函数使目录的链接计数为0,且没有其他进程打开此目录,则释放由此目录占用的空间。
- 若在链接计数达到0时,有一个或多个进程打开此目录,则在此函数返回前删除最后一个链接及.和…项。另外,在此目录中不能再创建新文件,但在最后一个进程关闭它之前并不释放此目录。(别的进程也不能执行操作,因为要保证目录为空)
- <uinistd.h>
4.22 读目录
- 为了防止文件系统产生混乱,只有内核才能写目录
- <dirent.h>
- DIR* opendir(const char* pathname)
- DIR* fdopendir(int fd)
- 返回指针,出错null
- struct dirent* readdir(DIR* dp)
- 返回指针,出错或目录尾null
- void rewinddir(DIR* dp)
- int closedir(DIR* dp)
- 返回0,出错-1
- long telldir(DIR* dp)
- 返回与dp关联的目录中的当前位置
- void seekdir(DIR* dp, long log)
- fdopendir最早出现在SUSv4(Single UNIX Specification 第4版)中,可以把打开文件描述符转换成目录处理函数需要的DIR结构
- telldir及seekdir不是基本POSIX.1标准的组成部分,是SUSv4中XSI的扩展
4.23 函数chdir、fchdir及getcwd
- 在进程中更改当前工作目录,只对本进程有效,对其他进程无效
- <unistd.h>
- int chdir(const char* pathname)
- int fchdir(int fd)
- 返回0,出错-1
- Linux内核可以确定完整路径名。完整路径名的各个组成部分分布在mount表和dcache表中,然后进行重新组装
- <unistd.h>
- 获取工作目录完整绝对路径名
- <unistd.h>
- char* getcwd(char* buf, size_t size)
- 返回buf,出错null
- char* getcwd(char* buf, size_t size)
- <unistd.h>
4.24 设备特殊文件
- 每个文件系统所在的存储设备都由其主、次设备号表示。
- 设备号数据类型为dev_t
- 主设备号标识设备驱动程序
- 次设备号标识特定的子设备
- 通常使用两个宏访问设备号
- major访问主设备号
- minor访问次设备号
- 早期系统用16位整型存放设备号,8位主设备号,8位次设备号
- FreeBSD8.0及Mac OS X10.6.8使用32位整型,8位主设备号,24位次设备号
- 32位系统中
- Solaris 10用32位整型表示dev_t,14位主设备号,18位次设备号
- 64位系统中
- Solaris 10用64位整型表示dev_t,主次设备号都为32位
- Linux3.2.0用64位整型表示dev_t,12位主设备号,20位次设备号
- 基于BSD的UNIX系统将宏major和minor定义在<sys/type.h>中,Solaris定义在<sys/mkdev.h>,Linux定义在<sys/sysmacros.h>中,此头文件又包含在<sys/type.h>中
- 系统中与每个文件名关联的st_dev值是文件系统的设备号,此文件系统包含了这一文件名及其对应的i节点
- 只有字符特殊文件和块特殊文件才有st_rdev值。此值包含时间设备的设备号
4.25 文件访问权限位
- 表格见书P113
第五章 标准I/O库
5.1 引言
- 标准I/O 库处理很多细节,如缓冲区分配、以优化的块长度执行I/O等,这些处理使用户不必担心如何选择使用正确的块长度。
5.2 流和FILE对象
- 标准I/O库,它们的操作是围绕流(stream)进行的(请勿将标准I/O术语流与System V的STREAM I/O系统相混淆,STREAM I/O系统是System V的组成部分,Single UNIX Specification 则将其标准化为XSI STREAMS选项,但是在SUSv4已经将其标记为弃用)。
- 当用标准I/O库打开或创建一个文件时,已经让一个流与一个文件相关联。
- 标准I/O文件流可用于单字节或多字节(“宽”)字符集。
- 流的定向(stream’s orientation)决定了读、写的字符是单字节还是多字节的。
- 当一个流最初被创建时,并没有被定向。
- 如若在未定向的流上使用一个多字节I/O函数,则将流的定向设置为宽定向的。
- 若在未定向的流上使用一个单字节I/O函数,则将流的定向设为字节定向的。
- <stdio.h>及<wchar.h>
- fwide(FILE* fp, int mode)
- 返回正值(若流是宽定向的),返回负值(若流是字节定向的),返回0(若流是未定向的)
- mode
- 负:fwide将试图使指定的流是字节定向的。
- 正:fwide将试图使指定的流是宽定向的。
- 0:fwide将不试图设置流的定向,但返回标识该流定向的值。
- fwide(FILE* fp, int mode)
- 应用程序没有必要检验FILE对象。
- 为了引用一个流,需将FILE指针作为参数传递给每个标准I/O函数。
5.3 标准输入、标准输出及标准错误
- 对一个进程预定义了3个流,并且这3个流能够自动地被进程使用
- 标准输入(stdin)
- 标准输出(stdout)
- 标准错误(stderr)
- 这3个标准I/O流通过预定义以上文件指针加以引用。
5.4 缓冲
- 标准I/O库提供缓冲的母的是尽可能减少使用read和write调用的次数。也对每个I/O流自动地进行缓冲管理,从而避免了应用程序需要考虑这一点所带来的麻烦。
- 标准I/O库提供3种类型的缓冲
- 全缓冲:在这种情况下,在填满标准I/O缓冲区后才进行实际I/O操作。
- 对于驻留在磁盘上的文件通常是由标准I/O库实施全缓冲的。
- 在一个流上执行第一次I/O操作时,相关标准I/O函数通常调用malloc获得需使用的缓冲区。
- 术语冲洗(flush)说明标准I/O缓冲区的写操作。缓冲区可由标准I/O例程自动地冲洗或者可以调用函数fflush冲洗一个流。
- 在标准I/O库中,flush意味着将缓冲区中的内容写入磁盘。
- 在终端驱动程序中,flush表示丢弃已存储在缓冲区中的数据。
- 行缓冲:在这种情况下,当在输入和输出中遇到换行符时,标准I/O库执行I/O操作。
- 这允许我们一次输出一个字符(用标准I/O函数fputc),但只有在写了一行之后才进行实际I/O操作。
- 当流设计一个终端时,通常使用行缓冲。
- 行缓冲的两个限制:
- 因为标准I/O库用来收集每一行的缓冲区的长度是固定的,所以只要填满了缓冲区,那么即使还没有写一个换行符,也进行I/O操作。
- 任何时候只要通过标准I/O库要求从一个不带缓冲的流,或者一个行缓冲的流(从内核请求需要的数据)得到输入数据,那么就会冲洗所有行缓冲输出流。
- 行缓冲的两个限制:
- 不带缓冲:标准I/O库不对字符进行缓冲存储。
- 标准错误流stderr通常是不带缓冲的,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个换行符。
- ISO C的缓冲特征:
- 当且仅当标准输入和标准输出并不指向交互式设备时,它们才是全缓冲的。
- 标准错误绝不会是全缓冲的。
- 这并不说明如果标准输入和标准输出指向交互式设备时,它们是不带缓冲的还是带缓冲的,以及标准错误是不带缓冲的还是行缓冲的。
- 很多系统默认类型的缓冲
- 标准错误是不带缓冲的。
- 若是指向终端设备的流,是行缓冲的,否则是全缓冲的。
- 全缓冲:在这种情况下,在填满标准I/O缓冲区后才进行实际I/O操作。
- 修改系统默认缓冲类型
- <stdio.h>
- void setbuf(FILE* restrict fp, char* restrict buf)
- int setvbuf(FILE* restrict fp, char* restrict buf, int mode, size_t size)
- 返回0,出错非0
- 通常设置了buf之后就是全缓冲,但如果该流与一个终端设备相关,那么某些系统也可将其设置为行缓冲。
- 为了关闭缓冲,可将buf设置为NULL。
- 具体使用见书P117-P118
- 若在一个函数内分配一个自动变量类的标准I/O缓冲区,则从该函数返回之前,必须关闭该流。
- 由于某些实现将缓冲区的一部分用于存放它自己的管理操作信息,所以可以存放在缓冲区中的实际数据字节数少于size。
- 一般而言,应由系统选择缓冲区的长度,并自动分配缓冲区。在这种情况下关闭此流时,标准I/O库将自动释放缓冲区。
- <stdio.h>
- 强制冲洗流,使流所有未写的数据都被传送至内核,
- <stdio.h>
- int fflush(FILE* fp)
- 返回0,出错EOF
- int fflush(FILE* fp)
- 若fp为NULL,则此函数将导致所有输出流被冲洗!!!
- <stdio.h>
5.5 打开流
5.6 读和写流
5.7 每次一行I/O
5.8 标准I/O的效率
5.9 二进制I/O
5.10 定位流
5.11 格式化I/O
5.12 实现细节
5.13 临时文件
5.14 内存流
5.15 标准I/O的替代软件
第六章 系统数据文件和信息
6.1 引言
6.2 口令文件
6.3 阴影口令
6.4 组文件
6.5 附属组ID
6.6 实现区别
6.7 其他数据文件
6.8 登录账户记录
6.9 系统标识
6.10 时间和日期例程
第7章 进程环境
7.2 main函数
- C程序总是从main函数开始执行
- main函数原型 int main(int argc, char** argv)
- 内核执行C程序时,在调用main前调用一个特殊的启动例程。可执行程序文件将此启动例程指定为程序的起始地址(由连接编辑器设置,连接编辑器由C编译器调用),启动例程从内核取得命令行参数和环境变量值。
7.3 进程终止
7.4 命令行参数
7.5 环境表
7.6 C程序存储空间布局
7.7 共享库
7.8 存储空间分配
7.9 环境变量
7.10 函数setjmp及longjmp
7.11 函数getrlimit及setrlimit
第14章 高级IO
14.4 IO多路复用
五种技术
- 单进程进行循环阻塞IO双向读取,此方法问题为不能对两个输入中的任一个使用阻塞read,因为无法知道到底哪一个输入会得到数据。
- 双进程分别进行循环阻塞IO读取,此方法问题为无法得知操作什么时候终止。
- 单进程轮询非阻塞IO读取。此方法问题为轮询read时,执行read系统调用浪费CPU时间,因为大多数时间无数据可读。
- 异步IO。此方法问题为一个是可移植性问题,另一个是信号重复性问题吗,因为信号数量远小于潜在的打开文件描述符的数量。
- IO多路转接。此方法较好。此方法先构造一个期望的描述符列表,之后调用一个函数,直到描述符中的一个已准备好进行IO时,该函数才返回。
14.4.1 函数select和pselect
- select的使用
- <sys/select.h>
- int select(int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds, struct timeval *restrict tvptr)
- 返回0,超时,出错,-1,非0,准备就绪的描述符数目
- 若tvptr为NULL,永久等待直到所指定描述符中的一个已准备好或捕捉到一个信号则返回;若tvptr->tv_sec与tv_usec均为0,不等待,不阻塞;若tvptr->tv_sec与tv_usec其中之一不为0,等待指定的秒数和微秒数直到所指定描述符中的一个已准备好或捕捉到一个信号则返回,若超时,则返回0
- readfds、writefds、exceptfds都是指向期望的描述符集的指针。存储于一个fd_set数据类型中
- 可使用如下对其进行赋值:
- <sys/select.h>
- int FD_ISSET(int fd, fd_set *fdset)
- 返回非0,fd在描述符集中;反之,返回0
- void FD_CLR(int fd, fd_set *fdset)
- void FD_SET(int fd, fd_set *fdset)
- void FD_ZERO(fd_set *fdset)
- int FD_ISSET(int fd, fd_set *fdset)
- <sys/select.h>
- 可使用如下对其进行赋值:
- int select(int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds, struct timeval *restrict tvptr)
- <sys/select.h>
第十六章 网络IPC:套接字
16.1 引言
16.2 套接字描述符
- 套接字是通信端点的抽象,实际上表示的是一个通信端口。应用层程序通过套接字描述符访问套接字。在UNIX系统中被当作是一种文件描述符。
- 创建套接字函数
- <sys/socket.h>
- int socket(int domain, int type, int protocol)
- 成功返回套接字描述符,出错返回-1
- domain确定通信特性包括地址格式
- 地址族:
- AF_INET IPv4因特网域
- AF_INET6 IPv6因特网域
- AF_UNIX UNIX域
- AF_UPSPEC 未指定(任何域)
- 地址族:
- type确定套接字类型,进一步确定通信特征
- SOCK_DGRAM 固定长度、无连接、不可靠的报文传递
- 此接口,两个对等进程间通信不需要逻辑连接,只需要对对方使用的套接字发送一个报文。
- SOCK_RAW IP协议的数据报接口
- 用于直接访问网络层(即因特网域中的IP层)。
- 使用此接口,应用程序将负责构造自己的协议头部,原因为传输协议被绕过。
- 当创建原始套接字时,需要有超级权限,防止恶意应用程序绕过内建安全机制创建报文。
- SOCK_SEQPACKET 固定长度、有序、可靠、面向连接的报文传递
- 与SOCK_STREAM类似,区别在于此接口是基于报文的服务,数据量保证收发一致。
- SOCK_STREAM 有序、可靠、双向、面向连接的字节流
- 此接口,要求在交换数据前,在本地套接字和通信的对等进程的套接字之间建立一个逻辑连接。
- 数据可能丢失,但为了保证数据报完整,会重复发送,直到完整为止。
- SOCK_DGRAM 固定长度、无连接、不可靠的报文传递
- protocol通常为0,表示给定的域及套接字类型均为默认协议。当对同一域及套接字类型支持多个协议时,可用此参数选择一个特定协议。
- AF_INET通信域中 SOCK_STREAM默认传输协议为TCP,SOCK_DGRAM默认传输协议为UDP
- IPPROTO_IP IPv4网际协议
- IPPROTO_IPV6 IPv6网际协议
- IPPROTO_ICMP 因特网控制报文协议
- IPPROTO_RAW 原始IP数据包协议
- IPPROTO_TCP 传输控制协议
- IPPROTO_UDP 用户数据报协议
- socket与open类似,都可通过函数获得用于I/O的文件描述符,可通过close关闭对文件或套接字的访问。并且释放以便重新使用。
- 套接字不支持文件偏移量概念。
- 套接字通信是双向的。
- 禁止一个套接字的I/O
- <sys/socket.h>
- int shutdown(int sockfd, int how)
- 成功返回0,出错-1
- how选择动作
- SHUT_RD 关闭读端
- SHUT_WR 关闭写端
- SHUT_RDWR 关闭读写
- 与close不同的是,shutdown能够直接指定一个套接字处于不活动状态,与引用它的文件描述符数目无关。
- int shutdown(int sockfd, int how)
- <sys/socket.h>
- 禁止一个套接字的I/O
16.3 寻址
16.3.1 字节序
大端(big-endian):最大字节地址出现在最低有效字节(LSB)上。
小端(little-endian):最小字节地址出现在最低有效字节上。
不管字节是何排序,最高有效字节(MSB)永远在左边。
字节排序与处理器架构相关,有些处理器可以配置选择大小端。
网络协议指定了字节序,异构计算机系统因此能够交换协议信息而不会被字节序所混淆。
- TCP/IP协议栈使用大端字节序。在应用程序交换格式化数据时,就需要注意处理器的字节序与网络字节序之间的转换。
- 处理器字节序与网络字节序之间转换
- <arpa/inet.h>
- uint32_t htonl(uint32_t hostint32)
- 返回网络字节序表示的32位整数
- uint32_t htons(uint16_t hostint16)
- 返回网络字节序表示的16位整数
- uint32_t ntohl(uint32_t netint32)
- 返回主机字节序表示的32位整数
- uint16_t ntohs(uint16_t netint16)
- 返回主机字节序表示的16位整数
- h表示主机字节序,n表示网络字节序,l表示长(4字节)整数,s表示短(2字节)整数
- uint32_t htonl(uint32_t hostint32)
- <arpa/inet.h>
- 处理器字节序与网络字节序之间转换
- TCP/IP协议栈使用大端字节序。在应用程序交换格式化数据时,就需要注意处理器的字节序与网络字节序之间的转换。
16.3.2 地址格式
一个地址标识一个特定通信域的套接字端点,地址格式与特定通信域相关。
为了参数统一,地址会被强制转换为一个通用的地址结构sockaddr
linux中此结构为
struct sockaddr {sa_family_t sa_family; // 地址族char sa_data[14]; // 可变长度地址 };
因特网地址定义在<netinet/in.h>中
// AF_INET,IPv4因特网域 struct in_addr {in_addr_t s_addr; // IPv4地址,uint32_t };struct sockaddr_in {sa_family_t sin_family; // 地址族in_port_t sin_port; // 端口号, uint16_tstruct in_addr sin_addr; // IPv4 地址 };// AF_INET6, IPv6因特网域 struct in6_addr {uint8_t s6_addr[16]; // IPv6地址 };struct sockaddr_in6 {sa_family_t sin6_family; // 地址族in_port_t sin6_port; // 端口号uint32_t sin6_flowinfo; // 交换及流信息struct in6_saddr; // IPv6地址uint32_t sin6_scope_id; // 域接口集合 };// linux中,sockaddr_in定义 struct sockaddr_in {sa_family_t sin_family; // 地址族in_port_t sin_port; // 端口号struct in6_addr sin6_addr; // IPv4 地址unsigned char sin_zero[8]; // 过滤器,填充字段,全置位0 };
sockaddr_in 与 sockaddr_in6均会被强制转换为sockaddr结构输入至套接字函数中。
16.3.3 地址查询
网络配置信息可以存放在静态文件(如/etc/hosts/ 和 /etc/services)中,也可以由名字服务管理。
找到给定计算机系统的主机信息
<netdb.h>
- struct hostent *gethostent(void)
- 成功返回指针,出错NULL
- void sethostent(int stayopen)
- void endhostent(void)
- struct hostent *gethostent(void)
若主机数据库文件没有打开,gethostent会打开它,直接返回文件中的下一个条目
函数sethostent会打卡文件,若文件已被打开则将其回绕。若stayopen参数非0,调用gethostent后文件依然是打开的。
函数endhostent可以关闭文件。
struct hostent {char *h_name; // 主机名char **h_aliases; // 指向轮换主机名阵列的指针int h_addrtype; // 地址类型int h_length; // 地址长度char **h_addr_list; // 指向网络地址阵列指针... };// 返回的地址采用网络字节序
另外两个函数gethostbyname和gethostbyaddr,原来包含在hostent函数中,现在则被认为是过时的。SUSv4已经删除了。
获得网络名字和网络编号
<netdb.h>
struct netent *getnetbyaddr(uint32_t net, int type)
struct netent *getnetbyname(const char *name);
struct netent *getnetent(void);
- 三个函数成功返回指针,出错NULL
void setnetent(int stayopen);
void endnetent(void);
struct netent {char *n_name; // 网络名char **n_aliases; // 轮换网络名阵列指针int n_addrtype; // 地址类型uint32_t n_net; // 网络号... };// 网络编号按照网络字节序返回
协议名字和协议编号之间进行映射
<netdb.h>
struct protoent *getprotobyname(const char *name);
struct protoent *getprotobynumber(int proto);
struct protoent *getprotoent(void);
- 三个函数成功返回指针,出错NULL
void setprotoent(int stayopen);
void endprotoent(void);
struct protoent {char *p_name; // 协议名char **p_aliases; // 轮换协议名阵列指针int p_proto; // 协议数... };
服务由地址的端口号部分表示。每个服务由一个唯一的众所周知的端口号支持
映射端口号
<netdb.h>
struct servent *getservbyname(const char *name, const char *proto);
struct servent *getserbyport(int port, const char *proto);
struct servent *getservent(void)
- 三个函数成功返回指针,出错NULL
void setservent(int stayopen);
void endservent(void);
struct servent {char *s_name; // 服务名char **s_aliases; // 轮换服务名阵列指针int s_port; // 端口号char *s_proto; // 协议名... };
POSIX.1定义了若干新的函数,允许一个应用程序将一个主机名和一个服务名映射到一个地址,或者反之。替代了较老的函数gethostbyname和gethostbyaddr.
<sys/socket.h> <netdb.h>
int getaddrinfo(const char *restrict host, const char *restrict service, const struct addrinfo *restrict hint, struct addrinfo **restrict res);
- 成功0,出错NULL。
void freeaddrinfo(struct addrinfo *ai);
- 需要提供主机名,服务名,或者两者都提供,若仅仅提供一个名字,另外一个必须是一个空指针。主机名可以是一个节点名或点分格式的主机地址。
getaddrinfo 返回一个链表结构addrinfo,可以用freeaddrinfo来释放一个或多个这种结构。
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; // 链表下一个 };
hint 是一个用于过滤地址的模板,包括ai_family、ai_flags、ai_protocol和ai_socktype字段,剩余整数字段必须设置为0,指针字段为空。
- AI_ADDRCONFIG 查询配置的地址类型(IPv4或IPv6)
- AI_ALL 查找IPv4和IPv6地址(仅用于AI_V4MAPPED)
- AI_CANONNAME 需要一个规范的名字(与别名相对)
- AI_NUMERICHOST 以数字格式指定主机地址
- AI_NUMERICSERV 将服务指定为数字端口号
- AI_PASSIVE 套接字地址i用于监听绑定
- AI_V4MAPPED 若没有找到IPv6地址i,则返回映射IPv6格式的IPv4地址
如果getaddrinfo 失败,不能使用perror或strerror生成错误消息,而是要调用gai_strerror将返回的错误码转换成错误消息
- <netdb.h>
- const char *gai_strerror(int error)
- 返回指向描述错误的字符串的指针
- const char *gai_strerror(int error)
- <netdb.h>
将一个地址转换一个主机名和一个服务名
- <netdb.h> <sys/socket.h>
- int getnameinfo(const struct sockaddr *restrict addr, socklen_t alen, char *restrict host, socklen_t hostlen, char *restrict service, socklen_t servlen, int flags)
- 成功0,出错非0
- addr 转换成一个主机名及一个服务名
- host若非空,则指向一个长度为hostlen字节的缓冲区用于存放返回的主机名
- service若非空,则指向一个长度为servlen字节的缓冲区用于存放返回的主机名
- flags
- NI_DGRAM 服务基于数据报而非流
- NI_NAMEREQD 如果找不到主机名,将其作为一个错误对待
- NI_NOFQDN 对于本地主机,仅返回全限定域名的节点名部分
- NI_NUMERICHOST 返回主机地址的数字形式,而非主机名
- NI_NUMERICSCOPE 对于IPv6,返回范围ID的数字形式,而非名字
- NI_NUMERICSERV 返回服务地址的数字形式(端口号),而非名字
- int getnameinfo(const struct sockaddr *restrict addr, socklen_t alen, char *restrict host, socklen_t hostlen, char *restrict service, socklen_t servlen, int flags)
- <netdb.h> <sys/socket.h>
16.3.4 将套接字与地址相关联
- 关联地址及套接字
- <sys/socket.h>
- int bind(int sockfd, const struc tsockaddr *addr, socklen_t len)
- 成功0, 出错-1
- 对于使用的地址存在限制:
- 在进程正在运行的计算机上,指定的地址必须有效,不能指定一个其他机器的地址
- 地址必须和创建套接字时的地址族所支持的格式相匹配
- 地址中的端口号必须不小于1024,除非该进程具有相应的特权(即超级用户)
- 一般只能将一个套接字端点绑定到一个给定地址上,有些协议允许双重绑定。
- int bind(int sockfd, const struc tsockaddr *addr, socklen_t len)
- <sys/socket.h>
- 获取绑定到套接字上的地址
- <sys/socket.h>
- int getsockname(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp)
- 成功0,出错-1
- alenp指定缓冲区sockaddr的长度,返回时会被设置成返回地址的大小。若地址和提供的缓冲区长度不匹配,地址会被自动阶段而不报错。若当前没有地址绑定到该套接字,则其结果未定义
- int getsockname(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp)
- <sys/socket.h>
- 若套接字已和对等方连接,
- <sys/socket.h>
- int getpeername(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp)
- 成功0,出错-1
- 除了返回对等方的地址,其他与上面的函数功能相同。
- int getpeername(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp)
- <sys/socket.h>
16.4 建立连接
- 在请求服务的进程套接字及提供服务的进程套接字之间建立连接
- <sys/socket.h>
- int connect(int sockfd, const struct sockaddr *addr, socklen_t len)
- 成功0,出错-1
- 若sockfd未绑定到一个地址,则会被connect绑定一个默认地址
- 连接时可能会产生一些瞬时错误,因此需要应用代码能够处理这些错误。
- 不同操作系统的实现不同,在重连时为了保持可移植性,连接失败后需要先将套接字关闭再重新获取套接字进行绑定,连接。
- 若套接字处于非阻塞模式,则在连接不能马上建立时,将会返回-1并将errno设置为特殊的错误码EINPROGRESS。应用程序可用poll或select来判断文件描述符是否可写,若可写,则连接完成。
- 可用于无连接的网络服务(SOCK_DGRAM)
- int connect(int sockfd, const struct sockaddr *addr, socklen_t len)
- <sys/socket.h>
- 服务器用于应答是否接收连接请求
- <sys/socket.h>
- int listen(int sockfd, int backlog)
- 成功0,出错-1
- backlog提示系统该进程所要入队的未完成连接请求数量,实际值由系统决定,上限由<sys/socket.h>中SOMAXCONN指定。
- 一旦队列满,系统会拒绝多于的连接请求,因此此参数应该基于服务器期望负载及处理量来选择,其中处理量指接受连接请求与启动服务的数量
- 一旦服务器调用listen,所用套接字就能接收连接请求
- int listen(int sockfd, int backlog)
- <sys/socket.h>
- 获得连接请求并建立连接
- <sys/socket.h>
- int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len)
- 成功返回套接字描述符,出错-1
- 返回的套接字描述符连接到调用connect的客户端。这个新的套接字描述符和原始套接字具有相同的套接字类型和地址族。传给accept的原始套接字没有关联到这个连接,而是继续保持可用状态并接收其他连接请求
- 若不关心客户端标识,可将addr及len设为NULL。否则,在调用accept之前,将addr设为足够大的缓冲区来存放地址,并将len指向的整数设为这个缓冲区的字节大小。返回时,accept会在缓冲区填充客户端的地址,并且更新指向len的整数来反映该地址的大小。
- 若没有连接请求等待,accept会阻塞直到一个请求到来。若sockfd处于非阻塞模式,accept返回-1,并将errno设为EAGAIN或EWOULDBLOCK
- int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len)
- <sys/socket.h>
16.5 数据传输
- 发送
- 可指定标志用于改变处理传输数据的方式
- <sys/socket.h>
- ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags)
- 成功返回发送字节数,错误-1
- 若成功,并不表示连接的另一端已收到数据,但可以保证数据已经被完整无误的发送到网络驱动上
- 使用send时必须保证套接字已连接
- 参数使用与write一致,除了flags
- MSG_CONFIRM 提供链路层反馈以保持地址映射有效
- MSG_DONTROUTE 勿将数据报路由出本地网络
- MSG_DONTWAIT 允许非阻塞操作(等价于使用O_NONBLOCK)
- MSG_EOF 发送数据后关闭套接字的发送端
- MSG_EOR 若协议支持,标记记录结束
- MSG_MORE 延迟发送数据包允许写更多数据
- MSG_NOSIGNAL 在写无连接的套接字时不产生SIGPIPE信号
- MSG_OOB 若协议支持,发送带外数据
- 成功返回发送字节数,错误-1
- ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags)
- <sys/socket.h>
- 可在无连接的套接字上指定一个目标地址
- <sys/socket.h>
- ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags, const struct sockaddr *destaddr,socklen_t destlen)
- 成功返回发送字节数,出错-1
- ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags, const struct sockaddr *destaddr,socklen_t destlen)
- <sys/socket.h>
- 指定多重缓冲区传输数据
- <sys/socket.h>
- ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags)
- 成功返回发送字节数,出错-1
- ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags)
- <sys/socket.h>
- 可指定标志用于改变处理传输数据的方式
- 接收
- 指定标志用于控制接收数据的方式
- <sys/socket.h>
- ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags)
- 成功返回数据字节长度,无可用数据或对等方已按序结束0,出错-1
- 参数同read,除了flags
- MSG_CMSG_CLOEXEC 为UNIX域套接字上接收的文件描述符设置执行时关闭标志
- MSG_DONTWAIT 启用非阻塞操作
- MSG_ERRQUEUE 接收错误信息作为辅助数据
- MSG_OOB 若协议支持,获取带外数据
- MSG_PEEK 返回数据包内容而不真正取出
- MSG_TRUNC 即使数据包被截断,也返回数据包实际长度
- MSG_WAITALL 等待直到所有数据可用
- 若发送者已调用shutdown关闭写端,则返回0
- ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags)
- <sys/socket.h>
- 获取数据发送者源地址
- <sys/socket.h>
- ssize_t recvmsg(int sockfd, void *restrict buf, size_t len, int flags, struct sockaddr *restrict addr, socklen_t *restrict addrlen)
- 返回同recv
- addr为数据发送者的套接字端点地址
- 此函数通常用于无连接套接字,否则等同于recv
- ssize_t recvmsg(int sockfd, void *restrict buf, size_t len, int flags, struct sockaddr *restrict addr, socklen_t *restrict addrlen)
- <sys/socket.h>
- 接收数据至多个缓冲区
- <sys/socket.h>
- ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags)
- 返回同recv
- 标志同sendmsg
- ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags)
- <sys/socket.h>
- 指定标志用于控制接收数据的方式
16.6 套接字选项
- 套接字提供了两个套接字选项接口来控制套接字行为。
- 一个接口用于设置选项
- 一个用于查询选项状态
- 可获取或设置三个选项
- 通用选项,工作在所有套接字类型上
- 在套接字层次管理的选项,但是依赖于下层协议的支持
- 特定于某协议的选项,每个协议独有
- 设置套接字选项
- <sys/socket.h>
- int setsockopt(int sockfd, int level, int option, const void *val, socklen_t len)
- 成功0出错-1
- 参数level标识了选项应用的协议
- 若选项为通用套接字层次选项,设置成SOL_SOCKET;否则设置成控制这个选项的协议编号。对于TCP,level为IPPROTO_TCP,IP,level为IPPROTO_IP
- 其余选项见书P503
- 参数val根据选项不同指向一个数据结构或一个整数
- 若整数非0,则启用;若整数为0,则禁用
- 参数len指定了val指向的对象的大小
- int setsockopt(int sockfd, int level, int option, const void *val, socklen_t len)
- <sys/socket.h>
- 查看选项当前值
- <sys/socket.h>
- int getsockopt(int sockfd, int level, int option, void *restrict val, socklen_t *restrict lenp)
- 成功0出错-1
- 参数lenp是一个指向整数的指针,在调用getsockopt前,设置该整数为复制选项缓冲区长度。若选项实际长度大于此值,则选项会被截断;若实际长度小于此值,则返回时将此值更新为实际长度
- int getsockopt(int sockfd, int level, int option, void *restrict val, socklen_t *restrict lenp)
- <sys/socket.h>
16.7 带外数据
- out-of-band data 带外数据是一些通信协议所支持的可选功能,与普通数据相比,允许更高优先级的数据传输
- 带外数据先行传输,即使传输队列已经有数据。TCP支持带外数据,UDB不支持。
- 套接字接口对带外数据的支持很大程度上受TCP带外数据具体实现的影响。
- TCP的带外数据称为紧急数据(urgent data)。TCP仅支持一个字节的紧急数据,但允许紧急数据在普通数据传递机制数据流之外传输。
- 可在3个send函数中的任一个里指定MSB_OOB标志,若带MSG_OOB标志发送的字节数超过一个时,最后一个字节将被视为紧急数据字节。
- 若套接字安排了信号的产生,那么紧急数据被接收时,会发送SIGURG信号。
- TCP支持紧急标记,即在普通数据流中紧急数据所在的位置。若采用套接字选项SO_OOBINLINE,则可在普通数据中接收紧急数据
- 确定紧急标记是否到达
- <sys/socket.h>
- int sockatmark(int sockfd)
- 若在标记处1,没在标记处0,出错-1
- int sockatmark(int sockfd)
- <sys/socket.h>
- 确定紧急标记是否到达
16.8 非阻塞及异步IO
- recv函数在没有数据可用时会阻塞等待。
- send函数在套接字输出队列没有足够空间来发送消息时会阻塞。
- 在套接字非阻塞模式下,函数不会阻塞而是会返回失败,将errno设置为EWOULDBLOCK或EAGAIN。此情况可使用poll或select来判断能否接收或传输数据
- 在基于套接字的异步IO中,当从套接字中读取数据或当套接字写队列中空间可用时,可发送信号SIGIO
- 启用异步IO的两步骤
- 建立套接字所有权,这样信号可以被传递到合适的进程
- 三种选择
- 在fcntl中使用F_SETOWN
- 在ioctl中使用FIOSETOWN
- 在ioctl中使用SIOCSPGRP
- 三种选择
- 通知套接字当IO操作不会阻塞时发送信号
- 两种选择
- fcntl中使用F_SETFL并且启用文件标志O_ASYNC
- ioctl中使用FIOASYNC
- 两种选择
- 建立套接字所有权,这样信号可以被传递到合适的进程
- 启用异步IO的两步骤
UNIX环境高级编程(APUE)读书笔记相关推荐
- 《Unix环境高级编程》读书笔记 第5章-标准I/O流
1. 引言 标准I/O库由ISO C标准说明,由各个操作系统实现 标准I/O库处理很多细节,如缓冲区分配.以优化的块长度执行I/O等.这些处理使用户不必担心如何使用正确的块长度,这使得它便于用于使用, ...
- (三) 一起学 Unix 环境高级编程 (APUE) 之 文件和目录
. . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...
- 《Unix环境高级编程》学习笔记:从点到面
以前在课堂上学习过<Unix初级教程(第四版)>,对于Unix有了一点了解.由于以后使用的需要,要对它进行比较深入的学习,为此需要阅读不少的书籍,这本<Unix环境高级编程>便 ...
- 《UNIX 环境高级编程》学习笔记—— 标准I/O库
UNIX环境高级编程--标准I/O库 流和 FILE 对象 标准输入.标准输出和标准错误 缓冲 打开流 读和写流 每次一行 I/O 二进制 I/O 定位流 格式化 I/O 临时文件 内存流 流和 FI ...
- 《UNIX 环境高级编程》学习笔记——UNIX 基础知识
UNIX环境高级编程--UNIX 基础知识 引言 UNIX 体系结构 登录 文件和目录 输入和输出 程序和进程 出错处理 用户标识 信号 时间值 系统调用和库函数 引言 所有操作系统都为它们所允许的程 ...
- 《UNIX环境高级编程 3rd》笔记(1 / 21):UNIX基础知识
文章目录 引言 UNIX体系结构 登录 登录名 shell 文件和目录 文件系统 文件名 路径名 工作目录 起始目录 输入和输出 文件描述符 标准输入.标准输出和标准错误 不带缓冲的IO 标准IO 程 ...
- Unix环境高级编程(阅读笔记)----sigaction函数
sigaction函数的功能是检查或修改指定信号相关联的处理动作,此函数取代UNIX早期版本使用的signal函数. [cpp] view plain copy #include<signal ...
- UNIX环境高级编程_学习笔记(一)
Unix 基础. Unix体系结构 1. 由内而外分别为: 系统内核->(系统调用)->Shell | 库函数->应用软件. 2. 用户登录: 用户名称在/etc/passwd文件中 ...
- 5w字总结 Unix系统编程学习笔记(面试向)(Unix环境高级编程/Unix环境程序设计)
文章目录 一.计算 C语言的数据表示与处理 计算 C语言的基本运算操作 内存表和符号表 类型转换 函数类型的分析 指令 复合指令 句法 函数 函数激活(Activation Record) 函数激活定 ...
- 开发日记-20190822 关键词 读书笔记《Unix环境高级编程(第二版)》《掌控习惯》DAY 2
Preface 话说,昨天开始尝试着去改变自己,从基础的习惯开始,11:30准时睡觉,平时差不多12:30才睡觉.按理说,比平时早了一个小时睡觉吧,然后我就把闹钟提前了45分钟,想着还能比平常多睡15 ...
最新文章
- 影响中国发展的七大垂直搜索引擎
- leetcode-9-回文数
- 如何移植.NET Framework项目至.NET Core?
- php数组添加省会城市,【JSON数据】中国各省份省会城市经纬度 JSON
- 推荐16款最棒的Visual Studio插件
- php搬迁安装,【资料搬迁】安装phpunit
- WebStorm 9 配置 Live Edit 功能与浏览器实现同步
- linux 格式化u盘_【Ventoy】一个U盘,启动多个系统
- 用例子看Swift4的GCD
- 例3.3 哈夫曼树 - 九度教程第30题(哈夫曼树)
- javaweb不同用户登录不同页面的页面_ssh+mysql实现的Java web论坛系统源码附带视频指导运行教程...
- android 拍照和选择相册图片剪切
- 工控组态编程相关知识点介绍
- odoo 12: 字段(Fields)
- 空气质量天气质量数据来源整理
- JVM —— Java 对象占用空间大小计算
- 淘宝总知道你要什么?万字讲述智能内容生成实践 | 技术头条
- Mac 修改系统默认Java版本
- 网校搭建完成,课程还需要设计吗?
- 面试官:RocketMQ是什么,它有什么特性与使用场景?