Linux编程从入门到精通_笔记
[TOC]
第一章 LINUX系统概述
1.1.1 操作系统概念
- 操作系统是计算机底层软件,是用户与计算机硬件系统间的接口
使用操作系统的方式:shell和程序

1.1.2 操作系统的基本功能
- 操作系统基本功能
进程管理、存储器管理、输入输出设备管理、文件管理
1.2.2 Linux系统组成
- Linux四部分:
系统内核:提供“系统调用”的接口,包含了诸如进程管理、存储管理等核心功能的处理。
另三个部分通过访问系统调用实现功能
shell:操作系统对用户提供的交互操作的接口
文件系统:管理存储在磁盘设备上的文件
实用工具:是操作系统提供给用户使用的各种工具软件如编辑器,计算器等
1.2.5 内核的组成
- 内核主要由进程调度、内存管理、虚拟文件系统和进程间通信机制等模块组成
- 进程调度:Linux将CPU的资源划分成大小相同的时间片(time slice), 每个占用CPU的进程运行完一个时间片后将释放CPU资源供其他进程使用。
- 三种进程调度策略
分时调度(SCHED_OTHE)、先到先服务的实时调度(SCHED_FIFO)、时间片轮转的实时调度(SCHED_RR) - 虚拟内存
一个程序被凋入内存后,如果其程序、堆栈和数据的总量超过实际内存的大小,内核只把当前使用的程序块保留在内存中,其余的内容则保留在磁盘中。在磁盘中的内容需要执行时,内核再负责在磁盘和内存间交换数据。这里的磁盘就是Linux交换区(Linux swap) - 虚拟文件系统(VFS)
隐藏各种硬件的具体细节和所有的文件系统提供统一的接口 - 进程间通信
Linux支持消息队列、共亨内存、信号量和用于网络环境下进程间通信的套接口、UNIX的System V进程间通信和Linux的POSIX进程间通信
第二章 shell环境
2.1.2 进入shell
- 普通用户$,超级用户#
- echo $PATH查看当前的PATH环境变量信息
- /etc/profile 该文件(初始化脚本文件)设置终端的种类、设置环境变童、设置新建文件的权限掩码、设置自定义的shell命令提示符等

2.2.1 创建和运行shell程序
- 执行shell脚本
chmod:改变文件所有者和模式如读写和执行权限
sh命令:sh 文件路径
2.2.2 环境变量
- 环境变量保存配置信息
- 全局环境变量位于/etc/profile,用户环境变量位于.profile
- 设置环境变量
export 环境变量名=环境变量值
在shell中使用该方法修改环境变量只暂时起作用,shell推出后将失效,而修改用户主目录下的.profile文件环境变量设置将一直有效 - 查看环境变量
shell env命令、echo $环境变量名 - 常见的由系统提供的环境变量

2.2.3 常用shell命令
- 常用shell命令
更改密码:passwd
帮助:man 命令名
列表文件或目录:ls
改变工作目录:cd 路径
复制文件:cp 源路径文件 目标路径
删除文件:rm 文件名 (-f参数强制删除)
修改文件访问权限:chmod 权限信息 文件名
定义别名:alias 自定义名称='命令路径'
2.2.4 管道与重定向
- 管道"|":将一个命令的输出作为另一个命令的输入
- 重定向
>:覆盖目标文件重定向; >> 追加至目标文件末尾重定向
<:输入重定向;<<:here文档操作符。它通知shell当前运行的命令的输入来自命令行,参数由两个占一行的相同的分隔符包含。
2.2.5 shell变量
- shell变量:环境变量"$环境变量名"、自定义变量、系统全局变量

- 创建变量(直接创建)
变量名=值 - 使用变量:$变量名,$0表示程序本身,$1,$2等表示命令行第几个参数
2.2.6 shell运算符
- expr命令进行数学计算,test进行逻辑测试
2.2.7 shell条件控制
- if...then...else...fi;for;while;until
- case用法P24
2.2.8 for循环
for var in list | for var
do | do
处理语句 | 处理语句
done | done
| 无list版var参数来自命令行
- for用法P25
2.2.9 while循环(条件测试查看2.2.6 shell运算符)
while 条件测试
do
处理语句
done
- while用法P26
2.2.10 until循环
until 条件测试
do
处理语句
done
2.2.11 函数
- 函数用法P27
第三章 C语言简介
3.2.8 运算符
- sizeof用于变量时输出变量类型所占字节而strlen输出变量中可见内容的大小,二者用于字符串时sizeof输出字符串容量,strlen输出字符串可见内容大小P43
- 冒泡排序P56
每次排序从第一个数据开始,记录每两个数据中的最值并将最值交换,每进行一趟都排序一个最值那么应当每一趟后都将对一端排序好的数据不再排序
第四章 vi编辑器
4.1.2 vi的操作方式
- vi编辑器三种模式P61
编辑、命令、ex转义
4.2.1 vi编辑器命令
- vi命令
esc:从插入返回到命令
enter:结束ex命令或开始一个新行
/与?:查找
::进入ex模式
退格键:删除字符
ctrl U:将光标移回至本次编辑的起始位罝重新开始编辑。刚刚进行的编辑操作将被取消。
ctrl V:消除下一个键入字符的特殊含义。通常情况下,可以用ctrl V键插入控制字符。例如,要向文件中插入字符“ Esc” ,只需要输入ctrl V键后,按下<£5€>键即可。
ctrl W:把光标移回至上次编辑的单词的第一个字符上。
ZZ:保存并退出
ex命令":x":保存并退出
:q!:不保存强制退出 - 光标移动指令P62
- 滚动屏幕

其余命令请查看书中本章节
第五章 gcc编译器
5.1.1 预编译过程
- 编译:预编译,编译,优化,汇编四个过程
- 预编译过程:
对伪指令(宏定义指令,条件编译指令,头文件包含指令)和特殊符号进行处理生成一个没冇宏定义、没冇条件编译指令、没冇特殊符号的输出文件,-E命令可以输出预编译生成的文件- 宏定义指令:#define/#undef
- 预编译过程:
define name string,用string替换程序中非字符串常量的name,#define紧跟被替换的东西
undef:取消宏定义
- 条件编译指令:#ifdef/#ifndef/#else/#elif/#endif
- 头文件包含指令
- 特殊符号:LINE表示行号,FILE表示当前源程序文件
- 编译过程:
将预编译产生的文件翻译成中间代码或汇编代码,-S可输出汇编代码文件 - 优化
- 对中间代码的优化
删除公共表达式、循环优化、无用值的删除等 - 对目标代码的生成进行优化
与计算机的硬件结构密切相关。如何根据不同计算机体系结构,充分利用计算机的各个硬件特点,提高代码的执码的执行效率。 - 优化方法
-O编译器优化选项:设定优化级别高到低为O3/O2/O1,一般O2
-march选项:为特定的CPU类型编译二进制代码,如果编译时指定了该选项,则编译出的代码不能在比march指定的级别低的CPU上运行
- 对中间代码的优化
- 汇编
代码经过优化后,再经过汇编程序的翻译,即可生成可为计算机系统所识別的二进制代码。汇编的过程实际上是将汇编语言代码翻译成为机器语言的过程。经过汇编生成的代码实际上只是目标代码,并不能直接执行。要想将目标代码变成可执行的程序,还需要进行链接的操作。
5.2.1 链接过程
- 链接
链接过程将函数库(.a文件)中相应函数的代码组合到目标文件中
静态链接:把各种函数库中包含的目标代码静态添加到可执行文件中。
动态链接:将函数库的路径、函数名等信息添加到可执行文件中,在执行过程中动态加载相应函数库。
5.3.1 程序编译链接过程
- 编译链接过程

预编译(生成.e)->编译生成汇编代码(.s)->汇编将汇编代码转换为目标代码(.o)
5.4.1 gcc常用语法
- gcc编译常用命令
-c:只编译不链接,生成同名.o文件
-o:链接生成可执行文件。该选项可以附加生成的可执行文件名称,缺省名为a.out
-g:产生符号调试工具(gdb)所必要的符号信息。
-O:编译优化选项
-I dirname:将dirname所指目录加入到头文件搜索目录列表中
-L dirname:将dirname所指目录加入到函数库搜索目录列表中
-lname:链接过程中加载"libname.a"的函数库
gcc -c -O -I dimame x.c//编译
gcc -L dirname -lyyy x.o -x//链接生成可执行文件x
5.4.3 生成动态链接库
- 生成动态链接库.so文件
从目标文件生成
gcc -fPIC -c xxx.c
gcc -shared -o libxxx.so xxx.o
或
从源文件生成
gcc -fPIC -shared func.c -o libfunc.so
5.4.4 使用动态链接库
- 设置LD_LIBRARY_PATH环境变量
设置环境变量LD_LIBRARY_PATH为动态链接库文件所在的目录即可,如LD_LIBRARY_PATH=/home/sharedlib,也可以在.profile中使设置长期有效 - dlopen系列系统调用函数打开动态链接库
dlopen:打开动态库,可以在函数中直接指定动态库的路径名
dlsym:将动态库中的函数映射到本地函数指针
dlclose:完成动态库的卸载
第六章 make的使用
6.1 makefile介绍
- 将一个具有特定文件名后缀的文件(如.c文件),转换成为具有另一种文件名后缀的文件(如.0 文件)的规则
.c:.o
$(CC)$(CFLAGS)$(CPPFLAGS) -c -o $@$<
- makefile可以用宏指代相关源文件及编译信息,使用时在宏前加$符号,若要使用真实的$则用$$表示,宏长度大于一个字符该宏需用()
6.2.1 基本语法规则
Targets:Prerequisites
Command
Targets:目标文件名(需要得到的文件如可执行程序、目标代码等见6.2.7),以空格分隔
Prerequisites:目标所依赖的文件
Command:命令行,\可用作换行符
6.2.2 定义变量
- makefile变量与C语言中的宏类似,代表一个字串,在执行时被替换
6.2.3 环境变量
- VPATH:在当前的目录中找不到依赖文件和目标文件时转而在VPATH指定目录中寻找,目录间用:分隔
- CFLAGS:c语言编译器参数
- LDFLAGS:链接器参数
6.2.5 条件判断
<conditional-directive>
<text-if-true>
else
<text-if-false>
endif
conditional-directive有四个选项:ifeq,ifneq,ifdef,ifndef
6.2.6 函数
- 使用函数
$(<函数名称><函数参数1><函数参数2><...>)
名称与参数用空格分隔
- 常用函数
substr(str1,str2,text):将text中的str1替换为str2
strip(string):将string前后空格去掉
findstring(find,in):在字串in中查找find
sort(string):排序并去除重复字符
dir(路径名):取目录
basename(文件名):取文件名,返回内容只有文件名无文件后缀
orgin(变量名):变量来源函数,来源可能是Undefined未定义、Default默认变量定义、File在makefile中定义的变量、Command line在命令行中定义的变量
6.2.7 实例

6.4.2 综合实例
- 伪目标
- 变量
第七章 程序调试
7.1.1 标准错误输出
- 在linux系统的编程环境下,每个运行的进程默认都将打开3个文件描述符:标准输入、标准输出及标准错误输出(stderr),stderr文件描述符为2,stderr写入的内容将显示在屏幕上
fprintf(stderr,"error information");
7.1.2 errno全局变量
- 在linux编程模式中,系统预先定义了全局变量errno(位于errno.h)。每当调用函数或者系统调用失败时,由系统内核将一个整形的错误代码保存至errno,每个errno值对应一个错误信息字符串
- 格式化错误信息函数:perror(stdio.h),strerror
void perror(_const char* _s);//输出上一个函数发生错误的原因
char* strerror(int _errnum);//返回_errnum对应的错误信息,_errnum可以直接用errno
perror是将错误信息输出到终端,而strerror返回errno对应的错误信息字串的指针,用此指针将错误信息输出到缓冲区
7.1.3 错误信号处理
- signal函数:singal(signalname,handel_function);
程序出错时系统内核将向进程发送signalname错误信号,可调用handel_function处理此信号
7.1.4 assert断言
- void assert(int expression);
某个条件表达式的值或者某个变诅的值,此时可以对该变量或者条件表达式使用断言。一旦该值与所预想的不符,程序将输出错误信息后结束运行
7.1.5 内存泄漏
- 申请的内存未释放
- 打开的文件未关闭(打开文件后在内核会分配内存区用于存储文件信息)
- 检查内存泄漏方法
ps命令查看内存是否动态持续增长(VSZ项表示进程占据内存的大小),通过代码分析工具对代码进行分析判断是否存在内存泄漏
7.1.6 其他内存错误
- 未初始化的指针
- 内存溢出,写入数据大小超出定义的变量空间大小
7.2.1 gdb调试的前提
- 编译过程中使用-g选项,以生成gdb工作所必需的调试信息
- 要调试的可执行程序需要有源代码,并且通常源代码应该放在与可执行文件相同的目录下
7.2.3 gdb基本功能
- gdb命令
file:装入想调试的可执行文件
kill:终止正在调试的程序
list/next:执行一行源代码但不进入函数内部
step:执行一行源代码且进入函数内部
run:执行当前被调试的程序,可以想程序传递命令行参数
如"run example a b",a,b是命令行参数,set args可修改程序运行时的命令行参数,如set args -b -x,未指定命令行参数时使用上一次调用时的参数,show args査看当前凋试程序的命令行参数
print:显示变量的值,还可以直接调用程序的函数或者显示程序中复杂的组合数据类型的值
whatis:显示变量的数据类型
break:设置断点
break line-number:使程序恰好在执行给定行之前停止
break function-name:使程序恰好在进入指定的函数之前停止
break line-or-function if condition:如果condition是真,程序到达指定行或函数时停止
break routine-name:在指定例程的入口处设置断点
info break:显示当前断点信息
delete breakpoint:删除断点
不带参数的delete命令删除所有断点,可以在delete breakpoint后面跟上断点序号单独删除某个断点。可以通过命令disable breakpoint和enable breakpoint临时禁用断点,在适当的时候再启用断点。
make:不退出gdb重新产生可执行文件
shell:不退出gdb执行shell命令
第八章 创建与使用库
8.1.1 系统函数库
- 函数库由目标文件组成
- 查看函数库内容命令 ar tv libname.a
- 查看目标文件包含的函数命令
ar x libname.a objectname.o //将目标文件从函数库分离
nm objectname.o //查看目标文件提供的函数
8.1.2 自定义函数库
- 功能函数源代码(.c)编译生成目标文件(.o),再将目标文件打包成函数库,打包命令为 ar c/r (c表示创建,r表示追加)
- 例子
有string1.c和string2.c,使用他们生成函数库libmystring.a
首先编译string1.c并创建函数库
cc -c -O string1.c
ar cv string1.o libmystring.a //c创建v显示详细信息
然后编译string2.c并追加到libmystring.a中
cc -c -O string2.c
ar rv string2.o libmystring.a
8.2.1 系统调用
- 系统凋用和普通的函数调用非常相似。它们的区别在于系统调用由操作系统内核提供,运行于核心态;而普通的函数调用由函数库或用户自己提供,运行于用户态
- 通常情况下,进程是不能访问内核的。它不能访问内核所占内存空间也不能调用内核函数。这是由CPU的硬件保护措施决定的,也就是所谓的“保护模式”
- 自陷
系统调用的原理是进程先用适当的值填充寄存器,然后调用一个特殊的指令,这个指令会跳到一个事先定义的内核中的一个位置,这个位置是用户进程可读但是不可写的。这一过程称为“自陷” 。通过自陷机制,用户的进程就可以访问在核心态运行的内核系统调用 - 在调用函数库时,需要在链接时指定要链接的库文件。而系统凋用是不需要执行与相关函数库链接的
- 创建动态库
编译时指定-fPIC 通知gcc编译器产生可以重定位的与位置无关的目标代码
链接时指定-shared选项。通过这一选项,使gcc编译器生成动态链接库 - 使用动态库
设置环境变量LD_LIBRARY_PATH
dlopen系列函数 - dlopen系列函数
声明位于dlfen.h中- dlopen原型为void* dlopen(const char* _file, int _mode);
_file:动态链接库文件绝对/相对路径,若使用库文件名则根据LD_LIBRARY_PATH值查找库文件
_mode:库文件打开方式,RTLD_LAZY:只打开动态库文件,而暂时不解析外部对动态库的引用,直到动态库函数被执行时再解析对动态库的引用;RTLD_NOW:打开动态库文件,在dlopen返回前解析所有的对动态库的引用
返回值:NULL表示调用dlopen失败可用dlerror获取错误信息;返回其他表示调用成功,返回访问动态链接库的句柄(可赋给void*类型的指针) - dlsym原型为void* dlsym(void* _restrict_handle, const char* _restrict_name);
_handle:动态链接库句柄,由dlopen函数返回
_name:要调用动态链接库中函数的名称
返回值:NULL表示调用dlsym失败可用dlerror获取错误信息;返回其他表示调用成功,返回要调用函数的指针,可定义一个函数指针获取该返回值(根据函数原型定义函数指针如函数double functionname(double,double),则相应的函数指针ptr_callfunction为double(*ptr_callfunction)(double,double)) - dlclose原型为int dlclose(void* _handle);
_handle:动态链接库句柄,由dlopen函数返回
返回值:0表示调用dlclose成功,其他表示调用失败 - dlerroe原型为char* dlerror(void);
返回调用dlopen系列函数时的详细错误信息
- dlopen原型为void* dlopen(const char* _file, int _mode);
- dlopen系列函数调用动态库实例P107


