处理系统调用中的错误
系统调用会遇到哪些错误呢?每个系统调用都有自己的错误集,以open为例,如果要打开的文件不存在,或者没有权限,都会报错,返回-1,但是如何确定到底是发生了哪一类具体错误呢?
每个进程在Linux内核中都有一个task_struct结构体来维护进程相关的 信息,称为进程描述符。task_struct中有一个指针(struct files_struct *files; )指向files_struct结构体,称为文件描述符表,其中每个表项包含一个指向已打开的文件的指针,如下图所示
用户程序不能直接访问内核中的文件描述符表,而只能使用文件描述符表的索引 (即0、1、2、3这些数字),它实际上是一个由内核保存的数组下标,所以不会是负数。这些索引就称为文件描述符(File Descriptor),用int 型变量保存。 当调用open 打开一个文件或创建一个新文件时,内核分配一个文件描述符并返回给用户程序,该文件描述符表项中的指针指向新打开的文件。当读写文件时,用户程序把文件描述符传给read 或write ,内核根据文件描述符找到相应的表项,再通过表项中的指针找到相应的文件。
文件描述符数组在每个进程中都会持有一份,所以理论上是每个进程最多可以打开 1024 个文件,而不是系统中所有的进程一共只能打开 1024 个文件。
学习使用cocos2dx的时候少不了的就是在各个时间环节出打Log了,游戏人物和场景进行到哪一步,坐标是怎么遍历的都可以通过Log输出到控制台,不仅是对于游戏编程,对于非黑窗口,或者不想往黑窗口中print调试语句的时候,往VS的控制台输出调试的语句跟踪程序运行是非常舒服的办法。
C++是那种让人又爱又恨的语言,爱她的理由有很多,恨她估计也只有一点,就是她太复杂多变,需要认真的熟悉她,记忆她的点点滴滴,每个细节,才能和她很好的相处。
而c++primer就像是C++的日记本,想要了解她,翻开这本书,无疑于是最好的办法。
那么,我们就开始吧。
找到自己的基线,可以理解成直到今天所掌握的知识,表现出来的特长,能力和圈子。
不同人的基线不一样,我们的所有工作,都应该建立在这条基线上,并想办法提升它。
第二条边是理论给出的极限值,它无法突破。
第三条边是能够扶着向上攀登的绳索,或者说是阶梯。它需要你把目标拆解为具体的行动步骤,并专注其中。
找到明确的目标,,第一步就是够到这个极限,或者说补上脚下这个不足。
换句话说,上下两条线都清楚了以后,为了达成这个目标,需要一个明确的通道,里面是一个个台阶,可以理解为一个个行动步骤。通道之外,所有事情都不要做。无论什么诱惑许下多大承诺都不要理。(在自己边界内做事)
不同CPU中,4字节整数1在内存空间的存储方式是不同的。4字节整数1可用2进制表示如下:
00000000 00000000 00000000 00000001
有些CPU以上面的顺序存储到内存,另外一些CPU则以倒序存储,如下所示:
00000001 00000000 00000000 00000000
若不考虑这些就收发数据会发生问题,因为保存顺序的不同意味着对接收数据的解析顺序也不同。
CPU向内存保存数据的方式有两种:
仅凭描述很难解释清楚,不妨来看一个实例。假设在 0x20 号开始的地址中保存4字节 int 型数据 0x12345678,大端序CPU保存方式如下图所示:
图1:整数 0x12345678 的大端序字节表示
对于大端序,最高位字节 0x12 存放到低位地址,最低位字节 0x78 存放到高位地址。小端序的保存方式如下图所示:
图2:整数 0x12345678 的小端序字节表示
不同CPU保存和解析数据的方式不同(主流的Intel系列CPU为小端序),小端序系统和大端序系统通信时会发生数据解析错误。因此在发送数据前,要将数据转换为统一的格式——网络字节序(Network Byte Order)。网络字节序统一为大端序。
主机A先把数据转换成大端序再进行网络传输,主机B收到数据后先转换为自己的格式再解析。
网络字节序转换函数,如下所示:
|
|
htons() 用来将当前主机字节序转换为网络字节序,其中
h
代表主机(host)字节序,
n
代表网络(network)字节序,
s
代表short,htons 是 h、to、n、s 的组合,可以理解为”将short型数据从当前主机字节序转换为网络字节序“。
常见的网络字节转换函数有:
通常,以s
为后缀的函数中,s
代表2个字节short,因此用于端口号转换;以l
为后缀的函数中,l
代表4个字节的long,因此用于IP地址转换。
举例说明上述函数的调用过程:
|
|
运行结果:
Host ordered port: 0x1234
Network ordered port: 0x3412
Host ordered address: 0x12345678
Network ordered address: 0x78563412
另外需要说明的是,sockaddr_in 中保存IP地址的成员为32位整数,而我们熟悉的是点分十进制表示法,例如 127.0.0.1,它是一个字符串,因此为了分配IP地址,需要将字符串转换为4字节整数。
inet_addr() 函数可以完成这种转换。inet_addr() 除了将字符串转换为32位整数,同时还进行网络字节序转换。请看下面的代码:
|
|
运行结果:
Network ordered integer addr: 0x4030201
Error occured!
从运行结果可以看出,inet_addr() 不仅可以把IP地址转换为32位整数,还可以检测无效IP地址。
注意:为 sockaddr_in 成员赋值时需要显式地将主机字节序转换为网络字节序,而通过 write()/send() 发送数据时TCP协议会自动转换为网络字节序,不需要再调用相应的函数。
Linux 不区分套接字文件和普通文件,使用 write() 可以向套接字中写入数据,使用 read() 可以从套接字中读取数据。
前面我们说过,两台计算机之间的通信相当于两个套接字之间的通信,在服务器端用 write() 向套接字写入数据,客户端就能收到,然后再使用 read() 从套接字中读取出来,就完成了一次通信。
write() 的原型为:
|
|
fd 为要写入的文件的描述符,buf 为要写入的数据的缓冲区地址,nbytes 为要写入的数据的字节数。
size_t 是通过 typedef 声明的 unsigned int 类型;ssize_t 在 “size_t” 前面加了一个”s”,代表 signed,即 ssize_t 是通过 typedef 声明的 signed int 类型。
write() 函数会将缓冲区 buf 中的 nbytes 个字节写入文件 fd,成功则返回写入的字节数,失败则返回 -1。
read() 的原型为:
|
|
fd 为要读取的文件的描述符,buf 为要接收数据的缓冲区地址,nbytes 为要读取的数据的字节数。
read() 函数会从 fd 文件中读取 nbytes 个字节并保存到缓冲区 buf,成功则返回读取到的字节数(但遇到文件结尾则返回0),失败则返回 -1。
Windows 和 Linux 不同,Windows 区分普通文件和套接字,并定义了专门的接收和发送的函数。
从服务器端发送数据使用 send() 函数,它的原型为:
|
|
sock 为要发送数据的套接字,buf 为要发送的数据的缓冲区地址,len 为要发送的数据的字节数,flags 为发送数据时的选项。
返回值和前三个参数不再赘述,最后的 flags 参数一般设置为 0 或 NULL,初学者不必深究。
在客户端接收数据使用 recv() 函数,它的原型为:
|
|
调用 close()/closesocket() 函数意味着完全断开连接,即不能发送数据也不能接收数据,这种“生硬”的方式有时候会显得不太“优雅”。
图1:close()/closesocket() 断开连接
上图演示了两台正在进行双向通信的主机。主机A发送完数据后,单方面调用 close()/closesocket() 断开连接,之后主机A、B都不能再接受对方传输的数据。实际上,是完全无法调用与数据收发有关的函数。
一般情况下这不会有问题,但有些特殊时刻,需要只断开一条数据传输通道,而保留另一条。
使用 shutdown() 函数可以达到这个目的,它的原型为:
|
|
sock 为需要断开的套接字,howto 为断开方式。
howto 在 Linux 下有以下取值:
howto 在 Windows 下有以下取值:
至于什么时候需要调用 shutdown() 函数,下节我们会以文件传输为例进行讲解。
确切地说,close() / closesocket() 用来关闭套接字,将套接字描述符(或句柄)从内存清除,之后再也不能使用该套接字,与C语言中的 fclose() 类似。应用程序关闭套接字后,与该套接字相关的连接和缓存也失去了意义,TCP协议会自动触发关闭连接的操作。
shutdown() 用来关闭连接,而不是套接字,不管调用多少次 shutdown(),套接字依然存在,直到调用 close() / closesocket() 将套接字从内存清除。
调用 close()/closesocket() 关闭套接字时,或调用 shutdown() 关闭输出流时,都会向对方发送 FIN 包。FIN 包表示数据传输完毕,计算机收到 FIN 包就知道不会再有数据传送过来了。
默认情况下,close()/closesocket() 会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,而shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包。也就意味着,调用 close()/closesocket() 将丢失输出缓冲区中的数据,而调用 shutdown() 不会。
客户端中直接使用IP地址会有很大的弊端,一旦IP地址变化(IP地址会经常变动),客户端软件就会出现错误。
而使用域名会方便很多,注册后的域名只要每年续费就永远属于自己的,更换IP地址时修改域名解析即可,不会影响软件的正常使用。
关于域名注册、域名解析、host 文件、DNS 服务器等本节并未详细讲解,请读者自行脑补。本节重点讲解如何使用域名。
域名仅仅是IP地址的一个助记符,目的是方便记忆,通过域名并不能找到目标计算机,通信之前必须要将域名转换成IP地址。
gethostbyname() 函数可以完成这种转换,它的原型为:
|
|
hostname 为主机名,也就是域名。使用该函数时,只要传递域名字符串,就会返回域名对应的IP地址。返回的地址信息会装入 hostent 结构体,该结构体的定义如下:
|
|
从该结构体可以看出,不只返回IP地址,还会附带其他信息,各位读者只需关注最后一个成员 h_addr_list。下面是对各成员的说明:
hostent 结构体变量的组成如下图所示:
下面的代码主要演示 gethostbyname() 的应用,并说明 hostent 结构体的特性:
|
|
|
|
通过 socket() 函数创建了一个套接字,
第一个参数 AF_INET 表示是地址族(address family),用来确定套接字将使用的网络。实际上几乎从来不需要使用任何其他类型,几乎总是使用AF_INET地址族。
AF 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。
第二个参数是套接字类型(Socket Type),SOCK_STREAM 表示使用面向连接的数据传输方式。如果想使用数据报的数据传输方式可以使用SOCK_DGRAM。
第三个参数是协议。IPPROTO_TCP 表示使用 TCP 协议。也是最流行的SOCK_STREAM协议。最流行的SOCK_DGRAM协议是IPPROTO_UDPhe IPPROTO_ICMP。
例:
匿名管道pipe是单向的,或者说是半双工的。
提供了一个和它有共同先祖的进程的通信方式。
注意:如果需要全双工通信,应该转而考虑套接字API
另一种类型的管道被称为命名管道。一个命名管道像正常管道一样工作,但是它存在于文件系统中,因而任意进程都可以找到它。这使得不同先祖的进程之间也可以进行通信。
注意:管道只不过是一对文件描述符,因此所有能够操作文件描述符的函数都可以用于管道。这些函数包括但不限于 select,read,write,fcntl,freopen。
标准I/O提供的f系列I/O文件操作,如(fwrite,fread,fseek等)都是提供了有缓存的接口,如果数据需要送到像控制终端等交互设备,必须要设置fflush或者使用非缓存I/O。
而缓存可以减少系统调用的次数,增加效率。
这是标准I/O相对于系统调用的特点,也是优点。
而系统调用的I/O读写是无缓冲的,实时性高。
到这里我们还是没有说明白为什么标准 IO 吞吐量高,而系统 IO 实时性高。我再举个简单的栗子:门卫老大爷负责送信到邮局,他去一次邮局要花费 10 分钟的时间,而每次最多能送 20 封信,每当信件累计到 20 封的时候他就要动身去邮局了。但是当他收到一封加急的邮件时,就会立即去一趟邮局。系统 IO 就好比每收到一封信时都要去一趟邮局,所以实时性高。而标准 IO 就好比要攒够 20 封信才去一趟邮局,所以吞吐量高,因为用户把信件交到老大爷的手上时就会立即返回,响应速度快,用户体验更好。而我们使用 fflush(3) 之类的函数强制刷新缓冲的时候,就相当于是老大爷收到了一封加急信件需要立即去一趟邮局送信。
|
|
这是今天要学习的第一个函数,在操作文件之前,我们需要通过 fopen() 函数将文件打开,通过这个函数我们可以告诉操作系统我们要操作的是哪个文件,以及用什么样的方式操作这个文件。
参数列表:
path:要操作的文件路径。
mode:文件的打开方式,这个打开方式一共分为6种。
r:以只读的方式打开文件,并且文件位置指针会被定位到文件首。如果要打开的文件不存在则报错。
r+:以读写的方式打开文件,并且文件位置指针会被定位到文件首。如果要打开的文件不存在则报错。
w:以只写的方式打开文件,如果文件不存在则创建,如果文件已存在则被截断为 0 字节,并且文件位置指针会被定位到文件首。
w+:以读写的方式打开文件,如果文件不存在则创建,如果文件已存在则被截断为 0 字节,并且文件位置指针会被定位到文件首。
a:以追加的方式打开文件,如果文件不存在则创建,且文件位置指针会被定位到文件最后一个有效字符的后面(EOF,end of the file)。
a+:以读和追加的方式打开文件,如果文件不存在则创建,且读文件位置指针会被初始化到文件首,但是总是写入到最后一个有效字符的后面(EOF,end of the file)。
返回值:
FILE 是一个由标准库定义的结构体,各位童鞋不要企图通过手动修改结构体里的内容来实现文件的操作,一定要通过标准库函数来操作文件。
这个函数返回一个 FILE 类型的指针,它作为我们打开文件的凭据,后面所有对这个文件的操作都需要使用这个指针,而且使用之后一定不要忘记调用 fclose(3) 函数释放资源。
如果该函数返回了一个指向 NULL 的指针,则表示文件打开失败了,可以通过 errno 获取到具体失败的原因。
|
|
这个函数是与 fopen(3) 函数对应的,当我们使用完一个文件之后,需要调用 fclose(3) 函数释放相关的资源,否则会造成内存泄漏。当一个 FILE 指针被 fclose(3) 函数成功释放后,这个指针所指向的内容将不能再次被使用,如果需要再次打开文件还需要调用 fopen(3) 函数。
参数列表:
fp:fopen(3) 函数的返回值作为参数传入即可。
|
|
从输入流 stream 中读取一个字符串回填到 s 所指向的空间。
这里出现了一个 stream 的概念,这个 stream 是什么呢,它被成为“流”,其实就是操作系统对于可以像文件一样操作的东西的一种抽象。它并非像自然界的小河流水一样潺潺细流,而通常是要么没有数据,要么一下子来一坨数据。当然 stream 也未必一定就是文件,比如系统为每个进程默认打开的三个 stream:stdin、stdout、stderr,它们本身就不是文件,就是与文件有着相同的操作方式,所以同样被抽象成了“流”。
这个函数并没有解决 gets(3) 函数可能会导致的数组越界问题,而是通过牺牲了获取数据的正确性来保证程序不会出现数组越界的错误,实际上是掩盖了 gets(3) 的问题。
该函数遇到如下四种情况会返回:
1.当读入的数据量达到 size - 1 时;
2.当读取的字符遇到 \n 时;
3.当读取的字符遇到 EOF 时;
4.当读取遇到错误时;
并且它会在读取到的数据的最后面添加一个 \0 到 s 中。
返回值:
成功时返回 s。
返回 NULL 时表示出现了错误或者读到了 strem 的末尾(EOF)。