赞
踩
1、GNU
GNU计划,又称革奴计划,是由Richard Stallman在1983年9月27日公开发起的。它的目标是创建一套完全自由的操作系统。
GPL:GNU通用公共许可证(GNU General Public License)自由软件关乎使用者运行、复制、发布、研究、修改和改进该软件的自由。而不是免费。
GPL:
1.它要求软件以源代码的形式发布,并规定任何用户能够以源代码的形式将软件复制或发布给别的用户。
2.如果用户的软件使用了受 GPL 保护的任何软件的一部分,那么该软件就继承了 GPL 软件,并因此而成为 GPL 软件,也就是说必须随应用程序一起发布源代码。
3.GPL 并不排斥对自由软件进行商业性质的包装和发行,也不限制在自由软件的基础上打包发行其他非自由软件。
自由软件开源证书
GPL GNU 通用公共许可证(GNUGeneral Public License, GPL),为用户提供了三种基本权利:
1、 复制和分发软件的权利
2、 修改软件的权利
3、 获得源码的权利
任何对GPL软件进行的改动都为GPL涵盖,即改动后的软件必须以源码的形式向其他人公开。“感染效应”
LGPL(Library GPL):“较宽松通用公共许可证”降低了GPL自由度,以LGPL发布的库可以被专利软件调用而不“感染”专利软件(即专利软件不必开发源码)。
Qt公共许可证(QtPublic License, QPL):破坏了开放性,分两种:自由许可证和商业许可证。
对于自由版本,任何与其连接的软件都必须按照QPL或GPL开放;对商业许可证,使用Qt框架开发应用程序,并将其封闭(不对开源社区开放程序源码)。
BSD许可证:更有利于商用,BSD社区鼓励开放修改后的源码,但没有强制要求。
Linux文件系统
/bin 存放对系统运行极为重要的二进制可执行文件。其中一些文件为/usr/bin目录中文件的符号链接,同时还有一些用户命令。为方便用户使用,安装程序时将可执行文件的符号链接放置该目录下,使用户不用设置可执行文件的路径。
/etc 存放系统配置文件。该目录下只存放静态文件,并且不能包含可执行的二进制文件。
/home 存放各个用户相关的文件。由于不同主机安装linux后该目录根据用户名不同存在差异,因此不能有程序依赖该目录。
/tmp 临时目录,很多程序在该目录下创建临时文件。
/var 存放经常变化的信息,用于log日志、邮件、spool文件等的存储
/proc 伪目录,系统将当前运行的进程映射为文件。
/dev 设备目录,包含特殊文件或设备文件
/boot 存放启动过程中需要的所有文件,包含linux内核和引导配置文件
/usr 包含用户相关的程序和文件
/sbin 存放与系统相关的可执行文件
Linux系统结构
1、 用户程序
用户程序通过系统调用接口与linux内核发生交互,以实现具体功能。
2、 Shell
命令解析器,按照一定的规则将输入的命令加以解释并传给系统,是用户和操作系统之间交互的平台。Linux采用Bash作为默认的命令解析器。
3、 库函数
库函数为程序员提供编程接口,通过对系统调用的再次封装,提供了比系统调用更方便的功能。
4、 linux内核
linux内核主要包括进程调度、内存管理、虚拟文件系统和进程间通信。
5、 设备驱动
设备驱动负责驱动系统的相关硬件,使其发挥作用。
Linux系统为整体式系统,即内核为用户进程提供所有的服务,如内存管理、文件管理、进程管理等(相对于微内核操作系统:内核很小,只是一个中转站,将用户进程提出的服务要求转发给相应的内核外部进程来完成)
GNU系统库(glibc)
Glibc 是一个实现标准C库函数的可移植的库,包括上半个系统的调用。应用程序与GNU C库连接来访问Linux内核和一些常用的函数。
构建应用程序时,GNU编译器首先自动将符号解析为GNU glibc(如果能够解析的话),在运行时再使用glibc共享对象的动态链接对其进行解析。
嵌入式系统开发中,使用标准C库有时候会出现问题。使用 –nostdlib,GCC 可以取消自动按标准C库进行符号解析的设置,使得开发人员可以将标准C库中的函数重写为自己的版本。
内核组件
Init组件: 在linux启动时运行,通过start_kernel函数提供内核的主要入口,该函数的架构依赖性很强,因为不同的处理器架构有着不同的初始化要求。
完成硬件和内核组件的初始化后,init组件打开初始化控制台(/dev/console),启动init进程,init进程是所有GNU/Linux进程的根进程,该进程没有父进程(其他所有进程都有父进程)。Init进程开始后,系统初始化的控制转移到内核本身之外。
进程调度器:linux内核提供抢占式调度器来管理系统中运行的进程。
存储管理器: 提供由物理存储向虚拟存储的映射功能(及其逆映射)及物理磁盘的分页和交换功能。Linux存储管理依赖于处理器的架构。
在内核保持其拥有的虚拟地址空间的同时,用户空间中的每个进程都拥有各自独特的虚拟地址空间。
虚拟文件系统:linux内核的虚拟文件系统VFS是一个抽象层,给不同的文件系统提供一个统一的视图,linux的虚拟文件系统VFS为不同的文件系统提供了一个接入的通用函数(如open, write, close, read等)的抽象层。
VFS还提供设备驱动程序的接口,通过这样的接口可以调制写入媒质中的数据。VFS提供了一个统一的视图,与硬盘(或其他媒质)的类型无关。
网络接口:
网络接口(BSD Socket API) |
核心服务(Inet Socket等) |
网络设备(Ethernet 等) |
网络协议(IPv4等等) |
进程间通信(IPC)
IPC组件提供标准System V IPC服务,包括旗语、消息队列、共享内存。IPC的所有元素采用一个通用接口。
可选加载模块:
为GNU/Linux的一个重要模块,提供了动态改变内核的方法。内核的封装可以做的更小,再根据实际情况动态加载所需要的模块。
设备驱动程序:
设备驱动程序组件提供大量设备的驱动程序。几乎一半的linux内核程序用于设备驱动程序。
架构依赖代码:
架构依赖代码位于内核的最底层,Linux能支持各种硬件平台,就拥有面向对应的架构族和处理器的相关程序。
Linux目录
Windows操作系统通过设备标识符来访问文件系统,在windows中我们常见的硬件设备、磁盘分区等,在linux、unix中都被视作文件,对设备、分区的访问就是读写对应的文件。
linux操作系统将具体的设备挂在到一个独立的树形层次结构(目录)中,在linux中,无论分区采用何种文件操作系统,都将挂载到某个目录上。对同一目录挂载不同的分区后,挂载的文件目录将覆盖原来的内容,只有将文件系统卸载后,才能看到挂载在原有目录的内容。【当你挂载某个设备到一个VFS挂载点上时(比如/home),系统就把VFS中的这个挂载点/home指向你最后所挂载的那个设备上。那么你现在访问该挂载点时,就会看到你最后挂载在此处的设备。而之前所挂载的设备依然在那里,只不过挂载点/home已经不再指向之前的设备。你可以把原来的设备卸载以后挂载到一个新的挂载点上来访问。)
SATA
SATA全称是SerialAdvanced Technology Attachment(串行高级技术附件,一种基于行业标准的串行硬件驱动器接口),是由Intel、IBM、Dell、APT、Maxtor和Seagate公司共同提出的硬盘接口规范。
IDE
IDE的英文全称为“IntegratedDrive Electronics”,即“电子集成驱动器”,它的本意是指把“硬盘控制器”与“盘体”集成在一起的硬盘驱动器。
hda一般是指IDE接口的硬盘,hda一般指第一块硬盘,类似的有hdb,hdc等
sda一般是指SATA接口的硬盘,sda一般指第一块硬盘,类似的有sdb,sdc等
现在的内核都会把硬盘,移动硬盘,U盘之类的识别为sdX的形式
SATA硬盘
使用SATA(Serial ATA)口的硬盘又叫串口硬盘,是未来PC机硬盘的趋势,现已基本取代了传统的PATA硬盘。
PATA硬盘
PATA硬盘叫做并行ATA硬盘,采用的是一根四芯的电源线和一根80芯的数据线与主板相连接,把数据并列传输和成列(串)传输。传输速率由于受到并行传输的限制,传输率较低,PATA硬盘是不需要安驱动的。
IDE接口就是PATA接口,指硬盘与主板间连接的方式。不过IDE不仅指接口形式,主要还指硬盘的形式,即IDE硬盘,但人们习惯用IDE来统称PATA接口类的硬盘。
虚拟文件系统 VFS (virtual file system)
VFS的基本思想是将各种文件系统的公共部分抽取出来,形成一个抽象层。对用户程序而言,VFS提供了文件系统的系统调用接口。而对具体的文件分区格式而言,VFS通过一系列同一的外部调用接口来屏蔽实现细节,使得文件系统的调用不用关心底层的存储介质和文件系统类型。
Linux系统初始化时,首先会在内存中初始化VFS目录树。VFS目录树不等同于文件系统目录树,VFS目录树的主要用途是给实际的文件系统提供挂载点。即使用mount命令将某个设备挂载到某个目录下,实际是挂载到了内存中的VFS目录树上。
Linux文件的实现
在linux系统中,实现文件的存储和相关信息保存的核心是索引节点(inode)结构,每个inode结构中存储有文件的属性、访问权限及文件数据块的位置。Inode就是文件系统定位文件的基本途径。
Inode结构包含:
Mode(包含inode的描述内容和用户的访问权限)
Owner info :文件或目录所有者的信息,包括所属组的信息
Size 用于记录文件的大小,以字节为单位
Timestamps: 时间戳,用于记录inode的创建时间和最后修改时间
Inode包含有15个块指针的数组,每个块指针32位,用于指向文件所在的数据块的位置,文件所占用的数据块都将记录在该数组中。
前12块为直接块指针:直接指向文件所在的存储块
第13块为间接块指针:指向存放有块指针的数据块,这些数据块再指向文件数据块。
第14块为双重间接指针,指向的数据块中存放着块指针,这些快指针指向了用于存放块指针的数据块,最后块指针指向文件数据块。
第15块为三重间接指针
1、符号链接
符号链接又叫软链接,是一类特殊的文件,这个文件包含了另一个文件的路径名(绝对路径或者相对路径)。路径可以是任意文件或目录,可以链接不同文件系统的文件。(链接文件可以链接不存在的文件,这就产生一般称之为”断链”的现象),链接文件甚至可以循环链接自己(类似于编程中的递归)。在对符号文件进行读或写操作的时候,系统会自动把该操作转换为对源文件的操作,但删除链接文件时,系统仅仅删除链接文件,而不删除源文件本身。
符号链接的操作是透明的:对符号链接文件进行读写的程序会表现得直接对目标文件进行操作。某些需要特别处理符号链接的程序(如备份程序)可能会识别并直接对其进行操作。
GCC GNU 编译工具链(GCC, GNU Complier Collection), 是GNU/Linux上标准的编译器,也是嵌入式系统开发的标准编译器。GCC支持大量不同的目标架构。
GNU编译器生成目标可执行文件的过程: 预编译---编译---汇编----链接(编译和汇编常视为一个阶段)。
各个编译阶段的输入和输出
阶段 | 输入 | 输出 | GCC示例 |
预编译 | *.c | *.i | Gcc –E test.c –o test.i |
编译 | *.i | *.s | Gcc –S test.i –o test.s |
汇编 | *.s | *.o | Gcc –c test.s –o test.o |
链接 |
| *.o | * GCC test.o –o test |
-o为输出选项,指定输出的文件,没有则为默认。
预处理阶段,源文件(*.c)和头文件(*.h)一起进行预处理,对#ifdef,#include, #define等预编译指令进行戒心,生成一个中间文件(通常不需要外部生成该文件);编译阶段,*.i文件经过编译生成*.s文件;汇编阶段,文件转化为相应的机器指令,生成目标文件(.*o);最后机器码连接起来(有可能与其他机器码对象或对象库连接)形成可执行的二进制文件。
GCC的格式(编译、汇编和连接)
1、 将一个C源程序文件编译为一个映像,假设所有的源程序都包含在一个文件里:
Gcc test.c -o test //将test.c文件直接生成可执行文件,将生成的可执行映像保存在test中。(-o 调整输出选项)
2、 如果只希望生成目标文件(目标文件编译器编译源代码后生成的文件,目标文件从结构上讲,它是已经编译后的可执行文件格式,只是没有经过链接的过程,其中可能有些符号或游资哦地址没有被调整。其实本身是按照可执行文件格式存储的。(链接的关键->符号表)),则可以使用编译选项:
GCC –c test.c //只生成目标文件(test.o)
3、 处理多个文件的情况
GCC –o image first.c second.c third.c //将first.c,second.c third.c 编译并连接生成一个名为image的可执行文件。在这些生成可执行文件的例子里,所有的C程序都需要一个main函数,在所有将要编译连接到一起的文件中应当只出现一次。
有用的选项:
1、若包含头文件的目录和保存源文件的目录不是同一个,可以通过-I选项指定头文件的目录。例如源程序文件保存在./src文件夹中,头文件保存在./inc文件夹中,在./src目录中进行编译:Gcc test.c –I ../ inc –o test
可以用—I选项指定多个包含子目录:
Gcc test.c –I ../inc -I ../../inc2 –o test
编译警告;
GCC编译器可以设置编译时需要打开或关闭的编译警告,通过编译选项设置编译警告类型:
Gcc –Wall test.c –o test //-Wall 打开所有编译警告
Gcc –Wall –Wno –unused test.c –o test //打开所有的警告选项,同时关闭unused警告集。
-Werror 该选项使得编译器将所有的警告都视作错误来处理,及报告问题,退出编译过程,可以确保代码的高质量。
GCC优化器
Gcc优化器有三个优化目标:
1、 文件更小、速度更快
2、 文件更小、允许速度变慢
3、 速度更快,允许文件变大
优化措施及说明
优化等级 | 说明 |
-O0 | 不进行优化、默认设置 |
-O, -O1 | 尝试同时缩短编码时间和减小映像大小 |
-O2 | 比-O1更多的优化,但只在不导致文件增大时进行加快速度的优化,只在速度变慢时进行减小文件的优化 |
-Os | 以减小结果文件大小为优化目标(所有-O2打开的优化,可能导致文件增大的除外) |
-O3 | 更多的优化(所有-O2打开的优化,再打开另外两个优化选项) |
通过在GCC命令行中指明优化等级就可以打开优化器并设置其限定条件:
Gcc –OS test.c –o test
-O0优化:
不优化,易于采用源码调试工具(如GNU Debugger, gdb)进行调试。编译的速度快。
-O1优化:
目标是尽可能快的编译,同时让结果代码文件尽量小,速度尽量快。
如果仍然需要安全的调试结果映像,-O1优化是一个安全的优化等级。
-O2优化
提供了更多的优化选项,但不包括用速度换取空间,或用空间换取速度的优化。
-Os优化
-Os优化即为O2水平的优化,除去那些可能增大结果映像的选项。目标是生成一个更小的可执行文件。
调试选项
如果想用符号调试器来调试代码,可以用-g标记来指定在映像中为GDB生成调试信息。-g选项可以带一个参数,用来指定调试信息的格式。例如想要生成dwarf-2格式的调试信息,可以这样设置命令选项:
Gcc –gdwarf-2 test.c –o test
其他工具
Size工具:指定文件大小,给出data段和bss段
$ size test.o
如果希望得到有关映像的更详细的细节,可以使用objdump工具。
objdump工具
$objdump –syms test.o
该函数将列出目标文件中可用符合的列表,他们的类型(text, bss, data)、长度、偏移等信息。
还可以使用-disassemble 参数来反汇编映像文件。
$objdump –disassemble test.o
该函数将列出在目标文件中找到的函数,以及GCC为这些函数分别生成的指令。
nm工具有助于理解目标文件中出现的符号,该工具不仅会列出各个符号,而且还会根据符号的类型列出相应的细节信息。
应用GNU make构建软件
GNU/Linux下的make工具用于自动生成软件。
Make 工具需要一个由开发人员创建的输入文件来描述将要生成的项目,GNU make 使用Makefile作为这个输入文件的默认文件名,因此输入make命令调用make工具时,会在当前目录寻找一个名为Makefile的文件来指导make工具如何生成项目。
简单的makefile文件:
Appexp : main.o app.o bar.o lib.o
Gcc–o appexp main.o app.o bar.o lib.o
main.o : src/main.c src/lib.h src/app.h
gcc -c -omain.o src/main.c
app.o : src/app.c src/lib.h src/app.h
gcc -c -o app.o src/app.c
bar.o : src/bar.c src/lib.h
gcc -c -o bar.o src/bar.c
lib.o : src/lib.c src/lib.h
gcc–c -o lib.o src/lib.c
makefile语法的基本结构:规则(rule).规则语句的冒号前的部分叫做“目标”,冒号后面的部分叫做“依赖”。规则后常常跟随将“依赖”转化为目标的命令。
第一行的规定告诉make工具要生成的目标是appexp,它必须有四个文件才能完成目标:main.o, app.o, bar.o, lib.o. make工具会检查那些文件是否存在并判定他是否具备了生成目标程序的必备条件。如果某个需求文件缺失或者该依赖文件比目标文件新,make工具就会开始查找生成那个依赖文件的规则。
找到那个依赖文件的规则之后,就会以开始检查这条新的规则的要求条件是否具备。就这样,make工具把这些规则链接到一起,形成生成最初的那个目标所需要的依赖树。然后make工具从该树的叶节点的规则对应命令开始执行,逐级生成更高级节点所需要的必备条件,直到该树的根节点。、
Make工具对不需要重新编译的依赖项不进行编译(比如和上次相比没有变化),节省了开销。
Makefile变量
该功能可以把一个变量名和任意长的字符串关联起来,将值赋予变量的基本语法如下:
MY_VAR = file.c file2.c
将字符串”file.c file2.c”赋给了MY_VAR变量。
也可以为MY_VAR = file.c MY_VAR +=file2.c
GNU make还提供了一些函数用于字符串操作。如:
SRC_VAR = this is a boy
TEST_VAR = $(subst this, that, ${SRC_VAR}) //提供字符串替换功能
模式匹配规则:
Makefile中把一种类型的文件转化为另一种类型的文件,常常要求遵循特定的模式,而不需要对应特定的文件。GNU make 提供“模式匹配”,允许把目标和依赖都指定为某种类型。
如通过 %.o : %.c
Gcc${LDFLAGS} -O appexp ${OBJ_FILES}
指明了将后缀名 .c 的文件转化为后缀名 .o的文件的方法。
自动依赖跟踪:
随着项目越来越大,保持makefile指明的依赖关系与各个文件中#include的一致性会越来越麻烦,因为工具链中的预处理器必须解析这些include,所以大部分现代编译器都提供挂自动输出相关规则的机制。Makefile文件中可以使用这些自动生成的规则来跟踪#include结构的变化,重新正确的生成项目。
库的构建和使用
库是目标文件的集合,简化应用程序开发人员对这些目标文件的存取和分发。
静态库由ar或者archive工具创建。在开发人员编译并与库链接之后,库中被需要的部分会被整合到可执行映像中。从应用程序的观点看,应用程序映像已经包含了库中自己需要的部分,因此他与外部的库不再有关联。
动态库或共享库也需要和应用程序连接,但不是直接整合到应用程序中,而是分为两个阶段:1、在应用程序生成阶段,连接器会检查并确认应用程序生成所需要的全部符号(函数或变量)在应用程序中可用,但是库中相应的部分并不被整合到应用程序映像中。
2、程序执行阶段,由动态加载器将所需要的共享库中的那部分载入内存,与应用程序映像动态链接到一起。生成映像文件小,但速度稍慢。
例子:
/*
*randapi.h
*生成随机数库头文件
*/
#ifndef __RAND_API_H
#define __RAND_API_H
extern void initRand();
extern float getSand();
extern int getRand(int max);
#endif
在其他文件中(比如写在initapi.c 和randapi.c中)中实现三个函数(initRand, getSand, getRand)
将这些函数生成一个库,ar工具来完成:
$ gcc -c Wall initapi.c
$ gcc -c Wall randapi.c
$ ar -cru libmyrand.a initapi.o randapi.o
最后一行,将生成的目标文件 initapi.o 和 randapi.o 链接到一起生成libmyrand.a库文件。
-cru 为建立存档或向存档中添加内容的一组标准选项。 c 选项指明构建静态库(如果已经存在,这个选项将被忽略); r 选项告诉ar 替换静态库中已经存在的目标(如果有的话); u选项是一个安全选项,限定仅当将要替换进库的目标比存档中现有的目标(当然是同名目标)更新时执行替换。
test.c函数中调用静态库,需要在头文件包含 #include “randapi.h”, 方法和调用普通函数没什么区别。
利用静态库生成应用程序:
$ gcc test.c -L . –lmyrand -o test
$ ./test
Gcc 编译器首先编译test.c,然后连接test.o和libmyrand.a 产生一个名为test的映像文件。- L 选项告诉gcc 那些库可以在当前子目录中找到(这个句点代表当前目录),也可以给库指定别的目录,如–L/usr/mylibs ; —l (小写L)选项指定了要使用的库,注意myrand并不是库的文件名,那个库的文件名为libmyrand, 使用—l选项时,它会自动在文件名的前面加上lib, 后面加上 .a(因此如果指定–ltest, gcc会去查找 libtest.a 库)。
通过-t 选项可以查看静态库中包含的内容:
$ ar -t libmyrand.a
initapi.o
randapi.o
$
通过-d 选项,将某个目标从静态库中移除
$ ar -d libmyrand.a initapi.o
$ ar –t libmyrand.a
randapi.o
$
如果移除目标失败,ar不会显示信息,加入-v选项可以使移除失败时显示失败信息。
Ar工具的重要选项
选项 | 名称 | 示例 |
-d | 删除 | ar –d <archive> <object> |
-r | 替换 | ar –r <archive> <object> |
-t | 列表 | ar –t <archive> <object> |
-x | 提取(相当于复制) | ar –x <archive> <object> |
-v | ‘显示详细 | ar –v |
-c | 创建 | ar –c <archive> |
-ru | 更新目标 | ar –ru <archive> <object> |
动态库/共享库的生成
为生成共享库时,源文件需要有一点改变,因为现在库和应用程序不像静态库时那样整合到一起,所以共享库并不知道应用程序的任何信息。比如地址就必须使用相对地址(使用GOT或者叫全局偏移表)。加载共享库时,加载器会自动解析所有的GOT地址,要生成地址无关的源文件,我们使用gcc的PIC选项。
$ gcc -fPIC -c initapi.c
$gcc -fPIC -c randapi.c //这样得到的源文件,就包含地址无关的代码
可以用gcc和-shared 标志在此基础上创建一个共享库。
$gcc -shared initapi.o randapi.o -o libmyrand.so
这样就指定了两个目标模块和输出的共享库文件,.so扩展名来表明此文件是一个共享库(shared object).
test.c函数中调用静态库,需要在头文件包含 #include “randapi.h”, 方法和调用普通函数没什么区别。
使用这个新的共享目标来生成应用程序,就像使用静态库那样把有关部分链接起来:
$ gcc test.c -L . –lmyrand -o test
可以用 ldd 命令查看这个新的映像依赖于哪些共享库。Ldd命令的功能是列出指定的应用程序的共享库依赖。例如
$ ldd test
Libmyrand.so=> not found
Libc.so.6=> /lib/tls/libc.so.6(0x42000000)
/lib/ld-linux.so.2=> /lib/ld-linux.so.2(0x4000000)
$
这里列出的标准c库(libc.so.6) 是动态连接器和加载器(ld-linux.so.2)。libmyrand.so显示未能找到,是因为没有显式的告知GNU/Linux 该库所在的位置。可以使用LD_LIBRARY_PATH环境变量来指出共享库的位置:
$ export LD_LIBRARY_PATH =./
$ ldd test
Libmyrand.so=> ./libmyrand.so(0x40017000)
Libc.so.6=> /lib/tls/libc.so.6(0x42000000)
/lib/ld-linux.so.2=> /lib/ld-linux.so.2(0x4000000)
$
指明共享库可以在当前目录(./)中找到,然后再次执行ldd test命令,便能成功找到那个共享库文件。
如果没有告知共享库的位置就执行应用程序,就会产生一条错误信息,指出共享库无法找到。故必须指明共享库的位置
动态加载库
这种库可以在应用程序运行中随时加载,而不必像共享库那样在程序启动时就立即加载。生成共享目标文件:
$ gcc -fPIC -c initapi.c
$ gcc -fPIC -c randapi.c
$ gcc -shared initapi.o randapi.o –o libmyrand.so
$ su_
<provide your root password>
$ cp libmyrand.so /usr/local/lib
$ exit
在这里,将共享库移动到一个常用的位置(保存到库文件的标准目录 /usr/local/lib),该动态加载库和之前的共享库是一样的,不同的是应用程序处理这个库的方式。
将应用程序和该动态加载库连接起来,需要更改应用程序调用库函数的方式。
在test.c程序中调用动态加载库,头文件需要包含
#include<dlfcn.h>
#include”randapi.h”
…
…
Int main()
{
Void* handle;
//声明本地函数指针,用于指向外部的函数
Void(*initRand_d)(void);
Float(*getSRand_d)(void);
Int(*getRand_d)(int);
//打开动态链接库
handle = dlopen(“/usr/local/lib/libmyrand.so”, RTLD_LAZY);
if (handle ==(void*) 0)
{
fputs( dlerror(), stderr);
exit(-1);
}
//检查是否可以得到initRand函数
initRand_d= dlsym(handle, “initRand”);
err= dlerror();
if(err != NULL)
{
fputs(err,stderr);
exit(-1);
}
//同样检查其他函数是否可以在动态链接库中正确获得
…
执行函数:
(*initRand_d)();
(*getRand_d)(10);
…
//关闭动态链接库的句柄
dlclose(handle);
return 0;
}
把一个本地函数的指针指向共享目标里的函数(使用dlsym),然后可以从应用程序中调用那个函数。相应的任务完成后,再用dlclose关闭那个共享库,并且移除其引用(释放刚才那个接口使用的内存)。
handle = dlopen(“/usr/local/lib/libmyrand.so”, RTLD_LAZY);
中参数 RTLD_LAZY, 表明一遍运行一遍解析;
也可以为参数RTLD_NOW ,表示在加载库时立即解析
使用了动态加载库的应用程序生成:
$ gcc -ldl test.c –o test
动态加载库API:
Void * dlopen(const char*filename, int flag);
Const char * dlerror();
Void * dlsym(void* handle, char* symbol);
Int dlclose(void* handle);
使用库的工具
file 工具
查看文件的信息
Size 工具
获得目标的text段, data段和bss段的大小。
nm命令
获得指定的目标文件中使用的符号,可以使用grep来过滤结果:
$ nm -n /usr/local/lib/libmyrand.so |grep “ T “
00000608 T _init
0000074c T initRand
00000784 T getSRand
000007be T getRand
00000844 T _fini
$
该例子使用nm得到共享库中的符号,但只把那些带有”T”标签的符号发送给stdout(即只输出了.text段或者说代码段的那些符号)。还可以使用-n选项来要求输出按地址顺序排序,而不是默认的按符号名字母表顺序排序。如果要得到.text段确切的大小,使用-S选项:
$ nm -n -S /usr/local/lib/libmyrand.so | grep “T”
00000608 T _int
0000074c 00000036 T initRand
…
…
.等其他函数
其中输出信息表示:initRand在库中的相对便宜为 0-74c, 大小为0-36字节(十进制)。
Objdump工具
Objdump工具可以将目标反汇编成为自然指令集合。
$ objdump -disassemble -S /usr/local/lib/libmyrand.so
-disassemble 选项从机器码反汇编成为自然指令, 在编译的时候使用-g 选项,则使用objdump -S选项可以输出散布的源代码.
ranlib工具
ranlib工具是创建静态链接库的最重要的工具之一,这个工具创建库所包含的内容的索引并把这个索引保存在库文件中,库文件含有索引时,生成映像的连接速度会显著提高。
$ ranlib libmyrand.a
用automake/autoconf打包
目标是为生成项目自动生成适当的Makefile文件。
例如一个简单的项目:
工程目录 |
Makefile |
应用程序目录app |
库目录 lib |
app.c |
app.h |
main.c |
Lib.c |
bar.c |
lib.h |
libheader.h |
Makefile解决方案:
1. VPATH = lib app
2. LIBSRC = lib.c bar.c
3. LIBOBJ = $(LIBSRC: .c = .o)
4. APPSRC = main.c app.c
5. APPOBJ = $(APPSRC: .c = .o)
6. CFLAGS = -I
7. INCLUDES = ./lib
8. all : libexp.a apex
9. %.o : %.c
10. $(CC) -c $(CFLAGS) $ (INCLUDES) -o $@ $<
11. libexp.a : $(LIBOBJ)
$(AR) cru libexp.a $(LIBOBJ)
12. appex : $(APPOBJ) libexp.a
$(CC) -o appex $(APPOBJ) -lexp -L .
第一行设置了VPATH 变量,VPATH变量指定了make工具依据生成规则查找源文件的路径。Make工具的VPATH功能使得一个makefile文件就可以引用lib和app两个子目录中的源文件;第2至5行创建了源文件和目标文件列表,以供生成规则使用,这里的文件列表不包括路径,因为路径由VPATH变量指定;第6,7行设置了进行生成所必须的编译器标志;第8行第8行指定了默认生成目标:生成库和应用程序;第9,10行确定了将c源文件转化为目标文件的规则;第11行描述了如何构建这些库,第12行秒速了如何构建这些应用程序。
自动工具的简单应用
使用自动工具创建5个文件来代替makefile文件,5个替代文件的创建方式都比较简单。支持一个简单的自动工具项目需要添加如下文件:
autogen.sh:运行自动工具以产生构建环境的shell脚本
configure.ac: autoconf工具的输入文件
Makefile.am:顶层的makefile模板
app/Makefile.am:可执行文件appexp的makefile模板
lib/Makefile.am:库文件libexp.a 的makefile模板
工程目录 |
Makefile.am |
应用程序目录app |
库目录 lib |
app.c |
app.h |
main.c |
Lib.c |
bar.c |
lib.h |
libheader.h |
Configure.ac |
Autogen.sh |
Makefile.am |
Makefile.am |
从结构图中可以看出,比直接使用makefile,多了5个文件:
autogen.sh, configure.ac, Makefile.am, ./app/Makefile.am , ./lib/Makefile.am
这些文件为自动工具描述了将要生成的产物和环境。自动工具获得相应的输入,生成一个构建环境模板,然后在生成系统上进一步进行设置,得到最终的makefile文件集合。假如在同一台机器上进行开发和生成,则下面命令可以设置并构建这个示例项目:
#./autogen.sh
#./configure
#make
首先运行autogen.sh脚本,用自动工具将输入文件转化为同在主机系统上设置的生成环境模板;然后执行configure脚本,针对生成的计算机定制构建环境模板;configure脚本的输出是可以用于生成这个系统的一系列GNU Makefile文件。这里的根目录下执行make命令就会构建库和应用程序。
1、简单的autogen.sh脚本:
1. #!bin/sh
2. #Run this to generate all theinitial makefiles, etc.
3. aclocal
4. libtoolize --automake
5. automake -a
6. autoconf
第1行指定运行此脚本时使用的shell;
第3行aclocal 工具建立automake和 autoconf工具工作所需要的本地环境,aclocal明确保证automaker和autoconf达成其功能所需要的m4宏环境已正确设置;
第4行运行libtoolize工具,激活automaker中的libtool功能;
第5行运行automake工具,将Makefile.am文件转化为Makefile.in文件;
第6行运行autoconf工具,读入configure.ac输入文件,将它转化为名为configure的纯shell脚本。
2、Automake工具
automake 工具的输入是一系列Makefile.am文件,这些文件描述将要生成的目标以及生成他们所用的参数。automake工具将Makefile.am文件转化为Makefile.in文件,Makefile.in文件是GNU make格式的文件,在配置脚本转化为最终的Makefile文件时作为模板工作。
根Makefile.am文件内容:
SUBDIRS = lib app
这个根Makefile.am文件的内容就是指明这个项目的所有工作都在子目录下完成。第一行告诉automake 工具区访问子目录,根据那里找到的Makefile.am进行工作。SUBDIRS变量中目录的顺序是很重要的:automake将会按照从左到右的顺序进入这些目录进行工作。
./libMakefile.am文件内容:
lib_LIBRARIES = libexp.a
libexp_a_SOURCES = bar.c lib.c
第一行是本目录中将要生成的静态库的列表,lib_LIBRARIES 变量名包含两个信息:lib部分指明这个库在安装时放在lib目录, LIBRRIES部分指明这个变量中列出的目标要作为静态库生成。
第二行列出了生成libexp.a静态库所需要的源文件,automake也利用变量名格式来确定这个变量对应的库和内容,变量名的libexp_a部分指明这个变量的值用于生成libexp.a, SOURCES部分指明这个变量的值是一个空格分隔的源文件列表。
./app/Makefile.am中的内容
bin_PROGRAMS = appexp
appexp_SOURCES = app.c main.c
appexp_LDADD = $(top_builddir)/lib/libexp.a
appexp_CPPFLAGS = -I $(top_srcdir)/lib
第一行列出将要生成的可执行文件。变量 bin_programs指定automake将结果安装在bin目录中,并指明这里的目标应当作为可执行文件生成;
第二行的SOURCES变量列出将要编译到appexp中的源文件
第三行指定变量 LDADD, 指明链接时需要包含的东西
第四行指定CPPFLAGS 变量,该变量在运行时会传递给预处理器。这个变量应该包含-I: (包含路径)和–D:(明确在Makefile中将传递给预处理器的那部分)
3、autoconf工具
autoconf工具将输入文件configure.ac转化为名为configure的shell脚本。Configure脚本负责收集当前生成系统信息,并使用这些信息将模板文件Makefile.in转化为GNU make工具使用的makefile文件。Configure脚本会确定模板文件Makefile.in中的设置变量的具体值,用具体值将设置量替换掉,从而完成转化工作。输入文件configure.ac包含描述configure脚本运行时应当执行的设置检查类型的宏。
dnl Process this file with autoconf to produce a configure script
AC_PREREQ(2.53)
AC_INIT(app)
AM_INIT_AUTOMAKE(appexp, 0.1.00)
AC_PROG_CC
AC_PROG_RANLIB
AC_OUTPUT(app/Makefile lib/Makefile Makefile)
Configure 脚本
Autoconf工具的输出是一个名为configure的脚本文件,运行configure脚本文件会收集执行系统的信息,然后对automake生成的Makefile.in文件进行一个替换步骤得到试用的Makefile文件
4、生成Makefile文件
#make make操作
生成的makefile文件有很多优点,是简单Makefile所不具备的:
1、 自动依赖跟踪。例如一个头文件被改动,只需重新生成受影响的源文件
2、 试用清洁(clean)目标清楚所有已产生的输出
3、 自动将产生的二进制文件安装到适当的目录下运行
4、 自动产生源文件的发新版并打包成tar文件
生成的makefile文件有很多预先定义的目标可供用户调用。下面列出了automake生成的Makefile文件中的普通目标:
1、 make:默认目标,用于生成该项目的二进制文件
2、 make clean :清洁目标,删除所有已产生的生成文件,下次调用make就可以对所有东西进行重建
3、 make distclean:删除所有已产生的文件的目标,包括由configure脚本产生的那些文件。使用该目标,则下一次生成前需要运行configure脚本
4、 make install:该目标奖产生的二进制文件和支持文件移动到系统目录结构中,安装的位置由参数–enable-prefix控制,该参数可以在运行时传递给configure脚本。
5、 make dist:这个目标产生一个.tar.gz文件,该文件可以用于发布源码及其生成设置。这个压缩包含有所有的源文件,makefile.in文件和configure脚本。
GNU/Linux 文件操作
创建文件句柄
#include<stdio.h>
FILE * pFile;
打开文件
FILE * open(const char* filename, const char* mode);
Mode为打开文件的方式,有
“r” : 打开一个已经存在的文件进行读操作
“w” :打开一个文件进行写操作,如果不存在该文件,则新建一个该名文件
“a” : 打开一个文件进行添加操作,在末尾添加(如果不存在,则创建一个新文件)
“rw” : 打开一个文件进行读写操作(读和写操作如果文件不存在,则创建一个新文件)
关闭文件
fclose(FILE* pFile)
数据的读写
1、 字符接口
Int fputc(int c, FILE* stream);
Int fgetc(FILE* stream);
2、 字符串接口
fputs(char* s, FILE* pFile);
fpets(char* s, int len, FILE*pFile);
3、 结构体接口 fprintf
考虑对多种类型组成的有规则格式的数据(如C结构体)进行读写的问题
Typedef struct
{
int id;
float x_cord;
float y_cord;
char name[10];
} MY_TYPE;
FILE* fout;
MY_TYPE object = {1, 4, 5, “Jason”};
fprintf(fout, “%d %f %f “, object.id, object.x_cord, object.y_cord, object.name);
///
或用sprintf(char*, const char* format …..)
Char line[91];
Sprint(line, 90,“%d %f %f “, object.id, object.x_cord, object.y_cord, object.name);
Fputs(line,fout);
从文件流中读入使用fscanf,sscanf
FILE* fin;
Fscanf(fin, “%d %f %f “,&object.id, &object.x_cord, &object.y_cord, object.name);
//也可以用
Char line[91];
Fgets(fin, 90, line);
Sscanf(line, 90, &object.id, &object.x_cord, &object.y_cord, object.name);
二进制数据的读写
Size_t fread(void* ptr,size_t size, size_t nmenb, FILE* stream);
Size_t fwrite(const void*ptr, size_t size, size_t nmmb, FILE* stream);
例子:使用fwrite发送结构体数据:
fwrite( (void*)object, sizeof(MY_TYPE), 3, fout);
Long ftell(FILE* ) ;// 得到当前指针的位置
Void rewind(FILE * stream); //将文件指针指回到文件头
Int fseek(FILE* stream, long offset, int whence); //移动文件指针,使指针距离 whence 有offset距离。
Offset为移动的大小, whence指定移动的位置是相对于文件头(SEEK_SET),还是相对于当前位置(SEEK_CUR),还是文件尾(SEEK_END)
Int fgetpos(FILE* stream, fpos_t *pos);
Int fsetpos(FILE* stream, fpos_t* pos);
管道编程
管道为两个实体之间的单向连接器。如考虑GNU/Linux命令:
ls –1| wc -l
此命令为创建两个进程,一个是ls –1,另个为wc –l,然后将第一个进程的标准输出设置为第一个进程的标准输入,从而将两个进程连接起来。该通信为单向的,半双工管道。
管道(匿名通道),或者说半双工管道,提供了一个进程与他的某个先祖子进程通信的方法。因为操作系统无法定位管道(因为他是匿名的),所以最普遍的用法是在父进程创建一个管道,将管道传递给子进程从而进行通信。如果需要全双工通信,则应该转而考虑套接字API。
另一种管道是命名管道,该管道像普通管道一样工作,但是他存在于文件系统中,任何进程都能找得到他,这使得不同先祖的进程之间也可以进行通信。
例子:在一个进程中创建管道,向他写入消息,然后从管道中读回消息。
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#define MAX_LINE 80
#define PIPE_STDIN 0
#define PIPE_STDOUT1
Int main()
{
Const char*string = “A sample message.”;
Int ret, myPipe[2];
Charbuffer[MAX_LINE + 1];
//创建管道
ret =pipe(myPipe); //执行成功,则数组myPipe就包含两个文件描述符
if (ret ==0)
{
//向管道中写入信息
write(myPipe[PIPE_STDOUT], string, stlen(string));
//从管道中读取信息
ret = read(myPipe[PIPE_STDIN], buffer, MAX_LINE);
//将字符串末尾添加/0
buffer[ret]= 0;
printf(“%s\n”,buffer);
}
Return 0;
}
例子中 pipe函数创建了管道,传递有两个元素的整形数组来描述管道。该管道定义为两个分离的文件描述符,一个输入、一个输出,可以向管道的一端写入,然后从另一端读出。API函数pipe函数执行成功返回值为0.返回0之后,数组myPipe就包含两个文件描述符,一个表示管道的输入myPipe[1], 一个表示管道的输出myPipe[0].
管道编程的API函数
1、pipe 用于创建一个新的匿名管道
2、dup 用于创建一个文件描述符的副本
3、mfifo 用于创建一个命名管道(fifo)
管道只不过是一对文件描述符,因此所有能够操作文件描述符的函数都可以用于管道。这些函数包括但不限于select, read, write, fcntl, freopen
Pipe函数:
#include<unistd.h>
Int pipe(int fds[2]);
Pipe函数成功时返回0, 失败时返回 -1, 并设置相应的errno。成功返回时,fds数组(作为pipe函数的参数传递的)会被赋予两个活动的文件描述符。数组中的第一个元素是应用程序可读取的文件描述符,第二个元素是应用程序可写入的文件描述符。
示例:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<wait.h>
#define MAX_LINE 80
Int main()
{
Int thePipe[2], ret;
Charbuf[MAX_LINE + 1];
Constchar* testbuf = “a test string”;
If(pipe(thePipe) == 0) //如果创建管道成功
{
If(fork() == 0)
{ //子进程中读取管道中的数据,阻塞式
ret= read(thePipe[0], buf, MAX_LINE);
buf[ret]= 0;
printf(“Child read %s\n”, buf);
}
else
{ //父进程发送数据,并等待子进程完成读取
ret= write(thePipe[1], testbuf, strlen(testbuf));
ret= wait(NUL);
}
}
return0;
}
在该程序中,创建一个管道,然后把进程分支为一个父进程和一个子进程,在子进程中试图从管道的输入文件描述符进行读取,这会将进程挂起,直到有数据可以读取;父进程使用write函数等待子进程退出。
该程序需要注意的地方:子进程所继承的文件描述符由父进程创建,然后使用文件描述符进行通信。Fork函数执行完成后,这两个进程就已经相互独立(不过子进程继承了父进程的一些特征,比如pipe文件描述符)。这两个进程使用的内存也分开了。
当进程结束后,为管道分配的资源会自动释放,实际编程中还是调用close关闭管道使用的描述符:
Ret = pipe(mypipe);
….
Close(mypipe[0]);
Close(mypipe[1]);
如果管道的write端已经被关闭,则一个进程试图从管道中进行读取时,将会返回零。意味着管道已经不再可用,应该将其关闭。
函数dup和 dup2
函数dup和dup2提供了复制文件描述符的功能,常用于stdin, stdout或进程的stderr的重定向。
#include<unistd.h>
Int dup(int oldfd);
Int dup2(int oldfd, int targetfd);
Dup函数传入一个已经存在的文件描述符,就会返回一个与该描述符相同的新描述符。即这两个描述符共享相同的内部结构,例如对一个文件描述符进行lseek操作,则第二个文件描述符也会指到同样的位置。
Dup2函数参数为两个文件描述符,第二个为第一个的复制
Int oldfd;
Oldfd = open(“app_log”, (O_RDWR |O_CREATE), 0644);
dup2(oldfd, 1);
close(oldfd);
在该示例中,打开一个名为app_log的文件,获得一个名为oldfd的文件描述符。以oldfd和1为参数调用dup2,将标识为1的文件操作符(stdout)的内容设为oldfd(新打开的文件)。所以stdout的内容转而写入名为app_log文件。在复制oldfd之后立即关闭oldfd描述符,这不会关闭刚刚打开的文件,因为现在文件描述符1已经指向了该文件。
示例:在C中用管道连接命令
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
Int main()
{
Int pfds[2];
If(pipe(pfds) == 0)
{
If(fork() == 0)
{
Close(1);
dup2(pfds[1],1);
close(pfds[0]);
execlp(“ls”, “ls”, “-1”,NULL);
}
Else
{
Close(0);
dup2(pfds[0],0);
close(pfds[1]);
execlp(“wc”, “wc”, “-1”,NULL);
}
}
Return0;
}
示例中,先创建管道,然后将应用程序分支为子进程和父进程。在子进程中,首先关闭stdout描述符,这个子进程提供的是ls-l的功能,不是把结果写入到管道的输入端(使用dup进行重定向),然后将stdout重定向到pfds[1],然后关闭管道的输入端描述符(因为子进程用不到了)。最后使用execlp函数把子进程的映像更换为命令ls –l,执行这个命令,所有生成的输出信息都会送到管道的输入端。
在接收管道信息的一端。父进程来完成,首先关闭stdin描述符(因为不会从它那里接收到信息)。然后再次使用dup2函数把stdin设置为管道的输出端,然后将管道的输出端描述符关闭(因为不会再用到)。最后用execlp指定命令wc –l,此命令将管道中的内容作为它的输入信息。
(常用)该例子中,把子进程的输出重定向到管道的输入,父进程的输入重定向到管道的输出。
函数mkfifo
函数mkfifo用于在文件系统中创建一个文件,提供FIFO功能(也称为命名管道)。
匿名管道只用于父进程和子进程之间,命名管道在文件系统中可见,可以用到任何进程之间。
#include<sys/types.h>
#include<sys/stat.h>
Int mkfifo(const char* pathname, mode_t mode);
示例:
Int ret;
ret = mkfifo(“/tmp/cmd_pipe”,S_IFIFO|0666);
If (ret == 0)
{
//命名通道成功创建。。。
}
Else
{
//创建失败
}
创建一个使用/tmp子目录下的cmd_pipe文件的FIFO(命名管道)。然后可以打开这个文件进行读写,从而实现通信。打开一个命名管道后,可以使用典型的读写命令对其进行读写:
//利用fgets从管道中读取:
Pfp = fopen(“/tmp/cmd_pipe”, “r”);
..
Ret = fgets(buffer, MAX_LINE, pfp);
..
向管道中写入内容
Pfp = fopen(“/tmp/cmd_pipe”, “w+”);
….
Ret = fprintf(pfp, “Here’s a test string\n”);
命名管道以集合点模式工作,除非一个写入者已经打开了命名管道的一端,否则读取者是不能打开命名管道的。如果没有写入者的话,读取者调用open时会被阻断。
系统命令 【mkfifo命令】
Mkfifo命令是通过命令行创建命名管道(FIFO特殊文件)的两种方法之一:
mkfifo [options] name
这里[opstions]是-m, 用于设定模式(读写模式),name是要创建的命名管道的名字(以及其路径,如果需要的话)。如果没有特别指明授权模式,默认值为0644授权。
示例:在/tmp目录下创建一个名为cmd_pipe的命名管道。
$ mkfifo /tmp/cmd_pipe
$mkfifo -m 0644 /tmp/cmd_pipe
在授权模式创建之后,可以在命令行中使用这个管道进行通信,在一个终端,试图用cat命令进行读取: $ cat cmd_pipe
输入该命令,则相应进程会挂起,等待管道的输入。在另一终端,使用echo命令向这个命名管道写入信息: $ echo Hello > cmd_pipe
这个命令执行完成,就会唤醒读者,并完成了读取操作。
即命名管道不仅使用不C程序,也适用于脚本。
套接字编程
主机、协议、端口
地址是由主机、协议、端口 构成的元组,用于将网络中一个节点和另一个节点分开。
如{tcp, 192.168.1.1, 8000}
套接字:是两个应用程序之间的通信管道的终点。套接字是使用相同协议的两个终点的联合体,比如 {tcp, 162.16.1.1, 8000} 和 {tcp,162.105.0.1, 9000}
只要两个终点的三个元素(主机、协议、端口)不是完全相同,但同时协议必须相同。
套接字用于实现进程间的相互通信(同一主机或不同主机)。
TCP socket采用服务器/客户端模式:
服务器:
Socket() -> bind() -> listen() ->accept() -> 数据传输 -> …. > close()
客户端:
Socket() -> connect() -> 数据传输 -> ….> close()
在数据传输阶段,服务器、客户端可以使用send(), recv()来异步的接收和发送数据。
网络操作以大端字节序进行,也成为网络字节序;主机的操作以主机字节序进行,根据主机架构的不同,可能采用大端字节序或小端字节序。
套接字API
1、 创建套接字
Int socket(int domain, int byte, int protocol);
mSocket = socket(AF_INET, SOCK_STREAM, 0);
mSocket = socket(AF_INET, SOCK_DGRAM, 0);
mSocket = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
AF_INET 指明要使用IPv4 Inetnet协议,第二个参数定义通信语法,有流式SOCK_STREAM或数据报式 SOCK_DGRAM, 第三个参数一般设为0
2、 关闭套接字
Int close(sock);
在调用close之后,就不能再从套接字接收数据了,在连接被物理关闭之前,所有排队等待传输的数据都会得到一点时间进行发送。
套接字地址
为了在Internet上(AF_INET)进行通信,使用结构体sockaddr_in来命名套接字。
Struct sockaddr_in{
Int16_t sin_family;
Uint16_t sin_port;
Struct in_addr sin_addr;
Char sin_zero[8];
};
Struct in_addr
{
Uint32_t s_addr;
};
struct sockaddr_in servaddr;
servsock = socket(AF_INET, SOCK_STREAM, 0);
memset(&servsock, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET; //为了进行internet通信,指定IPv4
servaddr.sin_port = htons(48000); //网络字节指定端口号,故先转换
servaddr.sin_addr_.s_addr =inet_addr(INADDR_ANY); //为指定IP地址(此例子中为通配符)
为了进行Internet通信,用AF_INET唯一确定sin_family. 域sin_port以网络字节确定指定的端口号(故要用htons转化),域sin_addr实际上是通过名为s_addr的一个32位的域来描述IPv4 Internet地址。Sin_addr通常为INADDR_ANY, 即通配符,接收链接的时候最为服务器,通配符说明可以接受任何地址发来的请求。
套接字原语
函数bind()
int bind(int sock, struct sockaddr* addr, int addrlen);
err = bind(servsock, (structsockaddr*)&servaddr, sizeof(servaddr));
一般TCP Socket需要绑定,UDP socket的接收socket需要绑定某个端口,而发送socket不需要。
函数listen
服务器等待接收客户端的连接请求
Int listen(int sock, int backlog);
//sock为建立的服务器套接字, 参数backlog指明等待连接的客户端队列中一共可以有多少个客户端。
函数accept
调用accept函数,使服务器接收客户端的连接。在能够调用函数accept之前,必须已经创建了服务器套接字,向它绑定一个名字,并且已经调用了listen。函数accept为客户端连接返回一个套接字描述符:
Int accept(int sock, struct sockaddr* addr, int *addrlen);
实际使用中:
1、 需要知道是谁向自己发起连接请求
Struct sockaddr_in cliaddr;
Int cliLen;
cliLen = sizeof(struct sockaddr_in);
clisock = accept(servsock, (sturctsockaddr*)&cliaddr, &cliLen);
返回时,clisock的值包括新的用户套接字和描述相应用户地址的cliaddr(主机地址和端口号)。
2、 服务器不需要知道客户端信息
Clisock = accept(servsock, (struct sockaddr*)NULL, NULL);
对accept的调用会阻断直到用户连接可用为止。
函数connect
Connect由客户端套接字应用程序使用,用来连接到服务器。客户端必须先创建一个套接字,定义一个地址结构体,在其中设置好将要连接的主机和端口。
Int connect(int sock, (struct sockaddr*)servaddr, int addrlen);
参数sock代表客户端套接字,servaddr是将要连接的服务器地址,最后需要传递servaddr结构体的长度,connect才知道是在传递sockaddr_in结构体。
函数connect会阻断到发生错误或与服务器的三次握手完成。如果发生错误,错误号由函数connect返回。
套接字输入输出(TCP流式socket)
Int send(int sock, const void* msg,int len, unsigned int flags); 【flag一般为0】
Int recv(int sock, void * buf, int len, unsigned int flags);
调用send函数时,它会阻断到所有buf中的数据都进入套接字的发送队列为止。
如果队列中没有足够的空间放buf中的数据,send会一直阻断到有可用的空间为止。如果希望避免之,当可用空间不足时,让send直接返回,就应该设置MSG_DONTWAIT标志:
Send(sock , (void*) buf, strlen(buf), MSG_DONTWAIT);
Send的返回值代表错误类型(如果小于0),或者成功发送的字节数(大于0)。函数send的执行结果并不意味着数据已经发送到主机上,仅仅表示数据已经加入套接字发送队列等待传输。
#define MAX_BUFFER_SIZE 50
Char buf[MAX_BUFFER_SIZE];
numBytes = recv(sock, buf, MAX_BUFFER_SIZE, 0);
使用recv函数之前要先指定接收缓冲区,且指定接收缓冲区的大小。函数返回值为msg缓冲区中当前内容的字节数,如果发生错误,则返回-1.
使用MSG_PEEK标志,可以只是看看对读取操作可用的数据,但不会将数据从套接字中取走。
非连接套接字(UDP 数据包类型)使用的函数
Sendto和recvfrom用于向对应套接字终点发送和从对应套接字终点接收消息:
Int sendto( int sock, const void* msg, int len, unsigned int flags,
Const struct sockaddr* to, int tolen);
Int recvfrom(int sock, void* buf, int len, unsigned intflags,
struct sockaddr* from, int romlen);
函数sendto由非连接套接字使用,用来将数据报发送到地址结构体事先指定的目的地。函数sendto需要事先指定发送到的socket地址结构体 sockaddr*。
参数sock为本地socket; msg 为数据包缓冲区; len 为数据包长度; flags为发送标志,一般为0;sockaddr* to为目的地址结构体, tolen为目的地址结构体长度。
函数sendto执行后,会返回加入传输队列的字节数,如果有错误发生,则返回 – 1.
函数recvfrom为非连接套接字提供接受数据报的能力,参数要提供地址结构体和长度,地址结构体用于保存发来数据报的发送者的地址。可以用这个地址调用sendto函数向发送者回复消息。
ecvfrom(int sock, void* buf, int len, unsigned int flags,
struct sockaddr* from, int romlen);
该函数会阻断,直到发生错误(返回-1)或者接收到数据报(返回0或者正数)
套接字选项
套接字选项允许应用程序改变套接字和操纵他们的一些行为。例如改变套接字的发送缓冲区的大小,改变某个套接字的最大TCP片段的大小。
Int getsockopt(int sock, int level, int optname, void * optval, socklen_t * optlen)
Int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
第一个参数sock用来指定套接字,然后必须指定将要应用的套接字选项的级别,参数level设置为SOL_SOCKET 表示套接字层选项, OPPROTO_IP表示IP层选项, OPROTO_TCP表示TCP层选项;某级别下的具体选项由参数 optname指定;参数optval和 optlen指定选项的具体值。
例如获得套接字的发送缓冲区的大小:
Int sock, size, len;
..
getsockopt(sock, SOL_SOCKET, SO_SNDBUF,(void*)&size, (socklen_t *)&len);
GNU/Linux 进程模型
GNU/Linux进程有两种基本类型,内核线程和用户进程。内核线程是在内核中由kernel_thread()函数创建,用户进程由fork()和 clone()创建。(讨论用户进程)
创建一个子进程(由fork创建),就创建了一个新的子任务,并为它复制了父任务使用的内存。两个进程使用的内存是相互独立的。在调用fork的时候,父进程当时的所有变量对子进程都是可见的;但在fork执行完成之后,父进程的变量的任何变动对子进程都是不可见的。
在创建一个新任务的时候,父进程使用的内存并不是真正的复制给子任务,他们都指向同一处内存空间,但把内存页面标记为copy-on-write。当任何一个进程试图向这些内存中写入内容时,就会产生一组新的内存页面由这个进程私有。默认情况下,子进程继承文件描述符、内容映像以及CPU状态。
GNU/Linux中每一个进程都有一个唯一的描述符,称为进程ID(或进程号id),每一个进程都有一个父进程(init进程除外)。
函数getpid()获得当前的进程号;函数getppid()获得当前进程的父进程的进程号,函数getuid()获得用户id,函数getgid()获得组id
用fork创建一个子进程
当API函数fork返回时,已经分裂出了新的进程,但fork的返回值指明的是进程运行的上下文环境。
Pid_t pid;
….
Pid = fork();
If (pid > 0)
{
//父进程的上下文,子进程号为pid
}
Else if (pid == 0)
{
//子进程上下文
}
Else
{
//父进程上下文,但fork调用出错,没有创建子进程
}
对于父子进程中共有的变量,如果对该变量发生写操作,则会将内存划分开,每个进程拥有各自的内存,这些内存相互独立。即每个进程都为自己复制一份独有的变量集。
创建者进程同步
在父进程上下文环境中调用了wait函数,函数wait把父进程挂起,直到子进程退出。如果父进程没有调用wait函数等待子进程退出,子进程就会成为“僵尸进程”(即既不是活的,也不是死的)。允许僵尸进程存在会导致问题,因为他们浪费了资源。因此需要正确处理子进程的退出操作,如果父进程先于子进程退出了,已经运行的子进程会视为继承自init进程。
【另一种避免僵尸进程的方法是告诉父进程,在子进程产生退出信号时忽略他们,这可以使用信号API函数完成。】
函数wait会把调用者挂起(这里即为父进程),等待子进程退出。在子进程退出之后,表示其特定退出状态的整数型值会传递给函数wait。
int status;
pid_t pid;
…..
Pid = wait(&status);
If (WIFEXITED(status))
{
Printf(“Process%d exited normally\n”, pid);
}
捕获信号
信号,即GNU/Linux中进程的回调符号。可以为某个进程注册为在某事件发生时接收信号,或是在某个默认操作退出时忽略信号。
为了捕获信号,要为进程注册一个信号句柄(一种回调函数)以及感兴趣的具体信号。
示例:为捕获信号注册句柄
#include<stdio.h>
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
Void catch_ctlc(int sig_num)
{
printf(“CaughtControl – C\n”);
fflush(stdout);
}
Int main()
{
signal (SIGINT, catch_ctlc);
printf(“Goahead, make my day.\n”);
pause();
return 0;
}
程序中注册了信号SIGINT,该信号表示接受到了Ctrl + C
使用API signal函数注册了句柄 signal(SIGINT, catch_ctlc);
首先指定感兴趣的信号,然后句柄函数就可以对这个信号有反应;
再使用pause,将进程挂起直到它接收到了一个信号。
接受到信号进入句柄处理函数,可以把一个消息发送给stdout,然后清空其缓冲区以确保内容显示出来。从信号句柄返回后,main函数可以从pause语句处继续运行并正常退出。
发出信号
可以在一个进程中使用API函数kill向另一个进程发出信号。API函数kill需要给出一个进程ID和要发送的信号。
: kill( pid_t pid, SIGNAL sig);
如果想要向自己发送一个信号(同一个进程),可以使用API函数raise。这个函数可以发出信号而无需指定进程ID参数(该参数由getpid()函数自动获取)。
传统的进程API
API函数 | 用途 |
Fork | 创建一个新的子进程 |
Wait | 将进程挂起直到子进程退出 |
Waitpid | 将进程挂起直到指定的子进程退出 |
Signal | 注册一个新的信号句柄 |
Pause | 将进程挂起直到捕获到信号 |
Kill | 向某个指定的进程发出信号 |
Raise | 向当前进程发出信号 |
Exec | 将当期进程映像用一个新的进程映像替换 |
Exit | 正常终止当前进程(退出) |
Fork函数:
Fork()调用复制了父进程,然后返回对某个特定进程的控制(父进程或子进程)。如果fork的返回值小于零,说明发生了错误。Errno的值可能是EAGAIN 或 ENOMEM,均是由于可用内存不足造成的。
API函数fork在GNU/Linux中效率很高。在调用fork的时候并不立即复制内存页表,父进程和子进程当时共享相同的页表,只是不允许对这些页表进行写操作。但出现对共享页表的写操作时,会为进行操作的进程复制一份页表以供其私有。“当写入时复制”(copy – on-write),允许fork函数很快的完成运行。只有在共享数据内存出现写操作时,内存才会发生页表的分页。
Wait函数
API函数wait用于将调用进程挂起,直到子进程(由调用进程创建)退出,或直到某个信号发出。如果父进程没有在等待子进程退出,而子进程又退出了,这个子进程就会成为僵尸进程。
Wait函数提供了一种同步机制,如果子进程在父进程调用wait函数之前退出了,这个子进程会成为僵尸进程。如果现在再调用wait还是可以释放资源,这种情况下,wait直接返回。
Pid_t wait(int *status);
函数wait返回推出的子进程的ID 值,如果发生错误则返回-1,参数status中包含有关子进程退出的状态信息。
评估wait函数所用的宏函数
宏 | 说明 |
WIFEXITED | 如果子进程正常退出,则不为0 |
WEXITSTATUS | 返回子进程的exit状态 |
WIFSIGNALED | 如果子进程因为信号二结束,则此宏值为true |
WTERMSIG | 返回引起子进程退出的信号(尽在WIFSGNALED为true时有意义 |
Waitpid函数
API函数waitpid是挂起父进程直到某个指定的子进程退出。
Pid_t waitpid(pid_t pid, int * status, int options);
Waitpid的返回值是退出的子进程的进程描述符,如果options参数设定为WNOHANG,则返回值为零,且没有子进程退出(waitpid会立即返回)。
Waitpid需要的参数由pid值,一个status参数(用来保存返回值)以及options参数。参数pid的值可以是子进程的ID,也可以是表示其他行为的值。
Waitpid的pid参数值
值 | 说明 |
>0 | 挂起直到由pid指定的子进程退出 |
0 | 挂起直到任何一个与调用进程的组ID相同的子进程退出 |
-1 | 挂起直到任何子进程退出(与wait功能相同) |
< -1 | 挂起直到任何一个其组ID与pid参数的绝对值相同的子进程退出 |
Pause函数
函数pause将进程挂起,直到接收到信号。在信号接收到以后,调用进程从pause中返回,继续运行。API函数pause的原型如下:
Int pause(void );
如果进程为捕获信号已经注册了信号句柄,那么pause函数会在信号句柄被调用并返回之后返回。
Kill函数
API函数kill向一个进程或一系列进程发送信号,如果信号成功发送了返回0,否则返回 -1.函数kill的原型:
Int kill(pid_t pid, int sig_num);
参数sig_num表示要发送的信号,参数pid可以是各种不同的值
Pid | 说明 |
>0 | 信号发送到由pid指定的进程 |
0 | 信号发送到与调用进程同组的所有进程 |
-1 | 信号发送到所有进程(init进程除外) |
<0 | 信号发送到由pid的绝对值指定的进程组中所有进程 |
Exec变体
API函数fork提供把应用程序分裂为独立的父进程和子进程的机制,两个进程分享共同的代码却可以扮演不同的角色。Exec系列函数则用于完全替换当前进程映像。
//虽然exec函数会返回当前pid,它实际上是开始了一个新程序,用它来替换了当前进程。
Exec变体的原型:
Int execl(const char* path, const char*arg, …)
Int execlp(const char* path, const char *arg, ….);
Int execle(const char* path, const char* arg, …. Char* const envp[]);
Int execv(const char* path, char* const argv[]);
Int execvp(const char* file, char* const argv[]);
…….
Exec命令允许把当前进程上下文替换为第一个参数指明的程序或命令:
execl(“/bin/ls”, “ls”, “-la”, NULL);
这个命令用 ls映像(列出目录)替换了当前进程。第一个参数指定了将要执行的命令(包含路径),第二个参数是这个命令的名字;第三个参数是传给ls 的选项,最后用NULL指明参数列表结束。在应用程序中执行这句代码的结果就是执行了命令 ls –la
Alarm函数
函数alarm在其他函数超时的情况下非常有用。函数alarm在预先设定的事件长度达到时会发出一个SIGALRM信号。
unsigned int alarm(unsigned int secs); //在secs秒之后发出SIGALRM信号。
用户要传入一个秒钟数,即在发送SIGALRM信号前等待的秒数。如果前面没有出现警告情况,alarm函数会返回零,否则它返回前面的警告所等待的秒数。
Exit函数
API函数exit终止调用进程。传入exit的参数会返回给父进程,为wait或waitpid调用提供所需要的状态信息。
Void exit (int status);
进程调用exit时还会向父进程发出SIGCHLD信号,释放当前进程占用的资源。如果进程注册了atexit 或on_exit函数,这些函数会在退出时调用(调用顺序与他们的注册顺序相反)。
POSIX线程(P线程)编程
要想知道正在使用的是哪个P线程库:
$ getconf GNU_LIBPTHREAD_VERSION
执行这个命令会显示出LinuxThreads或NPTL及其版本号
进程和线程都有控制流,两者都能同时运行,线程共享数据,进程不共享。
创建线程的时候,线程唯一独有的元素是线程独有的栈,线程的代码和全局变量都是共同的;但进程会复制代码、数据空间、内存栈。
一个GNU/Linux进程可以创建和管理多个线程。线程由线程描述符确定,系统中每个线程的描述符都是唯一的,每个线程都有一个私有的栈和一个独有的上下文环境(程序计数器和存储寄存器等)。线程共享数据空间,他们共享的东西就不只是用户的数据了。例如,打开文件的描述符和套接字是共享的。因此当一个多线程应用程序使用套接字或文件时,要防止多重访问同一资源的情况。
编写多线程应用程序时要注意线程的数据共享问题。
线程函数基础
之前API函数都有一个共同的模式:遇到错误时,返回 -1,错误码保存在进程变量errno中。线程API函数成功时返回0,在遇到错误时,返回一个大于0的错误码。
P线程API:
所有的多线程应用程序都要使pthread函数原型和相应的符号可用。故要包含<pthread.h>
所有的多线程应用程序都必须创建线程,并在最后销毁线程。
Int pthread_create(pthread_t *thread, pthread_attr_t* attr,
Void*(*start_routine)(void*), void*arg);
Int pthread_exit(void* retval);
创建一个新的线程,调用pthread_create,并把pthread_t对象和一个函数(start_routine)结合起来。这个函数表示线程执行的最高层代码。可以由pthread_attr_t(通过pthread_attr_init)提供一系列选项进行属性设置。第四个参数(arg)是在线程创建是时传入的可选参数。
#include<pthread.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<errno.h>
Void * ThreadFunc(void* arg) //线程体函数,运行结束时,自身调用pthread_exit结束
{
printf(“Thread ran\n”);
pthread_exit(arg);
}
Int main()
{
Int ret;
pthread_t mythread;
ret= pthread_create(& mythread, NULL, ThreadFunc, MULL);
//ThreadFunc为线程执行函数
If(ret != 0)
{
Printf(“Can’tcreate pthread(%s)\n”, strerror(errno));
exit(-1);
}
return 0;
}
线程管理
函数pthread_self可以用来取得自己独特的描述符; pthread_t pthread_self();
大多数应用程序需要一些初始化,对于多线程应用程序,初始化很困难,函数pthread_once允许开发人员为一个多线程应用程序创建一个初始化子进程。只会执行一次。
第一个调用pthread_once的线程调用initialize_app,之后调用pthread_once的线程不会再调用initialize_app.
//用pthread_once 提供一个单次使用的初始化函数
#include<pthread.h>
Pthread_once_t my_init_mutex = pthread_once_init;
Void initialize_app(void)
{
//只执行一次的初始化函数
}
Void* myThread(void* arg)
{
Pthread_once(&my_init_mutex, initialize_app);
…
}
线程同步
让线程创建者等待线程结束(也成为“加入”线程),该功能由API函数pthread_join提供。在调用pthread_join时,会挂起调用线程直到加入的线程完成。在加入线程完成后,调用线程会从pthread_join的返回值获得到加入线程的终止状态。
Int phtread_join( pthread_t th, void ** thread_return);
参数th是想要加入的线程,该参数由pthread_create返回或由线程调用pthread_self获取。参数thread_return可以是NULL,表明不捕获线程的返回状态。
//注意: 用默认属性经pthread_create创建的线程都是可以加入的,如果线程的属性设置为detached,则该线程是不可加入的(因为它被设置为与创建线程分离)。
很多情况下,一个线程被建立之后,就不再需要在意它了。那种情况下,可以把它设置为分离的线程,创建者和线程自己都可以完成分离操作,还可以在创建线程时就将其设置为分离的线程(作为属性设置的一部分)。在一个线程被分离之后,它就不能再被加入了。
Int pthread_detach(pthread_t th);
//在线程中调用pthread_detach 把线程分离出去
Void * myThread(void * arg)
{
printf(“Thread %d started\n”, (int)arg);
pthread_detach(pthread_self());
Pthread_exit(arg);
}
///上程序中,线程执行函数中,自己将自己和调用线程分开,该线程退出时,所占用的资源会立即释放(因为这个线程已是分离线程,不会再加入其它线程)。函数pthread_detach成功时返回零,发生错误时返回非零值。
线程互斥
互斥是一个保证线程在关键区正常执行的变量,这些关键区只能由线程独占访问,如果不加保护的话,会导致数据被毁。
要创建一个互斥,只需要声明一个表示互斥的变量,然后用特殊符号常量初始化。
pthread_mutex_t myMutex = PTHREAD_MUTEX_INITIALIZER;
互斥初始化可以有不同的特殊符号常量:
PTHREAD_MUTEX_INITIALIZER 快速互斥
PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP 递归互斥
PTHREAD_ERROCHECK_MUTEX_INITIALIZER_NP 检查错误的互斥
有了一个互斥,可以锁定或解锁它,从而创建关键区。通过pthread_mutex_lock和pthread_mutex_unlock来实现锁定和解锁。函数pthread_mutex_trylock用于尝试锁定互斥,但如果互斥已经锁定,它不会阻断。可以用pthread_mutex_destroy来销毁已经存在的互斥。
Int thread_mutex_lock( pthread_mutex_t *mutex);
Int thread_mutex_unlock ( pthread_mutex_t *mutex);
Int thread_mutex_trylock( pthread_mutex_t *mutex);
Int thread_mutex_destroy( pthread_mutex_t *mutex);
锁定一个线程意味着进入了一个关键区,在互斥锁定后,可以安全的进入关键区而不必担心数据毁损或多重访问,要退出关键区,需要解锁互斥,然后退出。
Pthread_mutex_t cntr_mutex = PTHREAD_MUTEX_INITIALIZER;
……
assert(pthread_mutex_lock(&cntr_mutex)== 0);
//关键区
//增加保护计数
Counter ++;
assert(pthread_mutex_unlock(&cntr_mutex) == 0);
pthread_mutex_trylock 操作的意义在于如不能锁定互斥,或许可以做点别的事情而不是阻断在pthread_mutex_lock调用上:
ret =pthread_mutex_trylock(&cntr_mutex);
if (ret == EBUSY)
{
//不能锁定,做其他的事情
}
Else if (ret == EINVAL)
{
//关键区错误
Assert(0);
}
Else
{
//关键区操作
ret =thread_mutex_unlock(&cntr_mutex);
}
要销毁线程,调用pthread_mutex_destroy函数,函数pthread_mutex_destroy仅在互斥当前没有被任何线程锁定时才能成功执行,如果互斥正被锁定,函数会失败并返回错误码:EBUSY,
Ret =pthread_mutex_destroy(&cntr_mutex);
If (ret == EBUSY)
{
//互斥被锁定,无法销毁
}
Else
{
//互斥被销毁
}
线程条件变量
条件变量是一个特殊的线程结构体,允许一个线程基于条件唤醒另一个线程。条件变量允许一个线程等待某个事件,让另一个线程在事件发生时向它发出信号。
P线程API提供了很多支持条件变量的函数,这些函数提供条件变量的创建、等待、信号和销毁功能。
Int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
Int pthread_cond_timedwait(pthread_cond_t * cond, pthread_mutex_t * mutex,
Conststruct timespec *abstime);
Int pthread_cond_signal(pthread_cond_t *cond);
Int pthread_cond_broadcast(pthread_cond_t *cond);
Int pthread_cond_destroy(pthread_cond_t *cond);
要创建一个条件变量,只需要创建一个pthread_cond_t类型的变量,把它设置为PTHREAD_COND_INITIALIZER就可以完成初始化(和互斥的创建于初始化相似)。
Pthread_cond_t recoveryCond = PTHREAD_COND_INITIALIZER;
条件变量要求互斥的存在,并会和互斥联合工作。
(创建互斥:pthread_mutex_t recoveryMutex= PTHREAD_MUTEX_INITIALIZER)
向一个线程发送信号:
1、 互斥先锁定
2、 调用signal函数
3、 完成后 再解锁互斥
向一个线程发送信号需要调用 pthread_cond_signal函数:
pthread_mutex_lock(&recoveryMutex);
pthread_cond_signal(&recoveryCond);
pthread_mutex_unlock(&recoveryMutex);
解锁互斥后,指定的一个线程收到信号并继续执行。在互斥解锁后,一系列线程都会恢复运行(不过他们也依赖于互斥,所以实际上是一个接一个的恢复运行)
P 线程API支持把时间等待作为条件等待的一种,函数pthread_cond_timewait,允许调用者设定一个绝对时间,用于规定什么时候放弃任务返回调用者
Struct timeval currentTime;
Struct timespec expireTime;
Int ret;
….
Assert(pthread_mutex_lock(&recoveryMutex)== 0)
gettimeofday(¤tTime);
expireTime.tv_sec = currentTime.tv_sec + 1;
expireTime.tv_nsec =currentTime.tv_usec*1000;
ret = 0;
while((workload < MAX_NORMAL_WORKLOAD)&&(ret !=ETIMEOUT))
{
ret= pthread_cond_timewait(&recoveryCond, &recoveryMutex,&expireTime);
}
If (ret == ETIMEOUT)
{
//
}
Else
{
//条件到达时,执行
}
assert(pthread_mutex_unlock(&recoveryMutex)== 0);
消息队列IPC
使用消息可以实现进程之间的异步通信,(linux进程享有独立的内存空间,一个进程不能直接调用另一个进程的函数)
#include<sys/msg.h>
通常引入一个通用的头文件,定义消息读写所需要的信息
#define MAX_LINE 80
#define MY_MQ_ID 111
Typedef struct
{
Long type; //msg type
Float fval; //user message
Unsigned int uival; //usermessage
Charstrval[MAX_LINE + 1]; //user message
} MY_TYPE_T;
创建消息队列
函数msgget可以创建消息队列,该函数需要一个消息队列ID(在一个给定的主机上唯一的描述符或关键字)和另一个识别消息旗语的参数。函数返回一个句柄,它和文件描述符相似,指向特定ID的消息队列。
Int msgid;
msgid = msgget(MY_MQ_ID, 0666|IPC_CREAT);
配置一个消息队列
消息队列创建后,会自动保存创建它的一些进程的一些细节(用于授权),并把消息大小默认设置为16KB。可以使用API函数msgctl调整消息的大小。
Int msgid, ret;
Struct msg_ds buf;
//创建消息队列
msgid = msgget(MY_MQ_ID, 0);
if (msgid >= 0)
{
ret= msgctl(msgid, IPC_STAT, &buf);
buf.msg_qbytes= 4096;
ret =msgctl(msgid, IPC_SET, &buf);
if (ret == 0)
printf(“Size successfully changed for queue %d.\n”, msgid);
}
向一个消息队列中写入消息
消息队列上下文环境中的消息只有一个限制,要发送的对象必须在它的开头有一个long类型的变量,用于定义消息的类型。
消息结构体的一般形式:
typedef struct{
longtype;
charmessage[80];
}MSG_TYPE_T;
发送消息使用函数msgsnd, int msgsnd(qid, (struct msgbuf*), sizeof(MY_TYPE_T), 0);
qid为消息队列ID,由msgget()获得; 之后为消息结构体
调用函数msgsnd之后,消息就已经在消息队列中,以后任何时候都可以又同一个或不同的进程进行读取。
从消息队列中读取消息
函数msgrcv(qid , (struct msgbuf*) &myObject, sizeof(MY_TYPE_T), 1, 0);
移除消息队列
Int msgctl(msgid, IPC_RMID, NULL);
消息队列API
API函数 | 用途 |
msgget | 创建一个新的消息队列 获取消息队列ID |
msgsnd | 向消息队列发送消息 |
msgrcv | 从消息队列读取消息 |
msgctl | 获得消息队列信息 设置消息队列信息 移除消息队列 |
进程旗语【PV操作】
旗语有两种基本类型:
1、 二进制旗语
二进制旗语代表单个资源,当一个进程得到它时,其他进程都会阻断等待它被释放。
2、 计数旗语
计数旗语用来代表数量大于1的共享资源。
每次有一个进程请求缓冲区时,都会获得旗语,然后把它的值减小。当旗语的值减小到零时,进程就会阻断,直到这个值重新变得大于零。
GNU/Linux 中的旗语实际上是旗语数组,说是一个旗语,实际上代表一个包含64个旗语的数组。GNU/Linux的这个特点允许同时对很多旗语进行元操作。
创建旗语
Int semid;
semdid = semget(MY_SEM_ID, 1,0666|IPC_CREAT);
函数需要一个旗语关键字、旗语计数(表示集合中旗语的数量)、旗语旗标。
本例中,旗语关键字为MY_SEM_ID, 旗语计数为1,旗语旗标为0666|IPC_CREAT
当旗语创建成功之后,其初始值为零。
获得和释放旗语
Int semid;
Struct sembuf sb;
Semid = semget(MY_SEMD_ID, 1, 0);
If (semid >= 0)
{
/构建旗语结构体,并获得旗语
Sb.sem_num = 0;
Sb.sem_op= -1; //获得旗语选项
Sb.sem_flg = 0;
//获得旗语数据
If(semop(semid, &sb, 1) == -1)
{
Printf(“semacq:Semaphore acquired %d\n”, semid);
}
Printf(“semacq:Semaphore acquired %d\n”, semid);
//示例,释放旗语
Sb.sem_num= 0;
Sb.sem_op= 1; //释放旗语标志
Sb.sem_flg= 0;
If(semop(semid, &sb, 1) == -1)
{
Printf(“semrel:semop failed.\n”);
exit(-1);
}
Printf(“semrel: Semaphore released %d\n”, semid);
}
很多情况下,一个进程在获取旗语后又释放旗语。这可以用于同步两个进程。第一个进程试图获得旗语,然后因为旗语不可用而阻断。第二个进程,直到第一个进程因旗语阻断后释放旗语,允许第一个进程继续。
配置旗语
配置旗语,读写旗语的值(当前计数)
Int semid, cnt;
semid = semget(MY_SEM_ID, 1, 0);
if (semid >= 0)
{
//读取当前旗语的值
Cnt= semctl(semid, 0, GETVAL);
If(cnt != -1)
Printf(“semcrd:current semaphore count %d.\n”, cnt);
//设置旗语的值
Cnt= semctl(semid, 0, SETVAL, 6);
}
移除旗语
Int semid, ret;
semid = semget(MY_SEM_ID, 1, 0);
if (semid >= 0)
{
ret= semctl(semid, 0, IPC_RMID);
If(ret != -1)
printf(“Semaphore %d removed.\n”, semid);
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。