第九章 文件操作
9.1.1 逻辑磁盘分区
- fdisk -l查看磁盘逻辑分区
dev/sda1:s表示移动存储设备,a/b表示第一/二块磁盘,1/2表示该盘第一/二个分区
9.1.2 文件系统建立
- 分区后对逻辑分区格式化建立文件系统
mkfs.fstype建立fstype类型的文件系统,如mkfs.ext2建立ext2文件系统
在sda5建立文件系统:mkfs.ext2 /dev/sda5 - 挂载文件系统
挂载,就是将逻辑磁盘分区与文件系统的指定目录之间建立关联
可以执行mount命令进行手工挂载和通过fstab文件进行自动挂载 - mount挂载适用于光盘等不经常使用的存储设备,也可用于磁盘分区挂载,umount卸载文件系统
- 在linux启动过程中,init进程会自动读取文件/etc/fstab中的配置内容挂载相应的文件系统
- 例子
安装光盘cdrom到/mnt/cdrom
mount -t iso9660 -o ro /dev/cdrom /mnt/cdrom //-t指定文件系统类型,-o指定文件系统读写权限ro只读
- df -T查看当前已安装文件系统,-T表示输出文件系统类型
- / 表示根目录,其他所有目录位于其下层且linux系统只有一个目录树
- 查看硬盘->硬盘分区->给分区建立文件系统->将建立好文件系统的分区挂载到指定目录
9.1.3 虚拟文件系统
- 为用户程序提供一个统一的、抽象的、虚拟的文件系统界面,用一种统一的接口对文件系统进行管理
9.1.4 ext2文件系统
- ext2第二代扩展文件系统(FAT表示文件分配表类型)
除存储文件数据外,还保存文件系统结构信息,如引导块、索引节点块、目录块,这些信息描述了系统中的每个文件的占用数据块的指针以及文件的存取权限、文件的修改时间和文件类型等信息 - 每个文件都只有一个索引节点,且每个索引节点都有一个惟一的标识符。目录块专门用来存储目录的数据信息。在linux系统中,目录被视为一种特殊的文件。这种文件中包含指向目录入口的索引节点的指针(l -i可显示i节点编号信息,位于输出第一列)
9.1.5 文件类型
- linux文件的扩展名与文件类型没冇必然的联系,文件类型主要与文件的属性有关
- 具体文件类型
- 普通文件:不包含有文件系统的结构信息。通常所接触到的文件,包括图形文件、数据文件、文档文件、声音文件等都属于这种文件
- 目录文件:用于存放文件名及其相关信息的文件,是内核组织文件系统的基本节点。目录文件可以包含下一级目录文件或普通文件
- 设备文件:系统通过设备文件访问物理设备,系统为外部设备提供一种标准接口,系统将外部设备视为一种特殊的文件。通常情况下,Linux系统将设备文件存放在/dev目录下,设备文件使用设备的主设备号和次设备号来指定某外部设备
- 链接文件:链接文件是一种特殊的文件,实际上是指向一个真实存在的文件的链接,类似于Windows下的快捷方式
- 管道文件(Named Pipes/FIFO):管道文件是linux系统中另一种很特殊的文件,主要用于不同进程间的信息传递。
- 查询文件信息时输出-/d/b/l/p分别表示普通文件、目录文件、符号链接文件、管道文件,rw表示读写权限
- 文件链接分为硬链接及符号链接两种。所谓硬链接,就是链接文件与被链接文件物理上是同一个文件。对文件进行硬链接后,只是增加了文件的引用计数,并没有物理上增加文件。硬链接是通过执行命令ls完成的。符号链接是一个物理上真实存在的文件,但是该文件的内容是指向被链接文件的指针。符号链接文件与原文件的i节点编号是不同的。符号链接通过执行命令ln -s完成
9.1.6 文件权限管理
- 传统文件权限管理UGO(user group other)、访问控制列表方式的权限管理ACL(Access Control List)
- UGO方式9个权限控制位,3种权限r/w/x读写与执行
l命令显示文件信息
-rw-r--r-- 表示普通文件,root用户rw权限,与root用户组的用户和其他用户r权限
- chmod修改filename权限
- 字母形式:chmod g+r filename u表示文件拥有者,g表示组内用户,o表示其他用户,a表示三者皆是,+表示增加权限、-表示取消权限、=表示唯一设定权限
- 数字形式:chmod 三位八进制数表示三种用户权限赋予,如chnod 751 filename八进制为111/101/001表示文件拥有者可rwx,组内用户rx,其他用户x
- 新建文件权限:访问环境变量UMASK得到文件权限掩码,可通过umask命令进行修改,在新建文件时,系统将UMASK值按二进制位取反,与指定的文件权限位进行与(AND)操作,以此来决定文件最终的权限,默认方式下,umask通过八进制数字的方式来定义用户创建文件或目录的权限掩码
- ACL权限项

9.2.1 文件编程基本概念
- 原始文件I/O编程模式及标准I/O库编程模式。原始文件I/O模式是指linux系统提供的文件编程API,是一种不带缓存的文件I/O,不带缓存是指对每个文件操作函数都调用系统内核的一个系统调用,标准I/O库则是带缓存的文件1/0。标准 1/0代符用户进行许多缓存细节方面的处理,如缓存的分配、执行1/0操作时优化缓存长度等
- 文件描述符:非负整数,0-OPENMAX,当进程创建一个新文件或者打开现冇的文件时,系统内核将向进程返回一个文件描述符。文件描述符在进程范围内是惟一的,并且每个进程可以同时打开的文件数目不能大于OPENMAX,宏定义OPENMAX值可以用ulimit -n查看
- 在LinUx进程启动时,内核默认为每个进程打开3个文件:标准输入文件、标准输出文件、标准错误输出文件。文件描述符分別是0、1、2,分别被映射到键盘,屏幕,屏幕,它们的宏定义:STDIN_FILENO,STDOUT_FILENO,STDERR_FILENO
- 如果文件是可执行的程序,那么当该文件被执行时就会在操作系统中形成进程。每个进程都冇一个有效用户ID和一个有效组ID。这两个与进程相关联的ID的作用是进行文件存取许可检査。除此之外,每个进程还有另外两个ID ,分別称作实际用户ID和实际组ID 。这两个ID 就是执行该程序用户的用户ID及组ID。
- 通常情况下,进程的有效用户ID就是实际用户ID,进程的有效组ID就是实际组ID,但是,Linux提供了一种特殊的机制,可以在文件的属主权限中设置一个标志。这个标志的作用是:当执行该文件时,将进程的有效用户ID设置为该文件所有者的用户ID,如果设置了该标志,那么当一个文件被执行时,其有效用户ID(用于文件存取许可检査)不再是调起该进程的用户,而是该文件的属主ID。这种机制就称作设置-用户-ID , 也称作是suid,类似的也有设置-组-ID,sgid,suid可以用chmod u+s完成,sgid用chnod g+s,设置suid相当于以管理员身份打开或执行,此时root权限为rws
- 文件创建
- creat系统调用函数:原型int creat(const char* pathname,mode_t mode); pathname文件路径,mode文件访问权限(S_IRUSR,S_IWUSR文件拥有者读写,S_IRGRP组内用户读,S_IROTH其他用户读)
- open函数:原型int open(const char* pathname,int oflag,.../*mode_t mode*/);oflag打开文件的方式,mode可选参数,只在创建新文件时有效,设置文件访问权限

- 文件打开错误处理

9.2.3 文件读写
- read函数
原型:size_t read(int filedes, void* buff,size_t nbytes),参数为文件描述符、缓冲区、要读取的字节数
若成功,返回实际读取的字节数。如果已读到文件尾,则返回0,若出错,返回-1,read系统调用不仅仅应用于读取文件,而且用于读取套接字及设备文件,如果read在读到要求的字节数(由参数nbytes指定)前到达了文件末尾,则将返回已读取到的数据。此时,返回值小于nbytes参数值,read操作将自动增加文件的读写指针。在文件刚打开时,文件夹的读写指针位于文件开始处(除非指定0_APPEND标志) - sizeof操作符
对于一个字符数组来说,调用sizeof的结果是数组空间的尺寸,包含结束符"\0"。而对于字符指针来说,用sizeof获取的是指针本身的尺寸,如int a[]="123456",sizeof(a)=7,int char* p="123456",sizeof(p)=4
sizeof是获取缓冲区的长度的运算符,即使缓冲区无任何数据 (全为空\0),sizeof也可以取得其长度。而strlen()是取得字符串的长度的函数,注意字符串是以\0为结束符的。strlen()在计算长度时不会包含结束符本身,sizeof则包括了\0的长度。 - write函数
原型:size_t write(int filedes, const void* buff,size_t nbytes)
9.2.4 文件的关闭与删除
- close函数
原型:int close(int filedes) - unlink删除函数
原型:int unlink(const char* path)
删除文件只是将文件的引用计数减一。只有在文件的引用计数变为零的情况下,才会物理删除一个文件
9.2.5 文件随机存取
- lseek函数(调整当前文件位移(从文件开始处到文件读写指针位置的字节数))
原型:off_t lseek(int fd,off_t offset,int whence),offset表示相对whence的偏移量
lseek(int fildes,0,SEEK_SET)//将当前文件位移转到文件头
lseek(int fildes,0,SEEK_END)//将当前文件位移转到文件尾
lseek(int fildes,0,SEEK_CUR)//获取当前文件位移
文件位移可以大于文件的当前长度。在这种情况下,对该文件的下一次写将延长该文件,并在文件中构成一个空洞。位于文件中但没有写过的字节都被设置为'\0'。
9.3.1 文件属主及用户组编程
- int chown(const char* pathname,uid_t owner,gid_t group)(修改文件所有者)
- int lchown(int fd,uid_t owner,gid_t group)(修改符号链接本身的属主而不是所指向的文件)
- int fchown(const char* pathname,uid_t owner,gid_t group)(同chown,但修改已打开文件的所有者和权限模式)
- 调用chown的进程冇效用户标识符必须是超级用户或者文件拥有者,否则将失败
9.3.2 设置文件权限
- int chmod(const char* pathname,mode_t mode);
- int fchmod(int fd,mode_t mode);
- 通过umask为进程设置文件权限屏蔽字(默认值0022)。所谓文件屏蔽字,就是指一旦在该调用中指定了某项访问权限,则该进程所创建的文件不允许拥有此项权限
mode_t umask(mode_t mask);//返回值是调用umask前的文件权限屏蔽字 - 获取一个文件的访问权限
int access(const char* pathname,int mode);//mode可用或运算符表达R_OK,W_OK等,当前进程具有所测试的权限,则返回0 - 调用chmod更改文件权限信息时,修改的是i结点的数据结构,并没有修改文件内容。在修改完成后,通过执行1命令可以看到文件的修改时间并没冇变化
- 设置文件权限ACL模式P142

- 设置文件ACL信息
int acl_set_fd(int fd,acl_t acl);
int acl_set_file(const char* path_p,acl_type_t type,acl_t acl);
参数type修改权限的方式,acl_t数据结构

- 获取文件ACL信息
acl_t acl_get_fd(int fd);
acl_t acl_get_file(const char* path_p,acl_type_t type); - 创建权限项
int acl_create_entry(acl_t* acl_p,acl_entry_t* entry_p)
将acl_p中的权限项赋给entry_p,参数entry_p新建权限项的指针,数据结构为

- 每个权限项都包含了 tag、qualitifier、permset过三方面内容。在创建一个新的权限项后,调用其他函数对这3项内容进行填充。最后调用acl_set_file进行权限设置
- acl_set_tag_type,acl_set_qualifier,acl_add_perm,acl_get_permset,acl_set_permset
设置权限方式,设置权限的对象及权限信息
acl_get_permset(acl_entry_t entry_d,acl_permset* permset_p);将entry_d中的权限信息赋给permset_p
acl_set_qualifier(acl_entry_t entry_d,const void* tag_qualifier_p)为tag_qualifier_p指定要变化权限的用户ID
原型:

- 调用acl_set_file进行权限设置

- ACL权限设置总结:acl_t包含acl_entry_t,acl_entry_t包含acl_permset_t三个数据结构依次包含,分别是基本数据结构,权限项和权限信息;首先获取当前acl信息、权限项和权限信息,然后改变权限信息和设置权限方式,指定改变用户,最后设置acl信息
9.4.1 获取文件状态信息
- 文件状态信息获取系统调用stat/fstat/lstat
原型:int stat(const char* file,struct stat* buf);
int fstat(int fd,struct stat* buf);
int lstat(const char* file,struct stat* buf);
stat是文件状态信息结构体P145 - 文件系统状态信息获取系统调用statfs
原型:int statfs(const char* file,struct statfs* buf); - 修改文件时间utime(存取时间,修改时间(文件内容变化的时间),状态更改时间(文件属性变化的时间))
原型:int utime(const char* file,const struct utimbuf* times)
存取时间和修改时间可以通过系统调用utime进行维护,修改文件属性操作将会改变文件状态更改时间
9.5.1 目录编程
- mkdir目录创建
原型:int mkdir(const char* path,mode_t mode)
参数:path要创建的目录名称,mode目录权限,新建目录的实际权限是由mode参数与文件权限掩码umask做与(AND)操作的结果 - rmdir目录删除
原型:int rmdir(const char* path)
只有在目录中除"."和 ".." 外,没有其他文件时,才能删除目录,非空目录unlink删除 - chdir/fchdir变更当前工作目录
- getcwd获取当前工作目录
原型:int chdir(const char* path);
int fchdir(int fd);
char* getcwd(char* buf,size_t size);
参数:path要变更的目标目录,fd要变更到的目标目录文件描述符,buf保存得到的工作目录名称缓存,size缓存大小 - 目录读取
DIR* opendir(const char* pathname);
struct dirent* readdir(DIR* dp);
参数:DIR结构是linux内核的目录文件结构定义,readdir返回的dirent结构里保存着目录中下一个目录项的相关数据
void rewinddir(DIR* dp);//重定位目录的读指针,将目录的读指针重新返回至第一个目录项
int closedir(DIR* dp);
第十章 标准输入输出库
10.1.2 缓存
- 标准IO缓存方式
全缓存:全缓存是指只有当前IO操作的缓存被填满时,才会向文件系统进行刷新。大多数的标准IO函数都是基于这种缓存方式的。
行缓存:行缓存是指在输入/输出过程中,如果遇到换行符,则向文件系统进行刷新。后面将要介绍的行读写函数是以行缓存方式进行的。另外,在向标准输出(STDOUT)输出信息时,也是以行缓存的方式进行的
不缓存:不缓存是指不设缓存机制的标准IO。在某些情况下是不适宜设置缓存的,如标准错误输出就是不缓存的IO操作。 - 除由系统自动进行缓存刷新操作外,也可以随时用fflush系统调用手工刷新缓存
- 改变流的默认缓存方式函数
void setbuf(FILE* _ restrict_stream, char* __restrict_buf);
void setbuffer(FILE* _restrict_stream, char* __restrict_buf,set_t size);
void setlinebuf(FILE* _stream);//设置某个流为行缓存模式
void setvbuf(FILE* _restrict_stream, char* __restrict_buf,int _modes,set_t size);
参数:_stream:输入参数,已打开的流对象;_buf:输入参数,自定义缓存区指针,如果该参数为空,表示设置流对象为无缓存的模式。对于setbuf来说,该缓存区的大小固定为BUFSIZ 。而对于setbuffer该缓存区大小可以自行定义,在参数size中指定了缓存区大小
10.1.3 标准输入输出及错误输出
- 输入输出重定向
<输入重定向;>/>>输出重定向
ls>/temp/a命令将当前目录的文件列表输出至文件/temp/a - dup2(fileno(fp),fileno(stdout))复制目标文件fp的描述符至标准输出stdout,即将标准输出指向目标文件
10.2.1 打开关闭流文件
- 打开关闭流文件
FILE* fopen(_const char* _restrict_filename,_const char* _restrict_modes);
int fclose(FILE* stream);
10.2.2 单字符读写
- 单字符读写
int fgetc(FILE_stream);
int getc(FILE_stream);
int fputc(int _c,FILE_stream);
int putc(int _c,FILE _stream);
int getchar(void);
int putchar(int _c);
参数_c要向流中写入的字符
10.2.3 行读写
- 行读写
char* fgets(char* _restrict_s, int _n,FILE* _restrict_stream);//将读取指定长度的数据至缓存区,并且在数据的最后附加字符串结束符'\0'
int fputs(_const char* _restrict_s,FILE* _restrict_stream);
参数_s读写缓冲区,n读取字节长度 - 标准输入输出设备读写
char* gets(char* _s);
int puts(_const char* _s);
函数puts向标准输出设备写入数据后,会附加写入一个换行符。而gets函数从标准输入读取一行数据时,并未对缓冲区_s的长度是否足够进行检査。如果从键盘输入过长的数据,将导致不可预料的结果。因此,在实际编程过程中,经常用fgets和fputs代替对gets和puts的使用
10.2.4 二进制读写
- 二进制读写
通常对数据结构进行读写(常规读写不能读写结构到文件)
size_t fread(void* _restrict_ptr,size_t _size,size_t _n,FILE* _restrict_stream);
size_t fwrite(_const void* _restrict_ptr,size_t _size,size_t _n,FILE* _restrict_s);
上述函数读取一条数据(记录)如一个结构体
参数_ptr读写对象,size要读取的每个记录的长度,n读取记录的个数
10.2.5 格式化输入输出
- 格式化输入输出

- %[^a]读取非a的所有字符
char buf[128] = "aaa,bbb,ccc,ddd";
sscanf(buf,"%[,],%[,],%[^,],%s",buf1,buf2,buf3,buf4);
sscanf分隔字符 - 格式控制符P170
10.2.6 在流文件中定位
- 定位函数
int fseek(FILE* _stream,long int _off,int _whence);//定位文件读写指针
long int ftell(FILE_stream);//获取文件当前读写指针
void rewind(FILE _stream);//定位文件指针到文件头部
第十三章 进程
13.1.1 获取进程的属性
- 可执行的程序被系统加载到内存空间运行时,就称为进程
- 进程属性P195
- 进程标识符
- 进程的父进程标识符
- 进程的用户标识:运行该程序的用户ID
- 进程的组标识:指运行该程序的用户所归属的组ID
- 进程的有效用户标识:是指该进程运行过程中有效的用户身份
- 进程的有效组标识:是指当前进程的有效组标识
- 进程的进程组标识符:一个进程可以厲于某个进程组。通过设罝进程组,可以实现向一组进程发送信号等进程控制操作。
- 进程的会话标识符:每个进程都属于惟一的会话
- ps命令可以查看进程属性
- 编程获取进程属性
_pid_t getpid(void);//获取当前进程的进程ID
_pid_t getppid(void);获取当前进程的父进程ID
_pid_t getpgrp(void);//获取当前进程的进程组ID
_uid_t getuid(void);//获取当前进程的实际用户ID
—uid_t geteuid(void);//获取当前进程的有效用户ID
_gid_t getgid(void);//获取当前进程的实际用户组ID
_gid_t getegid(void);//获取当前进程的有效用户组ID
_pid_t getsid(_pid_t _pid);//获取指定进程的会话ID
13.1.2 进程的内存映像
- 进程主要包括代码段、数据段、BBS段、堆栈段等部分
- 代码段是用来存放可执行文件的指令,是可执行程序在内存中的映像。对代码段的访问有严格安全检查机制,以防止在运行时被非法修改。代码段是只读的
- 数据段用来存放程序中己初始化的全局变量
- BBS段包含程序中未初始化的全局变量
- 堆是进程空间内的内存区,用于进程运行过程中动态分配内存
- 栈是存放程序中的局部变量的内存区。另外,栈还被用于保存函数调用时的现场。在调用函数时,函数的参数被压入栈,由函数从栈中取出。在函数返回时,将返回值压入栈,由主调函数从栈中取出。栈是由系统自动分配的,用户程序不需要关心其分配及释放

13.1.3 进程组
- setpid修改进程的进程组
原型:int setpgid(_pid_t _pid,_pid_t _pgid);
参数:_pid:输入参数,用于指定要修改的进程ID。如果该参数为0, 则指当前进程ID;_pgid:输入参数,用于指定新的进程组ID。如果该参数为0, 则指当前进程ID。
13.1.4 进程的会话
- 当用户登录一个新的shell环境时,一个新的会话就产生了。一个会话可以包括若干个进程组,但是这些进程组中只能有一个前台进程组,其他的为后台运行进程组。
- 前台进程组通过其组长进程与控制终端相连接,接收来自控制终端的输入及信号。
- 一个会话由会话ID来标识,会话ID是会话首进程的进程ID。
- setsid产生新会话
原型:_pid_t setsid(void);//返回进程的进程组ID
调用setsid的进程应该保证不是某个进程组的组长进程。
setsid调用成功后,将生成一个新的会话。新会话的会话ID是调用进程的进程ID。新会话中只包含一个进程组,该进程组内只包含一个进程:即调用setsid的进程,且该会话没有控制终端
13.1.5 进程的控制终端
- linux系统中,每个终端设备都有一个设备文件与其相关联,这些终端设备称为tty,可以通过执行命令tty査看当前终端的名称
- 控制终端,就是指一个进程运行时,进程与用户进行交互的界面。一个进程从终端启动后,这个进程的运行过程就与控制终端密切相关。可以通过控制终端输入/输出
- ps -ax查看进程控制终端
13.1.6 进程的状态
- 进程是由操作系统内核调度运行的。在调度过程中,进程的状态是不断发生变化的。这些状态主要包括可运行状态、等待状态(也称为睡眠状态)、暂停状态、僵尸状态、退出状态等

- 状态含义
- 可运行状态(RUNNING):该状态有两种情况,一是进程正在运行;二是处于就绪状态,只要得到CPU就可以立即投入运行。在第二种情况中,进程处于预备运行状态,在等待系统按照时间片轮转规则将CPU分配给它
- 等待状态(SLEEPING):表明进程止在等待某个事件发生或者等待某种资源。该状态可以分成两类:可中断的和不可中断的。处于可中断等待状态的进程,既可以被信号中断,也可以由于资源就续而被唤醒进入运行状态。而不可中断等待状态的进程在任何情况下都不可中断,只有在等待的资源准备好后方可被唤醒
- 暂停状态(STOPPED):进程接收到某个信号,暂时停止运行。大多数进程是由于处于调试中,才会出现该状态
- 僵尸状态(ZOMBIE):表示进程结束但尚未消亡的一种状态。一个进程结束运行退出时,就处于僵尸状态。进程会在退出前向其父进程发送SIGCLD信号(关于该信号的处理见第14章内容)。父进程应该调用wait为子进程的退出做最后的收尾工作。如果父进程未进行该工作,则子进程虽然已退出,但通过执行ps命令仍然可以看到该进程,其状态就是僵尸状态。在应用编程中,应尽童避免僵尸进程的出现
- top命令可以持续的动态的输出进程的信息
13.1.7 进程的优先级
- 时间片轮转
时间片通常很短,以毫秒甚至更小的时间级別为单位。系统核心依照某种规则,从大景进程中选择一个进程投入运行,其余的进程暂时等待。当正在运行的那个进程时间片用完,或进程执行完毕退出,Linux就会重新进行进程调度,挑选下一个可用进程投入运行 - 优先级的数值越低,进程越是优先被调度,优先级别(PR值)和谦让值(NI)相加以确定进程的真正优先级别
- 对于一个进程来说,其优先级別是由父进程继承而来的,用户进程不可更改
- nice系统调用修改进程自身的谦让值(缺省值为0)
原型:int nice(int _inc);
系统允许的谦让值范围为最高优先级的-20到最低优先级的19
只有超级用户可以在调用nice时指定负的谦让值。也就是说只冇超级用户才可以提高进程的调度优先级别。如果不是超级用户而指定负的谦让值,则nice调用返回失败,errno为EPERM - setpriority修改其他进程甚至一个进程组的谦让值
原型:int setpriority( _priority_which_t _which, id_t _who, int _prio);
参数:
which谦让值的目标类型,PRIO_PROCESS是为某进程设置谦比值;PRIO_PGRP是为某进程组设置谦让值;PRIO_USER是为某个用户的所有进程设置谦让值;
who对于PRIO_PROCESS类型的目标,其为进程ID;对于PRIO_PGRP类型的目标,其为进程组ID;对于PRIO_USER类型的目标,其为用户ID。如果该参数为0,对于3种目标类型,分別表示当前进程、当前进程组、当前用户。
prio要设置的谦让值,只有超级用户可以用负值调用setpriority - getpriority获取谦让值
原型:int getpriority(_priority_which_t _which, id_t _who):
由于getpriority可以返回负值,建议调用getpriority前置errno为0, 如果调用后errno为0, 表明成功,否则表明调用失败
13.2.1 进程的运行环境
- 入口函数main
原型:int main(int argc,char *argv[].char *env[]);
参数:argc:表明程序执行时的命令行参数个数。注意:该参数个数包含了程序名称本身;argv:命令行参数数组。其中每一个数组成员为一个命令行参数。程序名称是该数组的第一个成员argv[0];环境变量数组,可以在程序中访问这些环境变量 - 系统变量
$#:命令行参数个数;$n:命令行参数,n为非负整数,如$0表示程序名称,$1表示第一个命令行参数;$?:前一条命令的返回码;$$:本进程的进程ID;$!:上一进程的进程ID - getopt获取和解析命令行参数
原型:int getopt(int _argc, char* const* _argv,const char* _shortopts);//单字符选项
int getopt_long(int _argc, char* const* _argv,const char* _shortopts,const struct option* _longopts, int* _longind);//多字符选项
参数:shortopts输入参数,选项字符串。该参数指定了解析命令行参数的规则。getopt认可的命令行选项参数是通过“-”进行的。例如,ls -l中的“-l” . 该参数中,如果某个选项有输入数据,则在该选项字符的后面应该包含“:”,如ls-l./a,在指定本参数时,应该用 “l:”。对于该选项,有一点需要注意,如果该参数的第一个字符是 “:”,则getopt发现无效参数后并不返回失败,而 是返回 “?”或者 “ :” 。返回 “ ?”表示选项无效,返回 “:”表示需要输入选项值
_longind:输出参数,如果该参数没有设罝为NULL那么它是一个指向整型变量的指针。在getopt_long运行时,该整型变量会被赋为获取到的选项在结构数组_longopts中的索引值 - struct option结构
struct option
{
const char name;//多字符选项名称
int has_arg;//定义了是否有选项值,如果该值为0,表示没冇选项值;如果该值为1,表明该选项有选项值;如果该值为2,表示该选项的值是可有可无的
int flag;//如果该成员定义为NULL,那么调用getopt_long的返回值为该结构val字段值;如果该成员不为NULL, getopt_long调用后,将在该参数所指向的变量中填入val值,并且getopt_long返回0。通常该成员定义为NULL即可
int val;//该长选项对应的短选项名称
}; - getopt返回值:
'?'表示getopt返回一个未在_shortopts定义的选项
':'表示选项需要选项值,则实际未输入选项值
'-1'表示getopt已经解析完毕,后面已经没有选项
'0'表示getopt_long的结构数组参数_longtopts中的成员flag定义了值,此时getopt_long返回0,选项的参数将存储在flag指向的变量中
其他:返回的选项值 - 全局变量
optind:整型变量,存放环境变量数组argv的当前索引值。当调用getopt循环取选项结束(getopt返回-1)后,剩余的参数在argv[optind]~argv[argc-1]中
optarg:字符串指针,当处理一个带有选项值的参数时,optarg将存放该选项值
optopt:整型变量,当调用getopt发现无效的选项(getopt返回?或:),optopt包含了当前无效的选项
opterr:整型变量,如果调用getopt前设置该变量为0,则getopt在发现错误时不输出任何信息
13.2.2 进程的环境变量
- 全局环境变量文件/etc/profile
- 交互式shell使用.profile文件,非交互式shell使用.bashrc文件
- 设置环境变量
export 环境变量名=值//命令行中使用则只在本次会话有效,永久有效需写入.profile文件
echo $环境变量名//查询变量值
unset $环境变量名//删除变量 - 获取和设置环境变量系统调用getenv、putenv
原型:char* getenv(_const char* _name);
int putenv(char* _string);
参数:name输入参数,环境变量名;string输入参数,要设置的环境变量名,格式为"环境变量名=值"
13.2.3 进程的内存分配
- 程序的局部变量是在进程的栈中分配空间的,由系统自动完成分配
- 在程序中需要动态分配的内存将从系统可用内存中申请新的空间,并加入到进程的堆中,申请的空间应由用户释放
- 内存申请及释放系统调用P213malloc,realloc,free
原型:void* malloc(size_t _size);
void* realloc(void* _ptr,size_t _size);//增加已分配的内存空间
void* free(void* _ptr);
参数size申请空间字节大小;ptr已存在的内存缓存区指针
13.3.1 fork创建进程
- 调用fork进程将返回两次P215,分别在父进程和子进程中返回,父进程中返回子进程标识符,子进程中返回值为0
- 父进程和子进程并行运行,都会从fork()调用后的下一行代码开始执行,即调用fork()后有父子两个进程同时运行,但由于pid变量的值不同,它们会执行不同的代码路径。父子进程谁先执行是由系统决定的,switch(pid=fork()),该语句在父子进程中会执行不同的case块
- fork创建的子进程继承父进程的属性有
进程实际用户ID,实际用户组ID,有效用户ID,有效用户组ID
进程组ID,会话ID及控制终端
当前工作目录及根目录
文件创建掩码UMASK
环境变量 - 不能继承的属性有
进程号,子进程号不同于任何一个活动的进程组号
子进程的用户时间和系统时间,这二者被初始化为0
子进程的超时时钟被设置为0,其被alarm系统调用使用
子进程的信号处理函数指针组被置为空。原来的父进程中的信号处理函数都将失效
父进程的记录锁 - fork调用注意
父进程中已打开的文件或套接口等描述符可在子进程中直接使用,每fork一次,描述符的引用计数加1,在关闭描述符时要多次关闭直到引用计数为0
子进程复制父进程的数据段,包括全局变量,但各有一份全局变量的拷贝,因此不能用全局变量在父子进程间进行通信而应使用专门的进程间通讯机制
13.3.2 调用exec系列函数执行程序
- exec系统函数并不创建新进程,调用exec前后的进程ID是相同的
- exec系列函数的主要工作是淸除父进程的可执行代码映像,用新程序的代码覆盖调用exec的进程代码
- 如果exec执行成功,进程将从新程序的main函数入口开始执行
- 调用exec函数后有以下属性保持不变
- 进程ID
- 进程的父进程ID
- 实际用户ID和实际用户组ID
- 进程组ID,会话ID及控制终端
- 定时器剩余时间
- 当前工作目录及根目录
- 文件创建掩码UMASK
- 进程信号掩码
- exec系列函数分为execl和execv两个系列
execl、execle、execlp;execv、execve、execvp
1是list(列表)的意思,表示execl系列函数需要将每个命令行参数作为函数的参数进行传递;v是vector(矢量)的意思,表示execv系列函数将所有函数包装到一个矢量数组中传递即可
原型:int execv(_const char* _path, char* _const _argv[]);
int execve(_const char* _path, char* _const _argv[],char* _const _envp[]);
int execvp(_const char* _file, char* _const _argv[]);
int execl(_const char* _path, _const char* _arg,...);
int execle(_const char* _path, _const char* _arg,...);
int execlp(_const char* _file, _const char* _arg,...);
参数:path输入参数,要执行的程序路径。file输入参数,要执行的程序名称,指文件名。如果该参数中包含“/”字符,则视为路径名直接执行;否则视为单独的文件名,系统将根据PATH环境变量指定的路径顺序搜索指定的文件;argv输入参数,命令行参数的矢量数组;envp输入参数,带有该参数的exec函数,可以在调用exec系列函数时,指定
一个环境变量数组。其他不带该参数的exec系列函数,则使用调用进程的环境变景;arg程序的第0个参数,即程序名自身,相当于_argv[0];...输入参数,命令行参数列表。调用相应程序时有多少命令行参数,就需要有多少个输入参数项。注意:在使用此类函数时,在所有命令行参数的最后,应该增加一个空的参数项(P218),表明命令行参数结束。
13.3.3 调用system创建进程
- system将加载外部的可执行程序,返回码是可执行程序的返回码
原型:int system(_const char* _command);
参数:command输入参数,要执行的程序文件名
返回值:-1失败,127失败;在 system的内部实现中,system首先fork子进程,然后调用exec执行新的shell,在shell中执行要执行的程序。如果在调用exec时失败,system将返回127。由于要加载的外部程序也有可能返回127,因此,在system返回127时,最好判断一下errno。如果errno不为0,表明调用system失败;否则,调用system成功,被加载的程序返回码是127。其他,成功。
13.4.1 调用exit退出进程
原型:void exit(int _status);
void _exit(int _status);//_exit函数在退出时并不刷新带缓冲I/O的缓冲区
参数:status输入参数,程序退出时的返回码,可在shell中通过$?系统变量取得,也可以通过system系统调用的返回值取得,还可以在父进程中通过调用wait函数获得
13.4.2 调用wait等待进程退出
- 一个进程结束运行时,将向其父进程发送SIGCLD信号
- 父进程调用wait等待其子进程的退出。如果没有任何子进程退出,则wait在缺省状态下将进入阻塞状态,直到调用进程的某个子进程退出
原型:_pid_t wait(_WAIT_STATUS stat_loc);
_ pid_t waitpid ( pid_t _pid, int* _stat_loc,int _options);
参数:_stat_loc输出参数,用于保存子进程的结束状态P221;pid输入参数,用于waitpid;options输入参数,指定调用waitpid时的选项
返回值:-1失败,其他,退出的子进程ID
第十四章 信号
14.1.1 信号的定义
- 信号(signal)又称为软中断,用来通知进程发生了异步事件,信号的接收过程是异步的过程。也就是说,进程在正常运行过程中,随时可能被各种信号所中断。进程可以忽略该信号,也可以中断当前执行的程序转而调用相应的函数去处理信号。待信号处理完成后,继续执行被中断的程序
14.1.2 信号来源
- 信号的来源可能是系统内核,也可能是其他的进程,常见产生信号的情况如下:
- 程序中执行错误的代码。如内存访问越界、数学运算除零等
- 其他进程发送来的信号
- 用户通过控制终端发送来的信号,如用户通过在键盘输入<ctrl> +<c> 键或者<ctrl>+<\>键中止程序的执行
- 子进程结束时向父进程发送的SIGCLD信号
- 程序中设定的定时器生产的SIGALRM信号
14.1.3 信号分类
- kill -l查看信号列表
- 可靠信号(32-63)是指信号一旦发出,操作系统保证该信号不会丢失。而不可靠信号(1-31,SIGUP-SIGSYS)由于内核不对信号进行排队,造成的后果就是信号有可能丢失。信号的可靠与否与信号是如何发送的无关,它只与信号的类型有关
- 常见信号说明

14.2.1 信号的处理方式
- 忽略信号SIG_IGN、缺省处理SIG_DFN、捕捉信号用信号处理函数处理
- 多数信号可以忽略,SIGKILL和SIGSTOP不可忽略
- 主要信号的缺省处理

14.2.2 用signal安装信号
- 系统调用signal
原型:_sighandler_t signal(int _sig, _sighandler_t _handler);
参数:sig输入参数,信号名称,handler输入参数,指向信号处理函数的指针,指定收到信号后,调用的信
号处理函数。除自定义处理函数指针外,该参数还可以选择SIG_DFN:釆用缺省的信号处理方式;SIG_IGN:忽略该信号
14.2.3 用sigaction安装信号
- 系统调用sigaction
原型:int sigaction(int _sig, _const struct sigaction* _restrict_act,struct sigaction* _restrict_oact);示例P229
参数:sig输入参数,信号名称,act输入参数,安装信号的数据结构包括处理函数、标志位等,oact输出参数,调用sigaction成功后,该参数中将返回信号的原来处理方式。如果不需要获取原信号处理方式,则该参数可以传入NULL,struct sigaction结构如下
struct sigaction
{
#ifdef _USE_POSIX199309
union
{
_sighandler_t sa_handler;
void (*sa_sigaction) (int, siginfo_t*, void *);
}_sigaction_handler;
/*定义信号的处理函数,实际上是指向信号处理函数的指针,一种是与singal调用相同的处理函数,一种是只被sigaction系统调用所支持的函数*/
#define sa_handler _sigaction_handler.sa_handler
#define sa_sigaction _sigaction_handler.sa_sigaction
#else
_sighandler_t sa_handler;
#endif
_sigset_t sa_mask;//信号掩码。在信号处理函数执行的过程中,在该掩码中定义的信号将被阻塞
int sa_flags;
/*标志位,sigaction支持若干标志位,主要的冇SA_SIGINFO、SA_RESTART等。
其中SA_SIGINFO标志表明该信号在发送时可以附带其他数据;SA_RESTART参数表明如果信号中断
了进程的某个系统调用,则由系统自动重启该系统调用。如果不指定该参数,则被中断的系统调用
将返回失败,错误码为EINTR*/
void (*sa_restorer) (void);
};
siginfo结构
typedef struct
{
int si_signo;
int si_code;
union sigval si_value;
int si_errno;
pid_t si_pid;
uid_t si_uid;
void *si_addr;
int si_status;
int si_band;
} siginfo_t;
14.2.4 信号的阻塞处理
- 信号的阻塞就是通知系统内核暂时停止向进程发送指定的信号,而是由内核对进程接收到的相应信号进行缓存,直到进程解除对相应信号的阻塞为止。一旦进程解除对该信号的阻塞,则缓存的信号将发送到相应进程
- 几种信号进入阻塞状态的情况
- 信号的处理函数执行过程中,同类信号将被阻塞,直到信号函数执行完成,该阻塞将会解除
- 通过sigaction进行信号安装时,如果设置了sa_mask阻塞信号集,则该信号集中的信号在信号处理函数被执行期间将会阻塞,如一个信号处理函数在执行过程中,可能会有其他信号到来。此时,当前的信号处理函数就会被中断。而这往往是不希望发生的。此时,可以通过sigaction系统调用的信号阻塞掩码对相关信号进行阻塞。通过这种方式阻塞的信号,在信号处理函数执行结束后就会解除
- 系统调用sigprocmask,可以通过该系统调用指定阻塞某个或者某几个信号,如某个信号的信号处理函数与程序代码都要对某个共享数据区进行读写。如果在程序正在进行读写的过程中,进程收到了该信号,则程序的读写过程被中断转而执行信号处理函数,而信号处理函数也要对该共亨数据区进行读写,此时共享数据区就会发生混乱。这种情况下,需要在程序读写共亨数据区前阻塞信号,在读写完成后再解除信号阻塞
- 如果用户定义的信号处理函数还没有来得及执行,此时该信号也就不会阻塞。在这种情况下,将会发生一种称为“信号合并” 的现象。同时到达的同类信号将被合并处理,就像只有一个信号到达一样
- 被阻塞的信号的集合称为当前进程的信号掩码。每一个进程都有一个惟一的信号掩码
- 系统调用sigprocmask
原型:int sigprocmask(int _how,_const sigset_t* _restrict _set,sigset_t* _restrict_oset);
参数:how输入参数,设置信号阻塞掩码的方式,可以包括3种方式对信号的掩码进行设置,分别是阻塞信号的SIG_BLOCK,解除阻塞的SIG_UNBLOCK和设置阻塞掩码的SIG_SETMASK;set输入参数,阻塞信号集,当参数how为SIG_BLOCK时,该参数表明要阻塞的信号集;当how参数为SIG_UNBLOCK时, 该参数表明要解除阻塞的信号集;当how参数为SIG_SETMASK时,该参数表明要阻塞的信号集;oset输出参数,原阻塞信号集。调用成功后,将返回该进程原阻塞信号集
14.2.5 信号集的操作
- 信号集即信号掩码,信号集的数据类型为sigset_t,实际上是一个结构,结构中的惟一成员是一个long型数组,用于存储多个信号
- 信号集操作函数
原型:int sigemptyset(sigset_t* _set);//清空信号集
int sigfillset(sigset_t* _set);//向信号集填充全部信号
int sigaddset(sigset_t* _set, int _signo);//向信号集中增加信号
int sigdelset(sigsetJt* _set, int _signo);//从信号集中删除信号
int sigismember(_const sigset_t* _set, int _signo);//判断信号集是否包含某信号
参数:set输入参数,信号集,signo输入参数,要增加或删除的信号
14.2.6 未决信号的处理
- 信号的未决是信号产生后的一种状态,是指从信号产生(可能由硬件产生,也可能由其他进程发送)后,到信号被接收进程处理之前的一种过渡状态
- 如果程序中使用了sigprocmask阻塞了某种信号,则向进程发送的这种信号将处于未决状态
- 获取未决信号sigpending
原型:int sigpending(sigset_t* _set);
参数:set输出参数,处于未决状态的信号集
14.2.7 等待信号
- pause系统调用
一旦调用则进程将进入休眠状态。只有在进程接收到信号后,pause才会返回
原型:int pause(void);
要么返回-1要么返回错误码EINTR - sigsuspend系统调用,设置当前进程的信号掩码,然后进程进入休眠,直到某个信号到达,工作过程1.设置进程的信号掩码并阻塞进程2. 收到信号,恢复原来的信号掩码3.调用进程设置的信号处理函数4.等待信号处理函数返回后,sigsuspend返回。这四个步骤是由sigsuspend一次性完成的
原型:int sigsuspend(_const sigset_t* _set);
参数:set表示输入参数,执行过程中阻塞的信号集。返回值永远为-1
sigsuspend临时替换当前的信号屏蔽字为set,并在捕捉到信号后恢复原来的信号屏蔽字
14.2.8 信号处理函数的实现
- 信号处理函数应该尽量简洁,最好只是改变一个外部标志变量的值,而在另外的程序中不断的检测该变量,繁杂的工作都留给这些程序去做
- 如果信号处理程序中需要存取某一个全局变量,则应该在程序中用关键字volatile声明此变量。通知编译器,在编译过程中不要对该变量进行优化。因为编译器在优化时,往往会将引用该变量的拷贝。而信号处理函数中的全局变童,往往是异步修改的,其值随时可能被信号处理函数更改。通过volatile关键字声明,使用编译器在引用该变世时,直接从该变量的内在地址中引用
- 设置跳转点的sigsetjmp和执行跳转的siglongjmp
原型:int sigsetjmp(struct _jmp_buf_tag _env[1],int _savemask);
void siglongjmp(sigjmp_buf _env, int _val);
_env:输出参数,该参数实际上是一个结构数据。结构中包含了长跳转指针,是否保存信号掩码及保存的信号掩码值等信息 _savemask:输入参数,是否保存信号掩码。如果该参数非零,则在调用sigsetjmp后 ,当前进程的信号掩码将被保存。在调用sigsetjmp时,将恢复以前保存的信号掩码
14.3.1 kill发送信号
- kill不仅可以向某个进程发送信号,而且可以实现向一组进程发送信号
原型:int kill(_pid_t _pid,int _sig);
参数:pid,输入参数,进程ID,通过对进程ID赋予不同的值,实现多种信号发送方式
14.3.2 使用sigqueue发送信号
- sigqueue可以实现kill的全部功能,而且还可以实现支持附加数据发送信号
原型:int sigqueue(_pid_t _pid,int _sig,_const union sigval _val);
参数:pid输入参数,发送信号的目的进程ID,只能向一个进程发送,sig输入参数,要发送的信号名称,val,输入参数,信号的附加数据,结构为
typedef union sigval
{
int sigval_int;
void* sival_ptr;
}sigval_t;
在调用sigqueue时,该参数将会复制到信号处理函数的第二个参数中。在信号处理函数中可以获取到附加数据并进行处理
14.4.1 SIGALRM信号
- 如果不安装SIGALRM信号,则进程收到SIGALRM信号后,缺省的动作是终止当前进程
- SIGALRM的信号处理函数应该做的操作往往是设置超时标志。由外部的程序去判断该超时标志,并输出相应的错误提示信息
14.4.2 设置定时器
- 设置定时器系统调用alarm
原型:unsigned int alarm(unsigned int _seconds);
参数:seconds定时时间,在alarm调用成功后开始计时,超过该时间将触发SIGALRM信号。返回值为当前进程以前设置的定时器剩余秒数 - if((pid=fork())==0){}else{}的含义:此语句在子进程执行if块,父进程执行else块,父进程和子进程并行运行,都会从fork()调用后的下一行代码开始执行,但由于pid变量的值不同,它们会执行不同的代码路径。父子进程谁先执行是由系统决定的
14.5.1 SIGCLD信号
- 在子进程退出时,将向其父进程发送SIGCLD信号
- 当一个子进程退出时,并不是立即释放其占用的资源,而是通知其父进程,由父进程进行后续的工作,主要有
- 向父进程发送SIGCLD信号,子进程进入zombie(也称为僵尸)状态
- 父进程接收到SIGCLD信号,进行处理
- 如果父进程既没有忽略SIGCLD信号,也未捕获信号进行处理,则该子进程将进入僵尸状态
- 在SIGCLD的信号处理函数中,应该调用wait()或者waitpid()等函数获取子进程的退出状态
第十五章 进程间通信——管道
15.1 进程间通信概念
- 进程间通信是在不同进程间进行信息交换的机制
- 消息队列、共享内存、信号量3种机制合并起来称为进程间通信, 简称为IPC(Inter Process Communication)
15.2.1 管道的概念及特点
- 管道是单向的,一端只能用于输入,另一端只能用于输出。管道的输出遵循“先进先出(FIFO: First In First Out)”的原则
- 普通管道只能用于有亲缘关系(父子进程)的进程间进行通信
- ls|wc -l:ls的输出通过管道作为wc -l命令的输入
- 在管道满时,写管道操作将阻塞;在管道空时,读管道操作将阻塞
15.2.2 管道的分类
- 管道可以分为普通管道和命名管道两种
- 父进程创建一个位于内存的普通管道,并将该管道传递给子进程。通过普通管道父进程与子进程便可以进行通信
- 命名管道位于文件系统中,只要可以访问文件系统的进程都可以通过管道的名字进行访问。命名管道可以实现无仟何亲缘关系的两个进程间的通信处理
15.3.1 创建管道
- 创建管道函数pipe
原型:int pipe(int _pipedes[2]);
参数:pipedes输出参数,整型数组,在函数执行后,该数组中将保存系统返回的管道描述符。其中_pipedes[0]文件描述符用于读操作,而_pipedes[1]文件描述符用于写操作,创建管道后默认读阻塞 - 在多进程的环境下,一个有效的管道描述符在并发子进程时将引用计数加1。P251这里的每一次只是将其引用计数减1。在一个描述符的引用计数变为0的情况下,系统才会真正关闭一个描述符
15.3.2 读写管道
- 通过dup2函数将标准输入输出文件描述符关联到管道读写描述符
15.3.3 关闭管道
- 管道在使用完成后,应该调用close关闭。由于一个管道包含两个描述符,所以应该分别调用close进行关闭
15.3.4 管道IO
- 管道函数popen和pclose
原型:FILE* popen(_const char* _command, _const char* _modes);
int pclose(FILE* _stream);
参数:command要执行的命令串,modes方式,r返回读指针,w返回写指针
popen首先创建一个管道,然后fork一个子进程, 在子进程中通过exec系列函数加载要执行的命令或程序,modes为r返回的文件指针用来读取命令的输出;modes为w返回的文件指针用来向命令输入数据 - popen返回成功仅表示管道创建成功,不能说明命令执行成功,命令执行结果需要通过pclose返回值判断即pclose调用后命令结果才会输出
15.4.1 创建命名管道
- 在命名管道创建成功后,在文件系统中将产生一个物理的FIFO文件
- 创建命名管道mknod和mkfifo命令
mknod 管道名称 p
mkfifo -m 权限 管道名称
权限是指生成的管道文件的权限信息 - 创建命名管道函数mkfifo
原型:int mkfifo(const char* _path, _mode_t _mode);
参数:创建的管道文件名称路径,mode管道文件权限
15.4.2 打开命名管道及读写
- open打开管道阻塞和非阻塞方式,缺省阻塞方式,在open时的标志中指定O_NONBLOCK则将以非阻塞方式打开
- 在非阻塞方式下,对管道的只读打开将立即返回。而如果事先没有进程以只读方式打开管道,则对管道的写方式打开将返回失败,errno中的错误信息是ENXIO。先读打开管道可以确保在写入之前其他进程完成对管道的读取实现读取完整性
- 在阻塞方式下,当打开一个管道用于读时,如果没有打开管道用于写的进程,则该打开操作将阻塞,直到有一个进程用写的方式打开该管道为止。同样,当打开一个管道用于写时,如果没冇打开管道用于读的进程,则该打开操作也将阻塞,直到有一个进程用读的方式打开该管道为止。即要有两个进程分别对命名管道进行读写操作

15.4.3 管道的删除
- 管道不再使用时需要用ulink删除
第十六章 进程间通信——消息队列
16.1.1 Shell环境控制IPC
- IPC对象一经创建,系统内核即为该对象分配相关数据结构
- 查看IPC对象命令ipcs
命令:ipcs [-asmq]
参数:-a查看全部ipc对象信息、-q查看消息队列信息、-m查看共享内存信息、-s查看信号量信息
输出:key键值,在系统中是全局惟一的,表明该对象的键值。不同的IPC机制,其Key值是可以重复的;ID标识符,访问该ipc的标识符。对于同一键值的IPC对象,每重建(删除后再重新创建)一次,标识符都将加1,到达系统约定的最大值后归0重新开始加1;owner对象属主;perms对象权限。与文件权限类似,以3组共9位8进制数字表示,可在创建对象时指定访问权限;nattch共亨内存对象专用,表明有多少个进程对该共享内存进行了映射(shmat);nsems信号量专用,表明该信号量对象包含多少个信号量(一个信号量对象可以包含多个信号量);used-bytes消息队列专用,表明该消息队列中存储的数据量(以字节为单位);messages消息队列专用,表明该消息队列中有多少条消息 - 删除IPC对象命令ipcrm
命令:ipcrm [-smq] ID 或者 ipcrm [-SMQ] Key
参数:-q/Q删除消息队列信息,-m/M删除共享内存信息,-s/S删除信号量信息,如果指定了smq,则用IPC的标识符(ID)作为输入,如果指定了SMQ则用IPC对象的键值(key)作为输入
16.1.2 进程间通信关键字
- Linux的每个IPC对象都有一个名字,称为“键”(key)。这个关键字是全局惟一的,不同的进程通过键可以访问同一个IPC对象
- 进程间通信关键字是一个32位的长整型数据。在使用过程中,不同的进程直接使用key去创建IPC对象容易引起混淆,并且不同的应用之间可能会因为使用同一个key而产生冲突
- 创建IPC对象时,指定关键字为IPC_PRIVATE,系统会自动分配一个可用的关键字。通过该参数创建的IPC对象的关键字值是0,在其他进程中不能通过关键字对该对象进行访问,只能通过返回的标识符进行访问
- 调用函数ftok产生一个惟一的关键字值。通过IPC进行通信的进程,只需要按照相同的参数调用ftok即可产生惟一的参数。通过该参数可有效解决关键字的产生及惟一性问题
16.1.3 进程间通信标识符
- 对IPC对象的访问并不是通过“键”,而是通过标识符进行的,即必须先获得IPC对象的标识符
- 不同的进程打开同一个文件时,返回的文件描述符可能是不同的
- 不同的进程打开同一个IPC对象时返回的IPC标识符是相同的
- 对于一个IPC对象,在打开时返回一个标识符,而关闭后再次打开同一IPC对象时,该 标识符将顺序加1
16.1.4 IPC权限许可结构
- ipc_perm权限结构
struct ipc_perm
{
//关键字
_key_t _key;
//IPC对象的属主用户ID,是调用进程的有效用户ID,
//可以通过相应的函数(msgctl)对该值进行修改
_uid_t uid;
//IPC对象的属主组ID,是调用进程的有效组ID,
//可以通过相应的函数(msgctl)对该值进行修改
_gid_t gid;
//创建者的用户ID
_uid_t cuid;
//创建者的组ID
_gid_t cgid;
//读写权限,权限许可信息,指定哪些用户可以拥有该IPC对象的何种权限,
//其类似于文件系统中的权限,不过在IPC对象中,执行权限是没有意义的
unsigned short int mode;
//填充域
unsigned short int _pad1;
//Sequence number,系统在内核为IPC对象分配的计数器。每当IPC对象被删除时,该计数器值将加1,
//通过这种机制保证IPC的标识符在短时间内不会重复。对于应用编程来说,该字段无需考虑
unsigned short int _seq;
//填充域
unsigned short int _pad2;
//保留
unsigned long int _unused1;
//保留
unsigned long int _unused2;
};
16.2.1 队列
- 消息队列是存储消息的FIFO线性表,是消息在传输过程中的容器。消总队列一经创建,即可以向队列中写入指定类型的消息,其他进程则可以从该队列中取出指定类型的消息
16.2.2 消息
- 消息是进程间传递的数据的内容
- 一条消息包括消息类型和消息内容
- 一个消息队列可以存储不同类型的消息,进程可以从消息队列中取出所需类型的消息。同时,也可以向队列中发送不同类型的消息
- 消息结构模板struct msgbuf
struct msgbuf
{
//消息类型
long int msgtype;
//消息内容,在实际编程中,应根据该模板自行定义一个消息结构,
//这里的长度最大可以达到由系统内核规定的每条消息长度的最大值
char mtext[1];
};
16.2.3 消息队列
- 在消息队列中,入队称为发送消息,出队称为接收消息(从进程的角度)
16.3.1 键值生成函数
- 键值生成函数ftok,根据文件名和一个整型的变量(项目ID)生成一个惟一的键值,并且保证每次以同样的参数调用ftok返回的键值是相同的,该函数实际是将文件索引节点(i节点信息)和项目ID合成一个key,文件可以随便选取但应确保下面列出的要求
原型:key_t ftok(const char* _pathname,int _proj_id);
参数:pathname文件名称,绝/相对路径;proj_id项目ID,指定相同的文件名称和一个不同的项目ID , 可以生成不同的键值
选择的文件应该是内容和属性(修改时间等)长期不发生变化的,并且是对进程而言是可读的,如果文件内容或者属性发生变化,尽管用同样的参数去调用ftok,返回的键值还是不同的
16.3.2 创建消息队列
- 一个消息队列实际上是Linux在内核分配的一个数据结构
- msgget函数除了具有创建新的消息队列的功能外,还可以返回一个已存在的消息队列的标识符
原型:int msgget(key_t _key, int _msgflg);
参数:key输入参数,键值,可以直接指定一个键值/ftok产生一个键值输入/IPC_PRIVATE作为参数输入;一旦使用
IPC_PRIVATE凋用msgget,系统都将创建一个新的消息队列。并且,该消息队列的键值为0。由此产生的问题是,在其他进程中不能用键值访问该消息队列,而必须用msgget返回的标识符。所以,进程间必须通过某种机制(子进程继承等)获取标识符,然后通过该标识符进行消息传送和接收;msgflag输入参数,标志和权限信息。该参数用于创建消息队列时,指定创建标志和队列的权限。通常情况下在创建一个新的队列时,用参数IPC_CREAT;而在获取一个已存在的消息队列的标识符时,则将该参数置为0。该参数包括两部分内容,一是标志,二是权限
IPC_CREAT和IPC_EXCL合用表示创建IPC对象,如果IPC对象不存在则创建对象并返回标识符,如果对象已存在则返回-1,errno设置为EEXIST - 调用msgget成功后,操作系统在内核中分配了一个名称为msgqid_ds的数据结构用于管理该消息队列
- 一旦IPC对象被创建,除非显式的删除该对象或者系统重启,否则该对象将一直存在
struct msgqid_ds
{
struct ipc_perm msg_perm;//定义不同用户对消息队列的访问权限
_time_t msg_stime;//最后一次向该消息队列发送消息(msgsnd)的时间
unsigned long int _unused1;
_time_t msg_rtime;//最后一次从该消息队列接收消息(msgrcv)的时间
unsigned long int _unused2;
_time_t msg_ctime;//最后修改时间
unsigned long int _unused3;
unsigned long int _msg_cbytes;//当前该消息队列中的消息长度(字节)
msgqnum_t msg_qnum; //当前该消息队列中的消息条数
msglen_t msg_qbytes;//该消息队列允许存储的最大长度
_pid_t msg_lspid;//最后一次向该消息队列发送消息(msgsnd)的进程ID
_pid_t msg_lrpid; //最后一次从该消息队列接收消息(msgrcv)的进程ID。
unsigned long int _unused4;
unsigned long int _unused5;
};
16.3.3 消息发送
- 消息发送函数msgsnd
原型:int msgsnd(int _msqid, _const void* _msgp, size_t _msgsz,int _msgflg);
参数:msqid输入参数,消息队列标识符,由msgget返回的标识符;msgp输入参数,消息结构指针;msgsz输入参数,消息长度,指消息内容的长度不是消息结构的尺寸即消息结构msgbuf中字符数组的长度;msgflag输入参数,发送消息可选标志。如果不需要指定任何标志,在这里输入0即可,可以选择的标志只有IPC_NOWAIT,在默认状态下,消息的发送操作是阻塞的,一旦由于该消息队列中存储的消息已达到最大值(kernal.msgmnb) 或者存储的消息条数已达最大值(kernal.msgmni),此时调用 msgsnd将阻塞,直至有进程调用msgget从队列中读出消息。如果选择了IPC_NOWAIT标志,表明消息发送的过程是非阻塞的。如果队列已满,则msgsnd将立即返回-1 - 在指定发送的消息类型时,类型值必须大于0
- 发送消息的目标队列必须对于进程而言有可写权限
16.3.4 消息接收
- 消息接受函数msgrcv
原型:int msgrcv(int _msqid, void* _msgp, size_t _msgsz,long int _msgtyp, int _msgflg);
参数:msqid输入参数,消息队列的标识符,由msgget函数返回的标识符;msgp输出参数,消息结构指针;msgsz输入参数,要读取的消息长度。在不确定消息长度的情况下,此处可以定义一个稍大的长度,系统将读取相应消息的全部内容,并在msgrcv的返回值中指明消息的正确长度。如果此处定义的要读取的长度比真正消息内容长度要小,则默认状态下将返回失败。不过,如果指定了MSG_NOERROR可选标志,也可以成功读取;msgflg输入参数,接收消息可选标志
返回-1失败,其他为实际读取到的消息内容的长度
16.3.5 控制消息队列
- 消息队列创建成功后,内核不仅创建了消息队列结构用于存储消息,而且创建了消息队列的控制结构msqid_ds,可用msgctl函数控制该结构
原型:int msgctl(int _msqid, int _cmd, struct msqid_ds* _buf);
参数:msqid输入参数,消息队列标识符,msgget返回的标识符;cmd输入参数,控制命令;buf输入或输出参数,指向msqid_ds结构的指针。在_cmd参数为IPC_STAT时,该参数用作输出;在_cmd参数为IPC_SET时,该参数用作输入;_cmd参数为IPC_RMID为删除相应IPC对象
第十七章 进程间通信——共享内存
17.1.1 共享内存编程模型
- 消息队列在实现消息的收发时,首先由发送进程从进程空间将数据复制至内核分配的数据缓冲区,接收进程再从内核的缓冲区复制到进程的虚拟地址空间
- 共享内存是通过将内核分配的共享存储区映射到进程的地址空间实现的,没有数据的复制过程。因此,共亨内存的访问速度比消息队列要快
- 共享内存主要用于保存配置信息等相对静态的数据。应用程序启动时进行一次初始化,运行过程中各个进程从共享内存中读取信息
- 两个进程通过共享内存通信是指,同一块物理内存被映射到多个进程各自的进程地址空间,各个进程都可以对共享内存中数据进行更新。使用共亨内存可以更快地实现进程间通信
- 共享内存编程流程:创建共享内存shmget->将共享内存映射至调用进程的地址空间shmat->通过返回的共享内存读写指针对共享内存进行读写->关闭共享内存的映射shmdt
17.1.2 共享内存的映射
- 创建的共享内存被映射到进程的变量空间

- 进行共享内存映射时,可以映射至任意的数据结构。例如,可以映射到程序中一个普通的字符数组,也可以映射到程序自定义的一个数据结构,这是由程序根据需要自行确定的
17.1.3 共享内存控制结构
- 共享内存有访问计数器机制。每有一个进程进行一次共享内存映射,共亨内存计数器就会加1。而每做一次解除映射操作,该计数器就会减1。只有共亨内存的访问计数器为0时,才能真正地删除一块共享内存
- 共享内存控制结构struct shmid_ds
struct shmid_ds
{
//IPC许可权限结构
struct ipc_perm shm_perm;
//共享内存字节大小
size_t shm_segsz;
//最后一次调用shmat的时间
_time_t shm_atime;
unsigned long int _unused1;
//最后一次调用shmdt的时间
_time_t shm_dtime;
unsigned long int _unused2;
//最后一次调用shmctl修改共亨内存属性的时间
_time_t shm_ctime;
unsigned long int _unused3;
//创建共享内存的进程ID
_pid_t shm_cpid;
//最后一次操作共享内存的进程ID
_pid_t shm_lpid;
//当前共享内存的映射计数
shmatt_t shm_nattch;
unsigned long int _unused4;
unsigned long int _unused5;
}
17.2.1 创建共享内存
- 类似于消息结构msg_buf,共享内存也有共享内存结构用于共享内存映射到本地进程。映射完成后,对该结构的查询修改即是对共享内存的操作即读写共享内存
- 创建共享内存函数shmget
原型:int shmget(key_t _key, size_t _size, int _shmflg);
参数:key输入参数,键值;size输入参数,要创建的共享内存的大小,以字节为单位;shmflg输入参数,创建共享内存标志
返回共享内存描述符 - 在linux系统中,共享内存物理上是位于shm文件系统中的文件。该文件系统挂载于/dev/shm设备上,文件系统名称为tmpfs。tmpfs文件系统是完全驻留在系统内存中的,其访问速度非常快
17.2.2 映射共享内存
- 共享内存创建成功后,在整个操作系统内是全局可见的。只要具备访问权限,应用程序都可以使用该共享内存
- 将共享内存映射到本地进程的地址空间函数shmat
原型:void* shmat(int _shmid, _const void* _shmaddr, int _shmflg);
参数:shmid输入参数,共享内存标识符,由shmget返回;shmaddr该参数是由调用shmat的用户进程指定的一个虚拟空间地址,新创建的共享内存将附在该地址后。该地址一般情况下直接指定为NULL即可;shmflg输入参数,创建共享内存标志
返回值即是映射到本地进程的共享内存地址
17.2.3 删除共享内存映射
- 删除共享内存映射函数shmdt
原型:int shmdt(const void* _shmaddr);
参数:shmaddr输入参数,共享内存映射至本地的地址,即由shmat返回的地址
虽然进程在退出时会自动删除与共享内存的映射但应在程序中使用共亨内存结束后,要记住调用shmdt删除与进程的映射
共亨内存有引用计数器。调用shmdt只是将该共享内存的引用计数器减1,只有引用计数值变为0的情况下,才可以调用shmctl将共亨内存删除
17.2.4 控制共享内存
- 共享内存创建后,系统在内核为其分配了一个类型为struct shmid_ds的控制结构
- 函数shmctl获取该结构并通过修改该结构控制共享内存
原型:int shmctl(int _shmid, int _cmd, struct shmid_ds* _buf);
参数:shmid输入参数,共享内存标识符,由shmget返回;cmd输入参数,控制命令;buf输入或输出参数,指向shmid_ds结构的指针。在_cmd参数为IPC_STAT时,该参数用作输出;在_cmd参数为IPC_SET时,该参数用作输入;_cmd参数为IPC_RMID为删除相应IPC对象 - 共享内存不支持多进程同时读写,因此在实际应用中,如果存在多进程同时读写的情况,要釆用进程间同步机制(如信号量)进行读写的同步控制
第十八章 进程间通信——信号量
18.1.1 PV操作的来源
- 信号量semphore
18.1.2 PV操作的定义
- P是荷兰语passeren通过,V是荷兰语Vrijgeven释放
- PV操作是不可中断的原语(原子操作)
- PV操作的前提是假定存在整型变贵sem下对其进行加减操作
P操作将sem减1,V操作将sem加1,实际上信号量sem表示是否存在某种资源供进程调用,sem为0表示没有资源可调用则进程阻塞,下面的进程互斥是一个例子
18.1.3 PV操作的应用
- 进程互斥
假设系统中存在一个文件,两个进程A和B都要对其进行写操作
P(sem);
WriteFile;
V(sem);


- 进程同步
假设其应用系统有两个进程A和B,进程A负责进行问题的计算,进程B负责结果的打印。两个进程同时开始运行。进程A计算问题结果要一定时间,而进程B必须要等待A执行完成后,方可继续
在系统中定义一信号量,其初始值为0。进程A执行计算任务,执行完成后调用V操作通知进程B打印。而进程B一开始就执行P操作,等待进程A计算结束
进程A 进程B
ComputeResult(); P(sem);
V(sem); PrintResult();
该过程和进程互斥的理解一致,用图上图信号量的含义理解即可
18.2.1 信号量简介
- 与消息队列及共亨内存不同,信号量实现的是一种类似汁数器的功能,而不是用于存储进程间通信数据
- 信号量的互斥功能使用情况:多个进程共亨某项资源并且可能同时对该项资源进行操作
将访问该共享资源的代码定义为临界区,要访问临界区的代码,必须首先调用P操作获取对资源的访问权。而访问临界区结束后,要调用V操作释放信号量,以便于其他进程访问临界区 - 信号量的同步功能用于实现进程间同步,即某个进程需要等待另一个进程完成后再执行
在多进程编程环境下,可能某个进程(进程A)需要等待另一个的进程(进程B)先执行一些操作后再执行,建立一个信号量,其初始值为0 , 这表示当前可用的资源数为0。进程A执行时,首先调用操作量的P操作以获取资源,此时资源数为0,A进程将阻塞。进程B执行时,首先执行必须的功能代码,然后对信号量调用V操作。V操作执行完毕后,信号量值将变为1, 此时被阻塞的A进程将激活执行
18.2.2 信号量的控制结构
- 信号量控制结构
struct semid_ds
{
struct ipc_perm sem_perm;//许可权限结构
_time_t sem_otime;//最后一次semop操作的时间
unsigned long int _unused1;
_time_t sem_ctime;//最后一次调用semctl改变信号量的时间
unsigned long int _unused2;
unsigned long int sem_nsems;//信号量集中的信号量个数
unsigned long int _unused3;
unsigned long int _unused4;
};
18.3.1 创建信号量
- semget创建信号量
原型:int semget(key_t _key,int _nsems,int _semflg);
参数:key键值,可有ftok生成;nsems信号量集中的信号量个数;semflg创建信号量标志。主要包含两方面的信息:一是访问权限信息,该权限信息将初始化信号量控制结构中的sem_perm结构,二是创建标志,主要的创建标志包括IPC_CREAT,IPC_EXCL;返回信号量描述符
semget创建的信号量实际上是一个包含nsems个信号量的信号量集,编号0~nsems-1
在信号量已存在的情况下,如果调用semget只指定了IPC_CREAT标志,那么参数_nsems必须与创建时指定的值一致,返回已存在的信号量集的标识符,若_nsems与创建时的值不一致则semget将返回失败;如果调用semget时指定了IPC_CREAT|IPC_EXCL标志,semget将返回-1,errno为EEXIST,即使是nsems与创建时不同也是如此 - 信号量创建成功后,内核将为该信号量产生控制结构。根据semflg中的权限信息设置sem_perm的权限许可,根据nsems的值设置sem_nsems的值,根据调用进程的用户标识、调用时间等信息初始化其他相关内容
18.3.2 信号量操作
- 信号量操作semop
原型:int semop(int _semid, struct sembuf* _sops, size_t _nsops);
返回值:0成功,-1失败
参数:semid输入参数,信号量集标识符,由semget返回;nsops输入参数,操作的信号量个数。每次semop调用可以对一个信号量集中的多个信号量进行操作;sops输入参数,信号量集的操作缓存,表明了对该信号量集的详细操作,结构如下
struct sembuf
{
//信号量编号。同一个信号量集中多个信号量的编号
//该编号由0开始,指第一个信号量
unsigned short int sem_num;
//信号量的操作数,正值表示V操作,负值表示P操作,0表示等待信号量值变为0
//如果信号量值为0,则立即返回;否则进入阻塞,直至信号量值为0
short int sem_op;
//信号量的操作标志
short int sem_flg;
}
信号量的操作标志主要包括IPC_NOWAIT和SEM_UNDO
IPC_NOWAIT如果设置了该标志,则在sem_op中应该阻塞的调用都将立即返回失败。错误代码errno为EAGAIN
SEM_UNDO如果设置了该标志,内核将分配一个sem_undo的数据结构记录对该信号量的操作。在进程结束时,在进程中对该信号量进行的semop操作将被撤销。设该标志的意义在于,一旦进程没冇释放信号量就退出(进程异常结束等),系统内核将根据sem_undo的记录主动代为释放
- semop在对信号量集内的多个信号量的操作是原子操作,要么全部成功,要么全部失败
- 在semop被阻塞时,信号量的值符合semop要求时将会返冋。除此之外,还冇两种情况将导致semop返回;一是信号量被删除,此时semop将立即返回失败,错误码ERMID; 二是进程收到信号,此时semop将被中断返回失败,错误码为EINTR
18.3.3 信号量控制
- 信号量控制semctl
原型:int semctl(int _semid, int _semnum, int _cmd, ...);
参数:semid输入参数,信号量标识符,由semget返回;semnum输入参数,信号量的编号。信号量在信号量集中的索引;cmd输出参数,信号量控制命令,如设置信号量初始值等
信号量控制命令如下

...输入参数,信号量控制结构联合体,对应不同的控制命令,结构如下
union semun
{
//用于SETVAL控制命令。整型变量,用于设置信号量的值
int val;
//用于IPC_STAT和IPC_SET控制命令。
//指向semid_ds结构指针,用于获取或设置信号量控制结构
struct semid_ds* buf;
//用于GETALL和SETALL控制命令。
//指向短整型数组的指针,用于获取或设置信号量集的值
unsigned short* array;
//用于IPC_INFO控制命令。该控制命令是linux系统下特有的,
//用于返回系统内核定义的信号量极值的定义信息。
//该成员为一个结构指针,结构类型为seminfo
struct seminfo* _buf;
};
//seminfo结构为
struct seminfo
{
int semmap;//信号量映射入口,暂未用
int semmni;//系统中允许创建的信号量集的最大数目
int semmns;//系统中允许创建的信号量的最大数目
int semmnu;//暂未用
int semmsl;//一个信号量集中允许包含的最大信号量数目
int semopm;//一次semop可以同吋操作的最大信号量数目
int semume;//每个进程允许sem_undo结构的最大数目,暂未用
int semusz;//sem_undo结构的大小
int semvmx;//信号量允许设置的最大值
int semaem;//信号量的sem_undo机制可以记录的最大值
};
- semctl可能的错误如下
EACCES访问出错,权限不允许;EFAULT控制命令指定的参数(联合)中,buf或array所指的空间不可访问;EIDRM信号集已被删除;EINVAL参数无效;EPERM权限不允许;ERANGE给出的参数无效
18.4.1 生产者消费者模型
- 生产者进程生产信息,消费者进程使用信息
18.4.2 需求分析与设计
- 进程一作为生产者,不断产生随机数记入缓存;进程二作为消费者,不断消费缓存中的随机数
- 设两个信号量分別用于控制进程一与进程二的读写。信号量一用于消费者(进程二)通知生产者(进程一)数据已取走,缓冲区已经可写入新数据;信号量二用于生产者(进程一)通知消费者 (进程二)新数据已产生,消费者可以读取新产生的数据
- 设定信号量一的初始值为1,信号量二的初始值为0,程序逻辑为

- 信号量一表示写标志,信号量二表示读标志,互相操作对方的信号量实现进程间有序(同步)执行
IPC进程间通信流程小结
第十九章 Linux网络环境
19.1.1 计算机网络分类
- 计算机网络是把分布在不同地理区域的计算机与专门的外部设备用通信线路互联成一个规模大、功能强的网络系统 ,从而使众多的计算机可以方便地互相传递信息,共亨硬件、软件、数据信息等资源
- 局域网(LAN,Local Area Network)、广域网(WAN,Wide Area Network)、域域网(MAN,Metropolitan Area Network)
- 局域网标准:Ethernet(以太网)、Token King(令牌环网)、FDDI(光纤分布式接口网络)、ATM(异步传输模式网)及WLAN
- 网络覆盖范围:LAN<MAN<WAN
19.1.2 网络拓扑结构
- 网络拓扑结构是指用传输媒体互联各种设备的物理布局,反映了网络中各个实体的结构关系
- 网络拓扑结构有总线型拓扑、星型拓扑、环型拓扑
- 总线型拓扑
将网络中的所有设备通过相应的硬件接口直接连接到公共总线上,结点之间按广播方式通信,一个结点发出的信息,总线上的其他结点均可"收听"到
通常使用同轴电缆作为通信介质。由于所有结点共享同一传输链路,每次只能有一个设备使用传输链路。因此,需要采用某种形式的访问控制策略,来决定下一个使用链路的结点 - 星型拓扑
每个结点都由一条单独的通信线路与中央节点连接,任何两个节点要进行通信都必须经过中央节点控制 - 环形拓扑
各节点通过通信线路组成闭合回路,环中数据只能单向传输,信息按固定方向流动,或顺时针方向,或逆时针方向,,任意节点出现故障都会造成网络瘫痪
19.1.3 网络通信协议
- 网络通信协议是计算机网络和分布系统中,互相通信的对等实体间交换信息时所必须遵守的规则的集合,定义了网络上的各种计算机和设备之间相互通信、数据管理、数据交换的整套规则
- 主要的网络通信协议冇0SI协议、TCP/IP协议、NetBEUI协议、IPX/SPX协议、SNA协议等
19.1.4 OSI参考模型
- OSI(Open System Interconnection)开放系统互联参考模型
- OSI七层网络模型:物理层(原始比特流/高低电平/01数据的传输)、数据链路层(相邻节点间可靠数据通信)、网络层(主机间报文传输)、传输层(不同主机用户进程间的可靠数据通信)、会话层(不同主机的用户间建立会话关系)、表示层(数据表示)、应用层(网络应用接口)
- OSI模型

- OSI网络通信模型

- 在实际通信过程中,数据最终将抵达最低的物理层,变成二进制的数据比特流,在物理连接介质间传递。当信息传递过协议层,它们形成一个称为协议数据单元的分组。每一层的实体按照约定向协议数据单元中加入自己的信息头部。当数据通过物理层抵达对端系统,它向上通过每一层协议栈,相应的信息头部被取下,并传送给层对应的实体
- OSI数据流模型

19.2.1 TCP/IP分层模型
- TCP/IP四层模型:网络接口层、IP层、传输层、应用层
- 网络接口层为TCP/IP协议的最底层,也被称为数据链路层,网络接口层通常包括操作系统中的设备驱动程序和计算机中对应的网络接口卡,它们一起处理与传输媒介的物理接口细节,与OSI模型的下两层相对应,即物理层与数据链路层
- IP层/网络层也称作网络互连层,是整个TCP/IP协议栈的核心,功能是把分组发往目标网络或主机,完成路由及异构网络互连任务,该层协议包括IP协议(网际协议),、ICMP协议(Internet互联网控制报文协议)以及IGMP协议(Internet组管理协议)
- 传输层主要为两台主机上的应用程序提供端到端的通信,在传输层定义了两种服务质量不同的协议:传输控制协议TCP(transmission control protocol)和用户数据报协议UDP(user datagram protocol)。TCP是一个面向连接的、可靠的协议,它将一台主机发出的数据流无差错地发往到其他主机。它所做的工作包括把应用程序交给它的数据分成合适的小块交给下面的网络层,确认接收到的分组,设置发送最后确认分组的超时时钟等
- 应用层负责处理特定的应用程序细节。TCP/IP将OSI参考模型中的会话层和表示层的功能合并到应用层实现。应用层面向不同的网络应用引入了不同的应用层协议,有基于TCP协议的文件传输协议(FTP)、虚拟终端协议(TELNET)、超文本链接协议(HTTP),基于UDP协议的简单网络管理协议(SNMP)、简单文件传输协议(TFTP)等
- OSI参考模型与TCP/IP模型的对应关系

19.2.2 TCP/IP协议族
- TCP/IP协议族

- IP网际协议:包括32位IPv4版本,128位IPv6版本,给TCP、UDP,ICMP提供递送分组的服务
- TCP传输控制协议,是一种面向连接的协议,它给用户进程提供可靠的全双工的字节流。TCP关心确认、超时和重传等具体细节
- UDP用户数据报协议,是一种无连接协议。不能保证每一UDP数据报可以到达目的地
- ICMP网际控制消息协议,处理路由器和主机间的错误和控制消息。这些消息一般由TCP/IP网络软件自身产生和处理
- ARP地址解析协议,把IP地址映射到硬件地址(以太网地址),一般用于广播网络,如以太网、令牌环网等,但不用于点对点网络
- RARP反向地址解析协议,把硬件地址映射到IP地址。有时用于无盘节点,如引导时的x终端
- FTP、TELNET、HTTP等协议都是应用层协议,用于实现文件传输、超文本传输等高层功能
19.2.3 网络地址
- 参与网络通信的设备至少应该分配一个IP地址,IP协议使用这个地址在主机间传递信息
- IP地址包括两个标识码网络标识和主机标识,同一个物理网络上的所有主机都使用同一个网络标识,每台主机有一个对应的主机标识
- 根据网络标识划分5种类型的IP地址:A、B、C、D、E类地址
- A类IP由1字节网络地址和3字节主机地址组成,网络地址的最高位必须是"0",地址范围1.0.0.1~126.255.255.254,可用的A类网络有126个,每个网络能容纳1亿多个主机
- B类IP由2个字节的网络地址和2个字节的主机地址组成,网络地址的最高位必须是"10",地址范围128.0.0.1~191.255.255.254。可用的B类网络有16382个,每个网络能容纳6万多个主机
- C类IP由3字节的网络地址和1字节的主机地址组成,网络地址的最高位必须是"110”,地址范围192.0.0.1~223.255.255.254。B类网络可达209万余个,每个网络能容纳254个主机
- D类IP地址第一个字节以"1110"开始,它是一个专门保留的地址,用于多点广播,多点广播地址用来一次寻址一组计算机,它标识共亨同一协议的一组计算机。地址范围224.0.0.1~239.255.255.254。
- E类IP以"11110"开始,为将来使用保留。
- 全零IP"0.0.0.0"对应于当前网络本身,而不是具体主机,全1IP"255.255.255.255"是当前子网的广播地址
- MAC地址是网络接口设备的物理地址,是设备生产过程中固化在设备ROM中的
- 最大传输单元MTU,网络对一次传输数据帧的最大长度有一个限制,这个特性一般体现在网络接口层的数据链路子层。如果网络层有一个数据报要传,而且数据的长度比数据链路层的MTU还大,那么IP层就需要进行分片,把数据分成若干片,这样每一片都小于MTU
- eth指网络接口是以太网,lo表示环回接口(Loopback Interface),环回接口允许运行在同一台主机上的客户程序和服务器程序通过TCP/IP协议进行通信,A类网络127就是为环回接口预留的。大多数系统把地址127.0.0.1分配给这个接口,并命名为localhost
19.2.4 端口
- 端口号用于标识同一主机上不同的应用程序,一台拥有IP地址的主机可以提供许多网络服务,如Web服务、ftp服务、telnet服务等,这些服务通过同一个IP不同端口区分
- 端口号是16个二进制位的整形数,其取值范围为165535。为实现系统提供的公用网络服务,11023间的端口号由系统保留,这部分端口号称为知名端口号。而用户自己编写的网络应用程序应该在1024以后的端口号中选择使用
- TCP端口和UDP端口,由于TCP和UDP两个协议是独立的,因此各自的端口号也相互独立
- 网络配置文件/etc/hosts、/etc/services。/etc/hosts存放的是一组IP地址与主机名的列表,如果在该列表中指出某台主机的IP地址,那么访问该主机时将无需进行DNS解析。这个文件的作用是进行域名解析。
19.3 客户机/服务器模型
- 单个机器既可以作为客户机也可以作为服务器,这主要取决于软件配置及执行功能类型
- 在通常客户机/服务器模型中,服务器是活性的,处于等待客户机请求状态。典型的多客户机程序共享通用服务器程序服务。客户机程序和服务器程序都是大型程序或应用的一部分
- 重复服务器
重复服务器在同一时刻只能处理一个客户的请求 - 并发服务器
并发服务器有主服务器和其生成的从服务器,并发服务器利用生成其他服务器的方法来处理客户的请求,每个客户都有它自己对应的服务器处理进程。如果操作系统允许多仟务,那么就可以同时为多个客户服务
19.4.1 TCP连接建立
- 传输控制协议TCP
在不可靠的网络服务上为应用层提供面向连接的、端到端的可靠字节流服务,在进行数据传输时必须先建立一条运输连接,数据传输完成之后释放连接
特点是TCP数据传输服务是全双工、连接是点对点、面向字节流,支持数据缓冲和立即发送、提供紧急数据功能 - TCP连接建立
三次握手,假设主机A需要和主机B进行通信,第一次握手指主机A向主机B发送连接请求,第二次握手指主机B发送同意连接和要求同步的数据包,第三次握手指主机A确认主机B的要求同步,第三次握手之后才开始发送数据
19.4.2 TCP连接关闭
四次挥手,假设主机A要关闭当前连接,第一次挥手指主机A向主机B发送断开连接请求,此时,主机A不能再向B发送数据,但是可以从B接收数据;第二次挥手指主机B向主机A发送同意断开连接和要求同步的数据包;第三次挥手指主机B再向主机A发出一个关闭连接请求,此时,主机B不能再向A发送数据;第四次挥手指主机A再发出一个数据包确认主机B的关闭要求,同意主机B断开连接。即双方分别请求断开连接形成四次挥手
19.4.3 TCP数据报格式
- 每层协议在向下层传送时,都要加上自定义的包头。TCP接收到来自应用层的数据包,在前面填充TCP头部后,将报文送至网络层,网络层在TCP数据报前加上IP头部并向下传输至网络接口层,网络接口层在IP报文前填充相应头部并形成最终的二进制比特流
- 在以太网中,最大传输单元MTU为1500个字节,其中IP包头占20字节,TCP包头占20字节,因此,当应用层数据超过最大数据长度时,将对该数据进行分片处理,在IP包头中会看到有多个片在传输,但标识号是相同的,表示是同一个数据包
19.5 用户数据报协议
- UDP协议是一种面向无连接的协议。"面向非连接"就是在正式通信前不必与对方先建立连接,不管对方状态就直接发送
- UDP协议适用于一次只传送少量数据、对可靠性要求不高的应用环境,要保证可靠的数据传输,就需要在上层(应用层)进行可靠性处理
第二十章 基本套接口编程
- 套接口是位于应用层与TCP/IP协议族通信的中间软件抽象层,它逻辑上位于传输层与应用层之间,实际上由一组网络编程API组成
- Socket可以看作是一种文件描述符。Socket也具有一个类似于打开文件的函数调用Socket(),该函数返回一个整型的Socket描述符,连接建立、数据传输等操作都是通过该Socket描述符实现的
- 套接口在网络层次中的逻辑位置

20.1.1 半相关与全相关
- 一个连接一旦建立,则必然包括:通讯协议、本地地址、本地端口号、远端地址、远端端口号五要素,这一组五个要素称为全相关,通讯协议、本地/远端地址、本地/远端端口号一组三个要素称为半相关
20.1.2 地址族与协议族
- 协议族定义了通信环境
- 一个协议族可以支持多个地址族,但目前实现中是一个协议族支持一个地址族
- 常见协议族
PF_UNIX/PF_LOCAL/PF_FILE:用于主机内进程间通信
PF_INET:ipv4网络通信协议,用于远程主机间通信
PF_INET6:用于ipv6网络通信
PF_IPX:用于Novell IPX网络通信
PF_X25:用于ITU-TX.25/ISO-8208网络通信 - 调用创建套接口的函数时,应该使用PF系列宏定义。在类似地址绑定的系统调用中,应该使用AP系列宏定义
20.1.3 面向连接与面向无连接
- 面向连接TCP,面向无连接UDP
20.1.4 套接口类型
- 创建套接口时,需要指定协议族和套接口的类型
- 流式套接口(SOCK_STREAM),提供面向连接、可靠的全双工数据传输服务。流式套接口通过TCP/IP协议实现
- 数据报式套接口(SOCK_DGRAM),提供无连接服务。数据报套接口通过UDP协议实现
- 原始式套接口(SOCK_RAW),该接口允许对较低层协议(IP、ICMP)进行直接访问。在某些应用中,使用原始套接口可以构建自定义头部信息的IP报文
20.1.5 字节序
- 大端小端字节序
小端字节序指低字节数据存放在内存低地址处,高字节数据存放在内存高地址处;大端字节序是高字节数据存放在低地址处,低字节数据存放在高地址处
unsigned int a=1;
//int4字节32位,char1字节8位,取a的低八位数据,
//等于1说明低位数据存到低地址处
if(1==*((char*)&a));
printf("小端");
else
printf("大端");
20.1.6 套接口连接方式
- 短连接方式是指在每进行一次通信报文收发交易时都要先建立连接,然后进行数据收发,收发完毕后立即断开连接
- 长连接方式是指client方与server方先建立好通讯连接,然后进行报文发送和接收。报文发送与接收完毕后,连接并不断开而继续存在,以便进行下一次的数据收发,因此长连接方式可以连续进行交易报文的发送与接收
- 短连接方式简单,不需要过多考虑长连接方式下的网络故障异常处理,其缺点是每次通信都要有建立连接的过程,处理效率上不如长连接。
- 长接连的优点是处理效率高,但是需要考虑各种网络异常的处理,程序逻辑比短连接相对要复杂
- 典型的并发服务器采用的是短连接方式。如果客户机与服务器之间通信频繁,并且对传输的时间要求较高,可以考虑釆用长连接方式
20.1.7 数据传输方式
- 同步数据传输,报文发送和接收是同步进行的,即报文发送后,发送方等待接收方处理完成并返回应答报文,同步方式需要考虑超时问题,报文发出后发送方需要设定超时时间,超时后发送方不再继续等待,而直接返回
- 异步数据传输,发送方只负责发送数据,不需要等待接收任何返回数据;而接收方只负责接收数据。通常情况下,异步方式在客户端和服务器端各有两个进程专门负责数据收发。这两个进程相互独立,互不影响
- 网络通信模型通常有同步短连接、同步长连接、异步长连接等
- 异步长连接通信模型

异步长连接包括两条连接。一条连接负责发送数据,另外一条连接负责接收数据。每个通信节点实际是由两个子进程组成。每个子进程负责维持一条通信链路。异步长连接接收和发送分别由不同的进程完成。但是,较高的通信效率也增加了控制的复杂度。数据的接收和发送是异步完成的,这就存在数据同步的问题
20.2.1 套接口地址结构
- 套接口地址结构包含了套接口的地址及端口等信息
- 在TCP/IP协议下进行网络编程使用的地址族是Internet地址族(INET),INET地址结构sockaddr_in位于/usr/include/netinet/in.h
struct sockaddr_in
{
_SOCKADDR_COMMON (sin_);//宏定义,指定地址族,等价于sa_family_t sin_family
in_port_t sin_port;//端口号,必须为网络字节序
//INET地址,必须为网络字节序。该成员是一个结构变量,
//结构中惟一的成员是IP地址,数据类型为uint32_t
struct in_addr sin_addr;
//为与通用套接口地址结构保持大小一致而填充的数据。
//在调用套接口编程时,往往需要将地址结构进行强制类型转换,
//转换为通用套接口地址结构进行传递参数。为满足这一要求,
//需要保持两个数据结构大小的一致。该成员内容应设置为空字符"\0"
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;//uint32位IP地址
};
20.2.2 通用套接口地址结构
- 在向套接口的编程接口函数传递地址结构指针时,需要将各不相同的地址结构转换为一个通用的数据结构sockaddr
struct sockaddr
{
_SOCKADDR_COMMON (sa_);//宏定义,指定地址族,等价于sa_family_t sa_family
//地址数据,对于INET地址族来说
//包括sin_port、sin_addr、sin_zero在内的全部地址数据
char sa_data[14];
};
20.2.3 主机名称数据结构
- 在进行网络编程时,通常需要用到主机名称(域名)与IP地址转换
- 主机名称数据结构hostent,该数据结构定义了主机名与IP地址的对应关系。在套接口编程模型中,与地址绑定相关的操作都需要使用该结构
struct hostent
{
char* h_name;//主机名
//主机别名列表。主机别名可能存在多个
//该成员为指向别名列表的指针
char** h_aliases;
//主机地址类型。在INET地址族下,该值一般为AF_INET
int h_addrtype;
int h_length;//地址的字节长度
char** h_addr_list;//以0结尾的数组,包含该主机的所有地址
#define h_addr h_addr_list[0];//在h_addr_list中的第一个地址(非点分十进制)
};
- int gethostname(char* hostname,int hostname_size)获取标准主机名到hostname中,可能获取的是别名,hostent中h_name是主机规范名,如标准主机名myhost,规范名为myhost.mydomain.com
- 用于获取hostent结构的接口函数有gethostbyname、gethostbyaddr及gethostent等,gethostbyname把主机名映射成IP地址而gethostbyaddr则相反
- char* int_ntoa(struct in_addr in)将字节序IP地址(结构体)转为点分十进制IP(字符串)
20.2.4 服务名称数据结构
- 网络服务配置文件/etc/services,其中定义了网络服务名称、服务别名、端口号和协议;该文件一为linux超级服务所使用;二是在进行网络编程使用;服务程序和端口需要绑定,若把端口写到程序中当需要更改端口时则要重新编译;通过该文件将服务程序和端口绑定则只需修改该文件中相应服务对应的端口即可
- 对/etc/services文件进行操作的一系列函数getservbyname、getservbyport需要服务名称数据结构作为参数
struct servent
{
char* s_name;//服务名称
char** s_aliases;//服务别名
int s_port;//端口号
char* s_proto;//协议名称
};
20.2.5 通用数据收发结构
- 数据收发函数send/sendto、recv/recvfrom
- 通用数据收发函数sendmsg、recvmsg
- 通用数据收发结构msghdr
struct msghdr
{
void* msg_name;//套接口名,PF_INET协议族中,该参数为指向套接口地址结构(sockaddr_in)的指针。该参数对于sendmsg来说是一个传入参数,对recvmsg来说则是一个值-结果参数
socklen_t msg_namelen;//套接口地址结构msg_name参数的尺寸
struct iovec* msg_iov;//读取或者接收数据缓冲区结构
size_t msg_iovlen;//msg_iov结构数组的元素个数
void* msg_control;//辅助数据
size_t msg_controllen;//辅助数据尺寸
int msg_flags;//接收数据的标志对sendmsg函数无效
};
struct iovec
{
void* iov_base;//输入输出缓冲区指针
size_t iov_len;//要读取或接收的数据长度
};
20.3.1 字节操作函数
- 套接口是TCP/IP协议在传输层之上向应用编程人员提供的网络接口,提供了字节操作函数、字节序操作函数、地址转换函数及基本套接口编程函数等接口函数
- 两组字节操作函数bzero、bcopy、bcmp和memset、memcopy、memcmp
原型:
//将缓存区中指定长度的内容淸空,通常用来初始化套接口相关的地址结构,memset函数也可实现
void bzero(void* _s,size_t _n);
//从源参数向目标参数复制指定长度数据允许源参数与目标参数相同,memcpy也可实现但不允许源参数与目标参数相同
void bcopy(_const void* _src,void* _dest,size_t _n);
/*bcmp和memcmp比较两个缓存区内容的差异。如果二个缓存区内容相同,则返回0;*/
/*如果不相同,则返回值根据两个参数不相同的第一个字节的内容进行确定*/
/*如果参数s1的相应字节值大于参数s2的相应字节值,则返回值大于0; 反之则小于0*/
int bcmp(_const void* _s1,_const void* _s2,size_t _n);
void* memset(void* _s,int _c,size_t _n);
void* memcpy(void* _restrict_dest,_const void* _restrict_src,size_t _n);
int memcmp(const void* _s1,_const void* _s2,size_t _n);
- bcopy、memcpy、strcpy的区别,strcpy如果拷贝的源数据中存在空字符"\0",则strcpy将只能拷贝至空字符前的位置,而bcopy、memcpy忽略空字符根据指定长度进行拷贝
- 经过volatile限定声明的变量表示该变量的值随时可能被外部事件所更改,在运行过程中取值时,一定会从该变量的地址中获取
20.3.2 字节序操作函数
- TCP/IP协议规定采用大端字节序
- 在发送端统一将主机字节序转化为网络字节序。在接收端,则将网络字节序转换化主机字节序。至于主机字节序到底是大端字节序还是小端字节序,在应用程序中完全可以不必关心
- 字节序转换函数(host->network,short,long类型)htons、htonl,ntohs、ntohl
- 向套接口数据结构中赋值时,用主机字节序转换至网络字节序函数;从套接口数据结构中获取值时,用网络字节序转换为主机字节序函数
原型:uint16_t htons(uint16_t _hostshort);
uint32_t htonl(uint32_t _hostlong);
uint16_t ntohs(uint16_t _netshort);
uint32_t ntohl(uint32_t _netlong);
向套接口数据结构中赋值时,用主机字节序转换至网络字节序函数;从套接口数据结构中获取值时,用网络字节序转换为主机字节序函数,short用于网络端口的转换,long用于网络地址的转换
20.3.3 地址转换函数
- 在向套接口地址结构赋值时需要从点分十进制方式转换为套接口能够识别的数据类型,有时又相反
- 点分十进制转换为套接口内部数据类型函数有inet_addr、inet_aton(IPv4),inet_atop(IPv4/6),inet_addr和inet_aton功能相同,但inet_addr错误返回INADDR_NONE,这个值与地址"255.255.255.255"(广播地址)进行转换后的内部数据值是相同的,程序将无法判断到底是返回失败还是进行了"255.255.255.255"地址的转换,建议使用inet_atop
- 套接口内部数据类型转换为点分十进制函数有inet_ntoa、inet_ntop
- 点分十进制转换为套接口内部数据函数原型
原型:in_addr_t inet_aton(_const char* _cp, struct in_addr* _inp);
参数:in_addr_t32位无符号整数,cp输入参数,点分十进制IP或主机名;inp输出参数,指向in_addr结构的指针。该结构中存储网络字节序地址数据
struct in_addr
{
unsigned long int s_addr;
};
原型:int inet_pton(int _af,_const char* _restrict_cp,void* _restrict_buf);
参数:af输入参数,地址族名称如AF_INET;cp输入参数,点分十进制IP或主机名;buf输出参数,指向的内容将存储网络字节序地址数据,IPv4版本可以用指向struct in_addr的结构指针作为该参数
原型:in_addr_t inet_addr(_const char* _cp);
参数:cp输入参数,点分十进制IP或主机名;返回网络字节序地址数据 - 套接口内部数据类型转换为点分十进制函数原型
原型:char* inet_ntoa(struct in_addr _in);
参数:in输入参数,套接口数据地址数据,返回值为点分十进制表示的字符串指针
原型:_const char* inet_ntop(int _af, _const void* _restrict_cp,char* _restrict_buf, socklen_t len);
参数:af输入参数,地址族名称如AF_INET;cp输入参数,指向地址数据;buf输出参数,调用成功后,该字符串中将保存点分十进制IP地址;len输入参数,标明buf缓冲区的尺寸,根据IP版本的不同直接引用宏定义即可;返回指向buf的字符串指针 - 特殊IP地址INADDR_ANY(任意地址,0.0.0.0)、INADDR_BROADCAST(广播地址)、INADDR_LOOPBACK(回环地址),在建立套接口服务器前,都要在某个IP地址的某个端口建立监听,以便允许客户端向这个地址发起连接
- inet_pton()函数将点分十进制地址数据的网络字节序赋给struct sockaddr_in中的sin_addr;htonl可以将主机字节序IP地址转为网络字节序赋给struct sockaddr_in中的sin_addr.s_addr,点分十进制不是主机字节序
20.3.4 套接口函数
- socket函数创建套接口,套接口可以看作是一个文件。对文件操作的某些系统调用(read、write)可以直接应用在套接口中。从linux内核来看,建立套接口的操作实际上是在内核中分配了一块数据结构,并根据套接口的类型等参数对其进行初始化
原型:int socket(int _domain,int _type,int protocol);
参数:domain输入参数,域/协议族名称如PF_INET;type输入参数,套接口类型SOCK_STREAM、SOCK_DGRAM、SOCK_RAW;protocol输入参数,指定在当前协议族中采用的具体协议类型,可以直接输入0作为参数,表示采用符合
domain和type类型的默认协议,返回int型套接口描述符

- 在调用套接口相关函数时,需要将某种(INET)类型的套接口地址结构进行强制类型转换为通用套接口地址结构
- bind函数在服务器端为套接口绑定IP地址和端口,IP地址必须为本机的一个IP可以设为INADDR_ANY
原型:int bind(int _fd,_CONST_SOCKADDR_ARG _addr,socklen_t _len);
参数:fd输入参数,套接口描述符,由socket函数返回;addr输入参数,指向通用套接口地址结构的指针,_CONST_SOCKADDR_ARG是const struct sockaddr* 的宏定义,对于PF_INET协议族来说,该参数变量的类型为struct sockaddr_in类型的指针,传入bind函数时强制转换为struct sockaddr类型指针;len,输入参数,addr地址结构的长度 - 在对struct sockaddr结构赋值时可将sin_addr赋为INADDR_ANY,系统将选择本机上的所有的IP地址进行绑定
- 若将结构中sin_port赋为0,则将由系统随机分配端口号。通常情况下,只有在建立服务器端应用时需要调用bind, 而服务器程序大多需要明确指定监听的端口,一般不会对该值赋0
- 作为客户端的套接口一般不需要调用bind绑定地址和端口,而是由系统自动分配一个随机的可用端口进行绑定。如果明确要求客户端必须在某个端口上与服务器发起连接,则客户端也可调用bind
- sockaddr_in结构中的sin_addr、sin_port数据都是网络字节序
- connect函数用于建立与TCP服务器的连接一般用于客户端编程中,实际上在TCP协议中发起了三次握手的过程
原型:int connect(int _fd,_CONST_SOCKADDR_ARG _addr,socklen_t _len);
参数:fd输入参数,套接口描述符,由socket函数返回;addr输入参数,指向通用套接口地址结构的指针,变量中存储的值是要连接的服务器的配置信息,如服务器的IP地址端口号;len,输入参数,addr地址结构的长度 - netstat查看端口上是否已启动服务器LISTEN为启动
- listen函数用于在指定套接口启动监听,只在服务器端编程中调用,在一个套接口上调用该函数后,该套接口的状态将变为LISTEN,socket函数默认创建主动套接口即用于主动发起连接的客户端套接口,调用listen套接口变为被动套接口即被动接受连接的服务器端套接口
原型:int listen(int _fd,int _n);
参数:fd输入参数,套接口描述符,由socket函数返回:n输入参数,backlog值,指定排队等待接受连接的最值,可由socket.h中宏定义SOMAXCONN指定 - listen应在bind后调用
- backlog也称为积压值。在套接口底层的实现中,该参数是规定了TCP层接收链接的缓冲池(或者称为队列)的最大个数。当服务器端的TCP层接受客户端连接后,该连接就进入队列排队。如果此时应用层调用accept接受连接,TCP层将把该队列中相应的连接移除
- accept函数服务器端调用,从listen等待接受连接队列中取出一个完成连接,返回由内核产生的新套接口用于与客户端通信(新套接口与客户端建立了连接)
原型:int accept(int _fd,_SOCKADDR_ARG _addr,socklen_t* _restrict_addr_len);
参数:fd输入参数,套接口描述符,由socket函数返回:addr输出参数,指向通用套接口地址结构的指针,该变量中将返回与服务器连接的客户端配置信息,如客户端的IP地址、端口号等,对于PF_INET协议族来说,该参数变量的类型应为struct sockaddr_in类型的指针,在调用accept传参时,强制类型转换为struct sockaddr*类型;addr_len输入/输出参数,在调用accept时,该值为结构_addr结构的尺寸,在函数返回时,该值为返回的_addr结构的尺寸 - 若不需要客户端信息可将addr,addr_len置NULL
- 如果当前连接队列中没有准备好的连接,accept调用将阻塞,直至有连接请求到达
- 在并发服务器程序设计中,为保证服务器可以同时接受并处理多个客户端连接,通常采用多进程或者多线程技术。每次调用accept接受到一个客户端连接后,派生出一个新的子进程(线程),由新进程负责处理与客户端的数据收发处理。而主进程则继续调用accept接受下一个连接
- _addr_len参数既用来向accept传入_addr结构的尺寸(从进程传入内核),又用来返回值(从内核返回至进程),这种参数称为值-结果参数
- send和recv函数
原型:ssize_t send(int _fd, _const void* _buf, size_t _n, int _flags);
ssize_t recv(int _fd, void* _buf, size_t _n, int _flags);
参数:fd输入参数,套接口描述符,socket或/accept返回;buf输入/输出数据,对于send函数来说,该参数是输入参数,其中存储了要发送的数据,对于recv来说,该参数是输出参数,用于缓存接收到的数据;n输入/输出数据,表明要发送或者接收的数据长度;flags输入数据,标志位,表明send或recv调用的标志,为0表示不采用标志,可为MSG_DONOTWAIT、MSG_OOB、MSG_PEEK等,MSG_DONOTWAIT标志本次数据收发操作采用不阻塞模式,返回收/发的数据字节数 - 无论套接口是客户端套接口还是服务器套接口,这两个函数都用于在流式套接口中发送和接收数据,read和write也可完成收发数据,但不能指定标志参数,其他功能与send和recv相同
- send函数并不是真正地向网络对端发送数据, 发送数据的过程是由TCP协议完成的。send只是检査套接口的发送缓存区是否有足够的空间,如果空间足够放入本次发送的数据,则直接将要发送的数据拷贝至发送缓存区,send就成功返回;如果发送缓存区空间不足,则等待TCP协议发送完缓存中原有的数据后,再将要发送的数据拷贝至发送缓存区
- recv函数也不直接从网络对端中接收数据。该函数只是从套接口的接收缓存区中读取由TCP协议接收到的数据。在调用recv时,如果接收缓存区为空,则recv将阻塞直至有数据到达
- fcntl设置套接口为非阻塞模式
- sendto函数和recvfrom函数用于数据报类型套接口的数据收发
原型:ssize_t sendto(int _fd, _const void* _buf, size_t _n, int _flags, _CONST_SOCKADDR_ARG _addr,socklen_t _addr_len);
ssize_t recvfrom(int _fd,void* _restrict_buf, size_t _n,int _flags, _SOCKADDR_ARG _addr,socklen_t* _restrict_addr_len);
参数:fd输入参数,套接口描述符,由socket/accept返回;buf输入/输出参数,对于sendto来说,该参数是输入参数,其中存储了要发送的数据,对于recvfrom来说,该参数是输出参数,用于缓存接收到的数据;n输入/输出参数,表明要发送或者接收的数据长度;flags输入数据,标志位,见send函数;addr输入/输出数据,指向struct sockaddr结构的指针,对于sendto来说,是输入参数,存储了发送数据的目标地址。对于recvfrom来说,是个值-结果参数;addr_len输入/输出数据,表示addr结构的长度。对于recvfrom来说,该参数是一个值-结果参数。注意:在recvfrom函数中要传入的参数是一个指针 - recvfrom不仅可以应用于面向无连接模式的数据报接收,而且也可以用于面向连接的数据接收。如果调用recvfrom时addr参数传入NULL,则等同于调用recv。sendto和send的关系也是如此
- 通用数据收发函数sendmsg和recvmsg,仅用于流式/数据抱式套接口收发数据
原型:ssize_t sendmsg(int _fd, _const struct msghdr* _message,int _flags);
ssize_t recvmsg(int _fd,struct msghdr* _message, int _flags);
参数:fd输入参数,套接口描述符,由socket/accept返回;message输入/输出数据,指向msghdr结构的指针。对于sendmsg该参数指定发送的目标及数据内容,对于recvmsg该参数是一个值-结果参数。调用成功时,该参数中保存由内核返回的相关数据;flags同send/recv - close函数和shutdown函数用于关闭套接口。close是与文件操作的close共用的函数, shutdown则是套接口专用的函数
- close关闭套接口后,该套接口既不能用于读取数据,也不能用于发送数据
原型:int close(int _fd); - shutdown函数可以在关闭套接口时指定只关闭一个方向(读或者写)的通信或者关闭双向通信
原型:int shutdown(int _fd,int _how);
参数:fd输入参数,套接口描述符,由socket/accept返回;how:输入数据,表明套接口关闭的方式 - shutdown函数关闭套接口时, 不受套接口引用计数问题的限制, 它将直接关闭套接口,close只是将套接口引用计数减1
- close函数关闭套接字时,如果有其他的进程共享着这个套接字,那么它仍然是打开的,这个连接仍然可以用来读和写,并且有时候这是非常重要的 ,特别是对于多进程并发服务器来说。而shutdown会切断进程共享的套接字的所有连接,不管这个套接字的引用计数是否为零
shutdown和close区别 - shutdown函数只对而向连接的套接口有效, 面向无连接的套接口应使用close
- close关闭套接口后,该套接口既不能用于读取数据,也不能用于发送数据
- getsockname函数和getpeername函数用于获取与套接口关联的本地连接信息及远程信息
原型:int getpeername(int _fd, _SOCKADDR_ARG _addr,socklen_t* _restrict_len);
int getsockname(int _fd, _SOCKADDR_ARG _addr,socklen_t* _restrict_len);
参数:fd输入参数,套接口描述符,由socket/accept返回;addr输入/输出参数,指向通用套接口地址结构struct sockaddr的指针,调用成功后,该参数中保存本地或者远程网络字节序地址信息;len值-结果参数
20.4.1 套接口选项函数
- getsockopt和setsockopt函数获取或设置套接口选项
原型:int getsockopt(int _fd,int _level,int _optname,void* _restrict_optval,socklen_t* _restrict_optlen);
int setsockopt(int _fd, int _level, int _optname, _const void* _optval,socklen_t _optlen);
参数:fd输入参数,套接口描述符,由socket/accept返回;level输入数据,选项级别,该参数指定了在何种级别上对套接口选项进行操作,这里的级別可以理解为协议的层次,主要的级別包括SOL_SOCKET、IPPROTO_IP,lPPROTO_TCP等;optname输入参数,选项名称,在SOL_SOCKET选项级別下,主要的选项名称包括SO_KEEPALIVE、SO_LINGER、SO_RCVBUF、SO_SNDBUF、SO_RCVTIMEO、SO_SNDTIMEO、SO_REUSEADDR、SO_REUSRPORT等;optval输入/输出数据,指向选项值的指针,对于getsockopt来说,该值是值-结果参数,调用成功后,该参数中保存套接口的当前选项值,对于setsockopt来说,该值是输入参数,用于设置新的套接口选项值。该参数多数情况下为整形变量,但在某些选项中,该参数也可能为结构变量,如SO_LINGER选项;optlen值-结果参数,表示optval的尺寸 - 在调用socket创建套接口成功后,才能在套接口上获取或者设置选项
20.4.2 SO_KEEPALIVE选项
- SO_KEEPALIVE选项处理客户机长时间不收发数据导致服务器不明客户机是否还在活动状态。如果客户机已经宕机或者进程已经异常终止,会导致服务器端产生一个半开状态的连接,浪费资源,降低服务效率
- 套接口提供SO_KEEPALIVE选项探测对方主机是否崩溃。在套接口上打开该选项后如果在2个小时内没有任何数据收发,系统将自动向对方发送一个探测包。对方的TCP协议在收到这个探测包后,必须进行响应
- 对方回复以明确的响应。表明该连接一切正常。在下一个2小时内继续按照该规则发送探测包。这种情况对应用层没有任何影响
- 对方回复系统已崩溃或者重启的信息。此时,本地应用层的套接口将返回ECONNRESET错误。如果在套接口上调用select函数或者read函数等待读取数据,则很容易捕获到该错误信息。应用层根据应用的要求做出适当处理
- 对方未对探测包做出任何响应。在这种情况下,不同版本的套接口在实现上略微有差异。多数的做法是每隔75秒发送一个探测包,连续发送8个。如果在第一个包发出后超过11分15秒仍然无响应, 则应用层套接口将捕获到错误信息ETIMEOUT。应用层需要根据应用的要求做出适当处理
20.4.3 SO_LINGER选项
- 调用close关闭套接口TCP/IP协议在默认状态下,会将底层协议中发送缓冲区的数据发送完成后,再关闭连接,SO_LINGER选项用于修改这种默认行为
- SO_UNGER选项的获取和设置需要用到struct linger结构
struct linger
{
int l_onoff;
int l_linger;
};
l_onoff为0表示按照默认行为执行;
l_onoff为非零值且l_linger为0表示调用close关闭套接口时,该套接口立即关闭,底层协议中发送缓冲区中的数据将会丢失;
l_onoff为非零值且l_linger为非零值调用close后,如果发送缓冲区中还有数据尚未发送完成,则TCP/IP协议将继续进行发送,直至数据发送完成或者l_linger设置的时间已超时。如果出现超时的情况,则close将返冋失败,同时错误代码errno被置为EWOULDBLOCK, 底层协议中发送缓冲区中未发送完成的数据将全部丢失
*/
| l_onoff | l_linger | 模式 |
|---|---|---|
| 0 | / | 默认模式 |
| 非零 | 0 | 立即关闭套接口 |
| 非零 | 非零 | 直至数据发送完毕 |
20.4.4 SO_RCVBUF和SO_SENDBUF选项
- TCP/IP协议的底层为每一个套接口建立了两个缓冲区,分別是发送缓冲区SO_SENDBUF及接收缓冲区SO_RCVBUF
- SO_SENDBUF和SO_RCVBUF大小一般是4096-8192字节,在每次收发数据量比较大的情况下,可以调整两参数的大小
- 对于客户端套接口,应该在调用connect之前进行设置;对于服务器端套接口,应该在调用listen之前进行设置。在底层协议的实现中,该选项是在建立连接的过程中设置的,因此必须在套接口建立连接之前设置缓冲区大小
- TCP/IP协议存在滑动窗口机制,发送方在发送数据前会先査看接收方的接收缓存区大小,根据该大小发送数据。因此,接收缓存区永远不会溢出
20.4.5 SO_RCVTIMEO和SO_SNDTIMEO选项
- 在阻塞模式下,可以为套接口设置读写操作的超时时间,在默认状态下,这两个参数都是禁止的,即不设置超时时间
- 设置超时时间需要timeval结构
struct timeval
{
_time_t tv_sec;//以秒为单位的超时时间
_suseconds_t tv_usec;//以微秒为单位的超时时间
};
20.4.6 SO_REUSEADDR和SO_REUSEPORT选项
- 默认情况下如果一个套接口绑定了某个端口,则该端口将无法再绑定到其他套接口上。在linux系统中,套接口正常关闭或程序退出后,在一段时间内该端口将仍然保持被绑定的状态,其他程序(或者重新启动原程序)无法绑定该端口。为避免出现该情况,可以通过设置SO_REUSEADDR和SO_REUSEPORT选项解决
- 服务器出现故障后停止服务器并重新启动。此时,如果不设置该选项,由于刚刚停止的服务器所持有的TCP连接仍然处在TIME_WAIT状态,在进行绑定(bind)操作时将报错。设置该选项则可以保证服务器立即启动成功
20.5.1 重复服务器编程
- 重复服务器指的是服务器在接收客户机的连接请求后即与之建立连接,在处理完与客户机的通信任务后断开连接,再去接收另一客户机的连接请求,重复服务器每次只能有一个客户机与服务器建立连接
20.5.2 并发服务器编程
- 并发服务器实现方式有两种:一是通过子进程并发的方式实现并发服务器;二是通过IO多路复用实现并发服务器
- 每次fork()后套接口引用计数加一,需要在子进程中关闭旧监听套接口,完成业务后关闭accept返回的新套接口(P360程序逻辑)
20.6.1 UDP编程模型
- UDP服务器不会出现一个客户端占用连接后,其他客户端无法连接的情况,因此普通的循环(重复)服务器基本上已能满足并发应用的需要
20.6.2 UDP客户/服务器编程P363
- 服务端编程流程
- socket()创建数据报套接口
- 为sockaddr_in结构赋服务端程序要绑定的地址和端口等值
- bind()绑定地址及端口信息
- recvfrom()接收来自客户端的数据报文并保存发送方地址及端口
- 进行交易逻辑处理
- sendto()向客户端发送数据
- close()关闭套接口
- 客户端编程流程
- socket()创建数据报套接口
- 为sockaddr_in结构赋sendto要发送到的IP地址和端口
- sendto()发送数据报文
- recvfrom接收服务器发送的数据
- close()关闭套接口
20.6.3 TCP/IP网络编程小结P358
- 并发服务器服务端编程流程
- socket()创建套接口
- setsockopt()设置SO_REUSEADDR选项
- 为sockaddr_in结构赋服务端程序要绑定的地址和端口等值
- bind()绑定sockaddr_in结构
- 设置要安装的信号处理函数
- 安装子进程处理信号
- listen()监听连接
- 开始业务循环
- accept()接受连接并返回新的数据收发套接口
- fork()子进程(各套接口引用计数加一)
- 子进程close()关闭第一步创建的套接口使其引用计数减一
- recv()接收新套接口发送的数据
- 处理接收的数据(业务处理)
- send()向客户端发送数据
- 关闭新套接口并退出
- 父进程处理流程
- close()关闭新套接口
- 重复业务循环,接收下一连接
- 并发服务器客户端编程流程
- socket()创建套接口
- 为sockaddr_in结构赋客户端程序要连接的地址和端口等值
- connect()连接服务器
- send()向服务端发送数据
- recv()接收服务端发送的数据
- close()关闭套接口
第二十一章 综合实例——银行代理收费服务器
21.2 程序实现
- 程序相关参数本地服务器的地址和监听端口、被代理单位的服务器地址和监听端口保存到配置文件中,文件内容格式:
本地服务器IP 本地监听端口 被代理单位服务器IP 被代理单位服务器监听端口 - 命令行管理
-s 启动系统,-x 退出系统,二者必选一个;-c 必选项,指定配置文件名称 - 通讯报文格式
报文内容前面使用4个字节的报文长度字段。该长度字符所指定的长度不包括长度字段本身的长度 - commserv.h
// 日志记录函数
void errorlog(char* m,...);
// 可变参数,初始化一个va_list(宏定义,可能是一个字符指针)类型变量para,va_start(para,m)后para指向m后的第一个参数,使用完后va_end(para)释放资源
// 格式化时间处理函数,时间相关库函数time()、localtime()等
char* handletime();
// 解析命令行参数。optarg是getpt_long()定义的外部全局变量
getopt_long()
// 信号量控制函数根据控制参数的不同返回不同的非负值如进程ID、信号量值
semctl()
// P374为什么这么比较,gethostname返回标准主机名如arange,
strcmp(mhost,lhost)&&strcmp(mhost,ip)
// 设置新进程组,将目前进程所属的组识别码设为目前进程的进程识别码。此函数相当于调用setpgid(0,0)
setpgrp()

浙公网安备 33010602011771号