【转载】socket as an IPC

socket 是IPC的一种,是解决不同计算机上进程相互通信的机制。总的来说,socket就是通信端点的逻辑代表。即然代表的是通信端点,所以就要有相关参数反映通信端点的性质。这个socket所代表的端点有什么特征呢?――这个端点在哪?通信方式是什么?怎么通信?等等,弄清楚这些问题,对socket的理解就很easy了。下边让我们先来看看:

(1)socket总述:首先什么是套接字?->套接字是通信端点的抽象,就是这个通信端点的逻辑代表。fd->文件,then, 套按字描述符->套接字->通信端点。套接字描述符在UNIX中是用文件描述符来实现的。许多处理文件描述符的函数都可以处理套按字描述符。要创建一个套接字,可以用socket函数:int socket(int domain, int type, int protocol);若成功,则返回文件/套接字的描述符,出错返回-1。现在看看为什么这么调用?刚说了:套按字描述符->套接字->通信端点,既然套接字代表的是通信端点,那么自然会问,代表的是什么样的端点呢?即这个通信端点有什么特征?->端点在哪?通信有什么性质?

->端点在哪?->在哪个网即域?确定了通信的特征,包括地址格式。每个域有自己的格式表示地址,而表示各个域的常数都以AF_开头,意指地址族。通信域主要有:AF_INET,AF_INET6,AF_UNIX,AF_UNSPEC。哪个主机?哪个进程即端口上?这些都反映了端点的地址信息。

->通信的特征?->套接字的类型?进一步确定通信的的特征。即用什么方式通信,即SOCK_DGRAM(长度固定的,无连接的不可靠报文传递)、SOCK_RAW(IP协议的数据报接口)、SOCK_SEQPACKET(长度固定,有序,可靠的面向连接报文传递)、SOCK_STREAM(有序、可靠、双向的面向连接字节流)。->参数protocol通常是零(域和类型决定协议),表示按给定的域和套接字类型选择默认协议。当对同一域和套接字类型支持多个协议时,可以使用protocol参数选择一个特定协议。在AF_INET通信域中套接字类型SOCK_STREAM的默认协议是TCP。在AF_INET通信域中套接字类型SOCK_DGRAM的默认协议是UDP。有几点说明:(1)对于数据报SOCK_DGRAM接口,与对方通信时是不需要逻辑连接的。只需要送出一个报文,其地址是一个对方进程所使用的套接字。因此数据报提供了一个无连接的服务,另一方面,字节流SOCK_STREAM要求在交换数据之前,在本地套接字和与之通信的远程套按字之间建立一个逻辑连接。数据报是一咱自包含报文,相当于给某个人发信件,可以邮寄很多信,但不能保证投递的次序,并且可能有些信件丢失在路上。(2)使用面向连接的协议通信就像与对方打电话。首先,需要通过电话建立一个连接,连接建好了之后,彼此能向双向地通信。每个连接是端到端的通信信道。因为提前建立连接,所以会话中不包含地址信息,就像呼叫的两端存在一个点对点虚拟连接,并且连接本身暗含特定的源和目的地。(3)对于SOCK_STREAMS套接字,应用程序意识不到报文界限,因为套接字提供的是字节流服务。这意味着当从套接字读出数据时,它也许不会返回反有由发送进程所写的字节数。最终可以获得发送过来的所有数据,但也许要通过若干次函数调用得到。(4)SOCK_SEQPACKET套接字与上一个很像,但是,从该套接字得到的是基于报文的服务而不是字节流服务。这意味着从SOCK_SEQPACKET套接字接收的数据量与对方所发送的一致流控制传输协议SCTP提供了因特网域上的顺序数据包服务。(5)SOCK_RAW套按字提供一个数据报接口用于直接访问下面的网络层,使用这个接口时,应用程序负责构造自己的协议首部,这是因为传输协议TCP、UDP被绕过了。当创建一个原始套接字时需要有超级用户的特权,用以防止恶意程序绕过内建安全机制来创建报文。这么理解吧,在应用程序用调用这个函数,实际是要实现:应用层->传输层->IP层。在应用层就是要调用socket,但是要socket帮助实现传输层和IP层,这就是由内核帮助完成的,所以这个socket是系统调用。但是在应用层调用socket时,要传递相关信息告诉socket怎么完成些数据层层向下的格式转换,即在TCP层:协议(TCP/IP)、套接字类型(怎么传递这些数据,是数据报还是字节流,是连接的还是无连接的,等等)->在IP层:主要是寻址的问题,即自己和对方的通信端点在哪(在什么网中?->在哪个主机上?->在哪个进程中即端口中?)。调用socket与调用open一样,均可获得用于输入/输出的文件描述符,当不再需要该文件描述符时,调用close来关闭对文件或套接字的访问,并用释放该描述符以便重新使用。虽然套接字描述符本质上是一个文件描述符,但是不是所有的参数为文件描述符的函数都可以接受套接字描述符。套接字通信是双向的,可以采用shutdown来禁止套按字上的输入/输出。int shutdown(int sockfd,int how);如果how,是SHUT_RD,即关闭读端,那么无法从套接字读取数据,如果是SHUT_WR即关闭了写端,则无法使用套接字发送数据。使用SHUT_RDWR则将同时无法读取和发送数据。为什么使用shutdown呢,在close函数存在的情况下?首先,close只有在最后一个活动引用被关闭时才释放网络端点。这意味着如果复制一个套接字,套接字直到关闭了最后一个引用它的文件描述符之后才会被释放。而shutdown允许使一个套接字处于不活动状态,无论引用它的文件描述符数目是多少。其次,有时只关闭套接字双向传输中的一个方向会很方便。

(2)寻址问题:这是关于如何确定一个目标通信进程的问题,在什么网上?->在哪个机上?->在哪个端口上即服务或具体的进程?

由 于在不同的计算机上进行通信,所以要考虑每个计算机个的数据存储的表示。先看字节序的问题:这是一个处理器架构特性,用于指示像整数这样的大数据类型的内 部字节顺序(物理内存的字节序)。先看存储器:高址部分<-低址部分。大端:最大字节地址对应于数字最低有效字节上,也就是相当于从内存的最大字节序开始 是数据的开始即最小位,大端简记为从大端开始是数据的开始(就是最低位)。对于数字1234,则按这种方式存放就是:4321的顺序。小端:最大的字节地址对应于数字最高有效字节上(从小端开始是数据的开始),对于数字1234,则按这种方式存放就是:1234,即最高数字1,放在最高地址部分上。换个方式,注意,不管字节如何排序,数字最高位总是在左边,最低位总是在右边。对于一个数,最高位与最低位是一定的。其实就是给你一个数字1234,这个数是定下来的,怎么为其分配存储字节?为1分配最大字节地址,就是小端(数字所在的地位与存储是一致的,最高位的数字,占据最高的字节位),其实就是大数在大字节上。如果为1分配最小的字节地址,就是大端,就是大数在最小的字节上。先定数->为数分配字节。一般UNIX用的是大端,而LINUX用的是小端。对于一个给定的内存,低->高字节序,对于大端,有顺序读,顺序存的特性,比如1234,则在读时,就可以将其按读的顺序1.2.3.4存入由低址->高址的内存中,最高的1,却放在最低的字节序上。而小端就相当于先顺序读一遍,1.2.3.4,并入栈,然后,按由低址->高址的顺序,将出栈的顺序4.3.2.1,存入由低址->高址的部分。所以1对就于高址字节序。网络协议指定了字节序,因此异构计算机能够换信息不会混淆字节序。TCI/IP协议栈采用的是大端字节序,即对于内存由低址->高址字节序,顺序读时并存入内存中。最高位放在了最低字节序上。内存:低->高 , 大端在内存中的表现为:1234,最高位1在低字节序上。 小端在内存中的表现:4321。最高位1在最高字节序上。对于TCP/IP,地址用网络字节序来表示,所以应用程序有时需要在处理器的字节序(当与网络字节序不同时)与网络字节序之间进行转换。进程1的处理器字节序<->网络字节序(根据其网络协议而定)<->进程2的处理器字节序。提供了四个函数以实施在处理器字节序与网络字节序之间的转换:uint32_t htonl(uint32_t hostint32)//处理器字节序转换为网络字节序。unit16_t htons(uint16_t hostint16); uint32_t ntohl(uint32_t netint32); uint16_t ntohs(uint16_t netint16);注意这四个函数在使用时转换的方向,是什么向什么转换。->地址格式(地址标识了特定通信域中的套接字端点,即这个端点在哪?->在什么网/域中?->在哪个主机上?->在哪个进程中或服务端口?所以为了正确的标识一个套按字端点,这三个方面一个也不能少,这三个的整体组成了地址。为使不同的格式地址能够被传入到套接字函数,地址被强制转换成通用的地址结构sockaddr,即套接字中的地址,其表示为:

struct sockaddr

{ sa_family_t sa_family;//标识在哪个域上,或网中。

char sa_data[];

…}

套接字的实现可以自由的添加额外的成员,并定义sa_data成员的大小。)按域可分为sockaddr_in,sockaddr_in6,但是这两者在应用时,均被强制转换成sockaddr结构传入到套接字例程中。有时需要打印出能被人而不是计算机所理解的地址格式,需要要实现网络地址在二进制地址格式和点分十进制字符串表示之间的相互转换。inet_ntop()//将网络字节序的二进制地址转换成文本字符串格式。inet_pton将文本字符串格式转换成网络字节序的二进制地址。->地址查询:可以通过很多函数来访问各种网络配置信息。通过调用gethostent\sethostent\endhostent,找到与给定计算机的主机信息。当gethostent返回时,得到一个指向hostent结构的指针,该结构可能包含一个静态的数据缓冲区,每次调用gethostent将会覆盖这个缓冲区。返回的地址采用网络字节序。能够采用一套相似的接口来获得网络名字和网络号。getnetbyaddr()//通过地址获得网络的相关信息\getnetbyname()//通过名字来获得网络相关信息\getnetent()\setnetent()\endnetent()。网络号按照网络字节序返回。地址类型是一个地址族常量。可以将协议名字和协议号采用以下函数映射:getprotobyname()\getprotobynumber\getprotoent()\setprotoent()\endprotoent()。服务是由地址的端口号部分表示的,每个服务是由一个唯一的,熟知的端口号来提供。采用getservbyname可以将一 个服务名字映射到一个端口号,函数getservbyport将一个端口号映射到一个服务名,或者采用函数getservent顺序扫描服务数据库。getservbyname\getservbyport\getservent\setservent\endservent.允许将一个主机名和服务名字映射到一个地址,或者相反。getaddrinfo,如果getaddrinfo失败,不能使用perror或strerror来生成错误消息。替代地,调用gai_strerror将返回的错误码转换成错误消息。函数getnameinfo将 地址转换成主机名或者服务名。总之:主机->网络->协议->服务->地址。->将套接字与地址绑定:与客户端的套接字关联的地址没有太大意义,可以让系 统选一个默认的地址,然而,对于服务器,需要给一个接收客户端请求的套接字绑定一个众所周知的地址。客户端应有一种方法来发现用以连接服务器的地址,最简 单的方法就是为服务器保留一个地址并且在/etc/services或某个名字服务中注册。可以用bind函数将地址绑定到一个套接字。int bind(int sockfd,const struct sockaddr *addr,socklen_t len);//相当于将套接字看成某个具体的端点的抽象代表。对于所能使用的地址有一些限制:(1)在进程所运行的机器上,指定的地址必须有效,不能指定一个其它机器的地址,即调用进程只能为本机上的进程服务与一个套接字绑定。(2)地址必须和创建套接字时的地址族所支持的格式相匹配(定义socket只是说明这个socket支持什么样性质的端点。)。(3)端口号必须不小于1024,除非该进程具有的特权。(4)一般只有套接字端点能够与地址绑定,尽管有些协议允许多重绑定。对于INTERNET,如果指定IP地址为INADDR_ANY,套接字端点可以绑定到所有的系统网络接口。这意味着可以接收到这个系统所安装的所有网卡的数据包。以后会看到,如果调用connect listen,但没有绑定地址到一个套接字,系统会选一个地址并将其绑定到套接字。socket()只是建立一个抽象的端点,并说明这个端点支持什么样性质的端点->bind(),与一个具体的地址绑定,即相当于将这个抽象的端点看成一个具体端点的代表。可以调用getsockname来发现绑定到一个套接字的地址。可以调用getpeername来找到对方的地址,如果套接字已经和对方连接的话。

(3)建立连接:如果处理的是面向连接的网络服务,在开始交换数据之前,需要在请求服务的进程套接字和提供服务的进程套接字之间建立一个连接。可以用connect建立一个连接。int connect(int sockfd, const struct sockaddr *addr,socklen_t len);在connect中所指定的地址是想与之通信的服务器(对方的socket)。如果sockfd没有绑定到一个地址这(这个sockfd是调用进程所在主机的socket),connect会给调用者绑定一个默认地址。当连接一个服务器时,出于一些原因,连接可能失败,要连接的机器必须开启并且正在运行,服务器必须绑定到一个想与之连接的地址,并且在服务器的等待连接队形中应有足够的空间。函数connect不可以用于无连接的网络服务,实际上却是一个不错的选择。如果sock_dgram套接字上调用connect,所有发送报文的目标地址设为connect调用中所指定的地址,这样每次传送的报文时就不需要再提供地址。另外,仅能接收来自指定地址的报文。服务器调用listen来宣告可以接受的连接请求。 int listen(int dockfd, int backlog);//sockfd是被监听的socket,而backlog是表示该进程所要入队的连接请求数量,其实际值由系统决定。一旦队列满,系统会拒绝多余连接请求,所以backlog的值应该基于服务器期望负载和接受连接请求与启动服务的处理能力来选择。一旦服务器调用了listen,套接字就能接收连接请求。使用accept获得连接请求并建立连接。int accept(int  sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);//sockfd是服务器端的这个服务的端点,若成功,则返回的文件描述符是套接字描述符,该描述符连接到调用connect的客户端。这个新的套接字描述符和原始套接字sockfd具有相同的套接字类型和地址族。传给accept的原始套接字没有关联到这个连接上,而是继续保持可用状态并授其他连接请求。如果不关心客户端标识,可以将参数addr和len设为null。否则,在调用accept之前,应将参数addr设为足够大的缓冲区来存放地址,并用将len设为指向代表这个缓冲区大上的整数的指针。返回时,accept会在缓冲区填充客户端的地址并且更新指针len所指向 的整数为该地址的大小。如果没有连接请求,accept会阻塞直到一个请求的到来。如果sockfd处于非阻塞模式,accept会返回-1并将errno设置为EAGAIN或EWOURLBLOCK。也可以用poll select来等待一个请求的到来。

client: socket()->bind()->connect().

server: socket()->bind()->listen()->accept()

(4)数据传输: 既然将套接字端点表示为文件描述符,那么只要建立连接,就可以使用read和write来通过套接字通信。在套接字描述符上采用read和write是非常有意义的,因为可以传递套接字描述符到那些原先设计为处理本地文件的函数。而且可以安排传递套接字描述符到执行程序的子进程,该子进程并不了解套字。但是想指定选项、从多个客户端接收数据包或者发送带外数据,需要采用6个传递数据的套接字函数中的一个。三个函数用来发送数据:ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);//可以指定标志来改变处理传输数据的方式,使用send时,套接字必须已经连接。如果send成功返回,并不必然表示连接另一端的进程接收数据,所保证的仅是当send成功返回时,数据已经无错误地发送到网络上。如果单个报文超过了协议所支持的最大尺寸,send失败,并将errno设为EMSGSIZE,对于字节流协议,send会阻塞直到整个数据被传输。ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags, const struct sockaddr *destaddr, socklen_t destlen);对于面向连接的套接字,目标地址是忽略的,因为目标地址蕴涵在连接中,对于无连接的套接字,不能使用send,除非在调用connect时预先设定了目标地址,或者采用sendto来提供另外一种发送报文的方式。可以使用不止一个的选择来通过套接字发送数据,可以调用带有msahdr结构的sendmsg来指定多重缓冲区传输数据,这和writev很想像。ssize_t  sendmsg(int sockfd, const struct msghdr *msg, int flags);发送时有两种:(1)有connect()建立了连接,则后来可用send,sendmsg.(2)没有用connect()建立连接,则要指定目标地址,用sendto();对于写也有三个函数:recv 和read一样,但是允许指定选项来控制如何接收数据。ssize_t  recv(int  sockfd,void *buf, size_t nbytes, int flags);iv 当指定MSG_PEEK标志时,可以查看上一个要读的数据但不会真正取走。当再次调用read 或recv函数时会返回刚才查看的数据。对于SOCK_STREAM套接字,接收的数据可以比请求的少。标志MSG_WAITALL阻止这种行为,除非所需数据全部收到,recv函数才会返回。对于SOCK_DGRAM和SOCK_SEQPACKET套接字,MSG_WAITALL标志没有改变什么行为,因为这些基于报文的套接字类型一次读取就返回整个报文。如果发送者已经调用shutdown来结束传输,或者网络协议支持默认的顺序关闭且发送端已经定位,那么当所有的数据接收完毕后,recv返回0。可以使用recvfrom来得到数据发送者的源地址。ssize_t recvfrom(int sockfd, void *restrict buf, size_t len,int  flags, sruct sockaddr *restrict addr, socklent_t *restrict addrlen);通常用于无连接的套接字,否则,其等同于recv,也可以使用recvmsg ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

(5)套接字选项:套接字机制提供了两个套接字选项接口来控制套接字行为,一个接口用来设置选项,另一个接口允许查询一个选项的状态。可以获取或设置三种选项:(1)通用选项,工作在所有套接字类型上。(2)在套接字层次管理的选项,但是依赖于下层协议的支持。(3)特定于某协议的选项,为每个协议所独有。int setsockopt(int sockfd,int level, int option, const void *val, socklen_t len);参数level标识了选项应用的协议。可以使用getsockopt函数来发现选项的当前值。

(6)带外数据:out-of-band data,是一些通信协议所支持的可选特征,允许更高优先级的数据比普通数据优先传输。即使传输队列已经有数据,带外数据先行传输。TCP支持带外数据,但是UDP不支持。套接字接口对带外数据的支持,很大程度上受TCP带外数据具体实现的影响。TCP将带外数据称为“紧急”数据。TCP仅支持一个字节的紧急数据,但是允许紧急数据在普通数据传送机制数据流之外传输。为了产生紧急数据,在三个send函数中任何一个指定标志MSG_OOB,如果带MSG_OOB标志传输字节超过一个时,最后一个字节被看作紧急数据字节。如果安排发生套接字信号,当接收到紧急数据时,那么发送信号SIGURG。TCP支持紧急标记的概念:在普通数据流中紧急数据所在的位置。如果采用套接字选项SO_OOBINLINE,那么可以在普通数据中接收紧急数据。为帮助判断是否接收到紧急标记,可以使用函数sockatmark.int sockatmark(int sockfd);当下一个要读的字节在紧急标志所标识的位置时,sockatmark返回1。当带外数据出现在套接字读取队列时,select函数会返回一个文件描述符并且拥有一个异常状态挂起。可以在普通数据流上接受紧急数据,或者在某个recv函数中采用MSG_OOB标志在其他队列数据之前接收紧急数据。TCP队列仅有一字节的紧急数据,如果在接收当前的紧急数据字节之前又有新的紧急数据到来,那么当前的字节会被丢弃。

(7)非阻塞和异步I/O:通常recv函数没有数据可用时会阻塞等待,同样地,当套接字输出队列没有足够空间来发送消息时函数send会阻塞。在套接字的非阻塞模式下,行为会改变。在这些情况下,这些函数不会阻塞而是失败,设置errno为EWOULD或者EAGAIN。当这些发生时,可以使用poll或select来判断何时能接收或传输数据。在基于套接字的异步I/O中,当能够从套接字中读取数据,或者套接字写队列中的空间变得可用时,可以安排发送信号SIGIO。

 

在Linux和UNIX中有很多的输入输出函数,有时真是让想跟它攀点关系的菜鸟们束手无策。先来看看都有哪些函数,通过解析与总结,看看能不能让大家能这些函数有个理性的认识,哦,原来是这么回事,也就算我没白花这份闲。

内核文件I/O->标准库I/O->高级I/O->IPC中

  1. read()/write();
  2. pread()/pwrite();
  3. getc()/putc();
  4. fgetc()/fputc();
  5. getchar()/putchar();
  6. ferror()/feof();
  7. fgets()/fputs();
  8. gets()/puts();
  9. fread()/fwrite();
  10. scanf()/fscanf()/sscanf()/vscanf()/vfscanf()/vsscanf()
  11. printf()/fprintf()/sprintf()/snprintf()/vprintf()/vfprintf()/vsprintf()/vsanprintf()
  12. readv()/writev()
  13. read()/written()
  14. msgrcv()/msgsnd()
  15. revc()/recvfrom()/recvmsg()
  16. send()/sendto()/sendmsg()
  17. recv_fd()/send_fd()/send_err()

粗略总结了下,有如上边所示的17个大类,咋一看,的确让人头有点小晕。但是大师们都说存在的就是合理的,下边让我们看看,是怎么样的深入浅出,让这些函数有了存在的理由。要理解这些,先要知道系统在输入输出时所要经过的逻辑处理模块是怎样。如下图示

080707175006

 

以上的用户空间的应用程序利用系统调用完成文件的读写过程,说明如下:

(1)       用户空间与内核空间;这一组关系不用说明了。

(2)       读与写:都将cpu或是内存或是用户程序看成主体,则读,内存<-文件;写,内存->文件;因为主体是用户程序,所以在读或是写是,对读,要确定从什么读,对写,向什么写。

(3)       应用程序利用系统服务有三条路:通过shell命令等直接实现;利用库函数实现;直接调用系统调用的函数,如read,write等命令。在这里可以将库函数与系统调用的关系看清楚了。系统调用是最基本的了,任何想要获得系统服务的都要经过它,这是个关卡。

(4)       文件I/O与标准I/O:前者是指在用户空间中不需要其实进程明确提供一个缓冲(如图中的bf2),其实就是进程在用户空间直接调用read/write等函数,但是,在内核空间中都是要有缓冲的。这一般称为文件I/O。标准I/O:提供了一种对不用缓冲I/O的函数(这些函数即可以用于不用缓冲的I/O函数,也可以有于带有缓冲的I/O函数)的带缓冲的接口。这一般是库函数在用户空间建立的(这些缓冲由库函数完成,不需用户自己管理,是封装在库函数中的),如BUF2,可能是库函数想将对从上层接收过来的数据做个预处理,如格式变换等。使用标准I/O函数可以无需担心如何选取最佳的缓冲区大小(由库函数为你完成),还有一个是简化了对输入行的处理。标准I/O函数库提供了使我们能够控制该库使用的缓冲风格的函数。

(5)       BUF1/BUF2/BUF3:BUF1,其实是用户空间的一些字符串,变量等,理解为数据即可。有时也定义为名称BUF的形式,如char buf[MAXLINE];,但此时BUF只是名称叫BUF而矣,区别于真正的缓冲区的概念。BUF2,这是库函数为您老在用户空间建立的,不用您亲自管理,您只要一声令下,如调用个库函数中某个函数,自有人为你服务,这个BUF2,我们称之为真正的缓冲区。BUF3,不论您是选择文件I/O的形式还是标准I/O的形式,不论是哪一种,在内核中的都要用到缓冲区BUF3(这是怎么样都免不了的),但是这个也不要用户来亲力亲为,由内核代为管理。

(6)       流(stream):这是标准为I/O中用到的,流是文件的逻辑代表,将文件I/O的:    进程->fd->文件,改变为:进程->fp(FILE对象)->流/缓冲->文件。原来对文件的操作,现在用户只用处理:进程->流之间的操作,而流->文件之间的操作将由库函数为你完成。流的逻辑表示就是FILE对象,而流的实体就是流使用的缓冲区,这些缓冲区相对于应用进程来说就是文件的代表。流=FILE + 缓冲。标准I/O库提供缓冲的的是尽可能减少使用read 和write的次数。

好,暂此做以上四点说明吧,待有想法时再添加。下边进入正题,看看上边的这些函数,是什么形式的,为什么要有这些函数的存在,都为系统做些什么,怎么做的。

  1. 文件I/O相关(进程->fd->文件)(文件fd, buf):

(1)       read()

形式:#include<unistd.h>

ssize_t  read (int filedes,  void *buf,  size_t  nbytes );

成功:返回读到的字节数;出错:返回-1;文件尾:返回0;

原因:基本系统调用功能;

实现:文件(由filedes所指)-读nbytes字节->内存buf中。

补充:有多种情况可使实际读到的字节数少于要求读的字节数:

当从普通文件读时,在读到要求字节数之前已到达了文件尾端。

当从终端设备读时,通常一次最多读一行。

当从网络读时,网络中缓冲机构可能造成返回值小于所要求读的字节数。

当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么只返回实际用

的字节数。

当从某些面向记录的设备读时,一次最多返回一个记录。

当某一信号造成中断,而已经读了部分数据量时。

读操作从文件的当前偏移量处开始,在成功返回之前,该偏移量将增加实际读到的字节数。常用的unix系统shell都提供一种方法,它在标准输入上打开一个文件,在标准输出上追寻或重写一个文件,这使得程序不必自行打开输入和输出文件。

(2)       write()

形式:#include<unistd.h>

ssize_t  write (int filedes,  const void *buf,  size_t  nbytes );

成功:返回已写的字节数;出错:返回-1;

原因:基本系统调用功能;

实现:文件(由filedes所指)<-写nbytes字节-内存buf中。

补充:write出错的一个常见的原因是:磁盘已写满,或者超过了一个给定进程的文件长度限制。对于普通文件,写操作从文件的当前偏移量处开始。如果在打开该文件时,指定了O_APPEND选项,则在每次写操作之前,将文件偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数。

(3)       pread()

形式:#include<unistd.h>

ssize_t  pread (int filedes,   void *buf,  size_t  nbytes,  off_t  offset );

成功:返回读到的字节数;出错:返回-1;到文件结尾:返回0

原因:由于lseek和read 调用之间,内核可能会临时挂起进程,所以对同步问题造成了问题,调用pread相当于顺序调用了lseek 和 read,这两个操作相当于一个捆绑的原子操作。

实现:文件(由filedes所指)-读nbytes字节->内存buf中。

补充:调用pread时,无法中断其定位和读操作,另外不更新文件指针。

(4)       pwrite()

形式:#include<unistd.h>

ssize_t  pwrite (int filedes,   const void *buf,  size_t  nbytes,  off_t  offset );

成功:返回已写的字节数;出错:返回-1;

原因:由于lseek和write 调用之间,内核可能会临时挂起进程,所以对同步问题造成了问题,调用pwrite相当于顺序调用了lseek 和 write,这两个操作相当于一个捆绑的原子操作。

实现:文件(由filedes所指)<-写nbytes字节-内存buf中。

补充:调用pwrite时,无法中断其定位和读操作,另外不更新文件指针。

 

  1. 流(stream)或标准I/O( 进程->fp->流(FILE+缓冲)->文件)(内存buf, 流fp):

每次输入一个字符:

(1)       getc();

格式:#include <stdio.h>

int getc(FILE *fp);

成功:返回下一个字符;出错:返回EOF;文件尾:EOF;

实现:内存 <-读一个字符c- 流(由fp所指的流,是文件的逻辑代表)

原因:在标准I/O中用,将流看成文件的逻辑代表,将对进程->文件的操作,现转换为进程->流(也就是相当于文件)的操作。

补充:函数在返回下一个字符时,会将其unsigned char类型转换为int类型。为不带符号的理由是,如果最高位是1也不会使返回值为负。要求整形返回值的理由是,这样就可以返回所有可能的字符值再加上一个已出错或已到达文件尾端的指示值。即字符值变为正的int值,负的值就是出错或是到达文件尾端。(负值表特殊意义),同时不论是出错还是到达文件尾端,这三个函数都返回同样的值即都是-1。由于每个流在FILE对象中维持了两个标志,即出错标志和文件结束标志,为了区分其不同,必须调用ferror或feof。

(2)       fgetc();

格式:#include <stdio.h>

int fgetc(FILE *fp);

成功:返回下一个字符;出错:返回EOF;文件尾:EOF;

 

实现:同getc

原因:同getc

补充:同getc

(3)       getchar();

格式:#include <stdio.h>

int getchar(void);

成功:返回下一个字符;出错:返回EOF;文件尾:EOF;

实现:内存 <-读一个字符c- 流(由stdin所指的流,是标准输入文件的逻辑代表),所以getchar=getc(stdin);

原因:同getc

补充:同getc

 

每次输入一行:

(4)       fgets();

格式:#include <stdio.h>

char *fgets(char *restrict buf,  Int n,  FILE *restrict  fp);

成功:返回buf;出错:返回NULL; 文件结尾:NULL;

实现:内存buf <-从fp所指的流中取一行字符- 流(由fp所指)

原因:在标准I/O中用,将流看成文件的逻辑代表,将对进程->文件的操作,现转换为进程->流(也就是相当于文件)的操作。

补充:必须指定用户进程缓冲区的长度n,即buf的大小,此函数从流中一直读到下一个换行符为止,但是不超过n-1个字符,读入的字符被送入用户缓冲区buf中。该缓冲区以null字符结尾。如若该行包括最后换行符的字数大于n-1,则其只返回一个不完整的行,但是缓冲区buf总是以null字符结尾,对此函数的调用会继续读该行。缓冲区buf中的内容为:(字符+换行符)+null。所以字符+换行符<=n-1,因为一定要留一个NULL字符来标识缓冲区的结束;

(5)       gets();

格式:#include <stdio.h>

char *gets(char * buf);

成功:返回buf;出错:返回NULL; 文件结尾:NULL;

实现:内存buf <-从stdin所指的流中取1行字符-标准输入流(由fp=stdin所指)

原因:同上;

补充:不推荐使用,问题是调用者在使用gets时,不能指定缓冲区buf(用户进程)的长度,这样可能造成缓冲区溢出。

 

每次输出一个字符:

(6)       putc();

格式:#include <stdio.h>

int putc(int c ,FILE *fp);

成功:返回c;出错:返回EOF;

实现:内存中整形变量c-写字符C->流(由fp所指)。至于流什么时候将C写入文件中,这个由库函数来实现,不用用户操心;

原因:

补充:

 

(7)       fputc();

格式:#include <stdio.h>

int fputc(int c ,FILE *fp);

成功:返回c;出错:返回EOF;

实现:内存中整形变量c-写字符C->流(由fp所指)。至于流什么时候将C写入文件中,这个由库函数来实现,不用用户操心;

原因:

补充:

 

(8)       putchar();

格式:#include <stdio.h>

int putchar(int c);

成功:返回c;出错:返回EOF;

实现:内存中整形变量c-写字符C->流(由fp=stdout所指)。至于流什么时候将C写入标准输出文件中,这个由库函数来实现,不用用户操心;

原因:

补充:putchar(c)=putc(c,stdout);

 

每次输出一行:

(9)       fputs();

格式:#include <stdio.h>

int fputs(const char *restrict  str, FILE  *restrict  fp);

成功:返回非负值;出错:返回EOF;

实现:内存中字符数组str-写字符数组str->流(由fp所指)。

原因:

补充:将一个以null符终止的字符串(相当于用户空间buf,肯定有null,对应于fgets的buf中一定要有个null来标识缓冲区buf的结束。)写到指定的流,尾端的终止符null不写进流中。注意,这并不一定是每次输出一行,因为它并不要求在null之前一定是换行符,buf中有就有,没有就没有,通常,在空字符之前是一个换行符,但并不要求总是如此。用户空间buf:字符(+换行符)+null;流中的buf:字符+换行符。

 

(10)   puts();

格式:#include <stdio.h>

int puts(const char * str);

成功:返回非负值;出错:返回EOF;

实现:内存中字符数组str-写字符数组str->标准输出流(由fp=stdout所指)。

原因:

补充:将一个以null结尾的字符串写到标准输出上,相当于进程->流->标准输出文件。终止符不写出,但是puts然后又将一个换行符写到标准输出。应当少用,以免需要记住它在最后是否添加了一个换行符。而fgets和fputs在处理换行符,本着实事求是的态度,有就有,没有就没有,不会在用户buf和流缓冲以及文件中自己添加,只是在数据经过流缓冲时,增加或是过滤到null字符。当fgets时会在用户buf中增加一个null以标识用户buf的结束,而fputs时,以null为终止字符,但是尾端的null并不写在流中。

 

二进制I/O:

(11)   fread()

格式:#include <stdio.h>

ssize_t  fread(void *restrict ptr, size_t size, size_t nobj, FILE *restrict  fp);

成功:读到的对象数。

实现:内存始址ptr<-读N个对象- 流(由fp所指)

原因:以上有一次一个字符或是一次一行的方式进行I/O操作,当我们读或写一个结构时,对于一次一个字符的方式,必须循环通过整个结构,每次循环处理一个字节,一次读或写一个字节,这会很烦。而对于一次一行的方式,当每次结构体中有null字符时,fputs就会停止,所以也不能用它实现读结构,同时fgets中包含有null字节或换行符,其也不能正常工作。所以要并实现结构体作为一个整体的读或写。

补充:使用二进制的基本问题是:它只能用于读在同一系统上已写的数据。其原

因是:在结构中,同一成员偏移量可能因为编译器和系统而异,另外,用来存储多字节整数和浮点值的二进制格式在不同的机器体系结构之间也可能不同。

 

 

(12)   fwrite()

格式:#include <stdio.h>

ssize_t  fwrite(const void *restrict ptr, size_t size, size_t nobj, FILE *restrict  fp);

成功:写的对象数。

 

实现:内存始址ptr-写N个对象-> 流(由fp所指)

原因:

补充:

 

格式化输入:文件-流->格式转换->内存变量中

(13)   scanf();

格式:#include <stdio.h>

int scanf(const char *restrict format,…)

成功:指定的输入项数;出错:返回EOF;输入出错或在任意变换前已到达文件结尾:EOF;

实现:标准输入流->格式转换->内存变量中。用于分析输入字符串,并将字符序列转换成指定类型的变量。格式之后的各个参数包含了变量的地址,以用转换结果初始化这些变量。

原因:要在流中做格式转换,再将结果放到内存变量中

补充:

 

(14)   fscanf();

格式:#include <stdio.h>

int fscanf(FILE *restrict fp, const char *restrict format,…)

成功:指定的输入项数;出错:返回EOF;输入出错或在任意变换前已到达文件结尾:EOF;

实现:输入流->格式转换->内存变量中

原因:

补充:

 

(15)   sscanf();

格式:#include <stdio.h>

int sscanf(const char *restrict buf, const char *restrict format,…)

成功:指定的输入项数;出错:返回EOF;输入出错或在任意变换前已到达文件结尾:EOF;

实现:内存buf->格式转换->内存变量中。

原因:

补充:对于scanf(), 从标准输入流中输入;fscanf,从流中输入; sscanf,这个比较特殊,不是从流中输入,而是内存的一个buf相当于string中输入。

 

(16)   vscanf();

格式:#include <stdio.h>

int vscanf(const char *restrict format, va_list  arg);

成功:指定的输入项数;出错:返回EOF;输入出错或在任意变换前已到达文件结尾:EOF;

实现:标准输入流->格式转换->内存变量中。用于分析输入字符串,并将字符序列转换成指定类型的变量。格式之后的各个参数包含了变量的地址,以用转换结果初始化这些变量。同于scanf,只是将原来的可变参数…换成了arg;

原因:要在流中做格式转换,再将结果放到内存变量中

补充:

 

(17)   vfscanf();

格式:#include <stdio.h>

int vfscanf(FILE *restrict fp, const char *restrict format, va_list  arg)

成功:指定的输入项数;出错:返回EOF;输入出错或在任意变换前已到达文件结尾:EOF;

实现:输入流->格式转换->内存变量中, 同于fscanf,只是将原来的可变参数…,换成了arg;

原因:

补充:

 

(18)   vsscanf();

格式:#include <stdio.h>

int vsscanf(const char *restrict buf, const char *restrict format, va_list  arg)

成功:指定的输入项数;出错:返回EOF;输入出错或在任意变换前已到达文件结尾:EOF;

实现:内存buf->格式转换->内存变量中。同于sscanf,只是将原来的可变参数…,换成了arg;

原因:

补充:对于scanf(), 从标准输入流中输入;fscanf,从流中输入; sscanf,这个比较特殊,不是从流中输入,而是内存的一个buf相当于string中输入。

 

 

格式化输出:文件-流<-格式字符串<-内存变量

(19)   printf();

格式:#include <stdio.h>

int  printf(const char *restrict format, …);

成功:返回输出字符数;出错:返回负值;

实现:标准输出流<-格式字符串<-内存变量

原因:要将内存变量的数据做格式变换,再将变换的结果放入流中

补充:

 

(20)   fprintf();

格式:#include <stdio.h>

int  fprintf(FILE *restrict fp,const char *restrict format, …);

成功:返回输出字符数;出错:返回负值;

实现:文件-输出流<-格式字符串<-内存变量

原因:

补充:

 

(21)   sprint();

格式:#include <stdio.h>

int  sprintf(char *restrict buf, const char *restrict format, …);

成功:返回输出字符数;出错:返回负值;

实现:内存字符串buf<-格式字符串<-内存变量,就是将格式化的字符串送入数组buf而不是指定的流中。在数组的尾端自动加一个null字节,但该字节不包括在返回值中。

原因:

补充:

 

(22)   snprintf();

格式:#include <stdio.h>

int  snprintf(char *restrict buf, size_t n , const char *restrict format, …);

成功:返回输出字符数;出错:返回负值;

实现:内存字符串buf<-格式字符串<-内存变量,就是将格式化的字符串送入数组buf而不是指定的流中。在数组的尾端自动加一个null字节,但该字节不包括在返回值中。只能输入n-1个字符,超过的任何字条都会被丢弃。

原因:

补充:

 

(23)   vprintf();

格式:#include <stdarg.h>

#include <stdio.h>

int  vprintf(const char *restrict format, va_list  arg);

成功:返回输出字符数;出错:返回负值;

实现:标准输出流<-格式字符串<-内存变量,同于printf,只是将原来的可变参数…换成了arg;

原因:要将内存变量的数据做格式变换,再将变换的结果放入流中

补充:

 

(24)   vfprintf();

格式:#include <stdarg.h>

#include <stdio.h>

int  vfprintf(FILE *restrict fp,const char *restrict format, va_list  arg);

成功:返回输出字符数;出错:返回负值;

实现:输出流<-格式字符串<-内存变量,同于fprintf,只是将原来的可变参数…换成了arg;

原因:要将内存变量的数据做格式变换,再将变换的结果放入流中

补充:

(25)   vsprintf();

格式:#include <stdarg.h>

#include <stdio.h>

int  vsprintf(char *restrict buf, const char *restrict format, va_list  arg);

成功:返回输出字符数;出错:返回负值;

实现:内存数组buf<-格式字符串<-内存变量,同于sprintf,只是将原来的可变参数…换成了arg; 就是将格式化的字符串送入数组buf而不是指定的流中。在数组的尾端自动加一个null字节,但该字节不包括在返回值中。

原因:要将内存变量的数据做格式变换,再将变换的结果放入流中

补充:

 

(26)   vsnprintf();

格式:#include <stdio.h>

int  vsnprintf(char *restrict buf, size_t n , const char *restrict format, va_list arg);

成功:返回输出字符数;出错:返回负值;

实现:内存字符串buf<-格式字符串<-内存变量, 同于snprintf,只是将原来的可变参数…换成了arg; 就是将格式化的字符串送入数组buf而不是指定的流中。在数组的尾端自动加一个null字节,但该字节不包括在返回值中。只能输入n-1个字符,超过的任何字条都会被丢弃。

原因:

补充:

 

  1. 高级I/O:(文件(fd), 内存buf )

(1)       readv()

格式:#include <sys/uio.h>

ssize_t  readv(int filedes, const  struct iovec *iov, int iovcnt);

成功:返回已读的字节数;出错:返回-1;

实现:文件(fd)->内存向量中

原因:在一次函数调用中读、写多个非连续缓冲区,但是这些缓冲区已经用iovec表示好了。减少了系统调用的次数。

补充:

(2)       writev()

格式:#include <sys/uio.h>

ssize_t  writev(int filedes, const  struct iovec *iov, int iovcnt);

成功:返回已读的字节数;出错:返回-1;

实现:文件(fd)<-内存向量

原因:在一次函数调用中读、写多个非连续缓冲区,但是这些缓冲区已经用iovec表示好了。减少了系统调用的次数。

补充:

 

(3)       readn()

格式:#include <sys/uio.h>

ssize_t  readn(int filedes, void *bug, size_t  nbytes);

成功:返回已读的字节数;出错:返回-1;

实现:文件(fd)->内存buf中

原因:管道、FIFO以及某些设备,特别是终端、网络和STREAMS设备有下列两种性质:一是,一次read操作所返回的数据可能少于所要求的数据,即使还没达到文件尾端也可能是这样的。这不是一个错误,应当继续读该设备。二是,一次write操作所返回的值也可能少于所指定输出的字节数,这可能是由若干因素造成的。这些也不是错误,也应当继续写余下的数据至该设备。通常只对非阻塞描述符,或捕捉到一个信号时,才发生这种write的中途返回。但是在读写磁盘时,很少遇到这样的情况。所以这个函数其实是按需要多次调用read 和write直至读、写了N个字节数据,即我们称之为:直到集齐了再返回。

补充:

 

(4)       written()

格式:#include <sys/uio.h>

ssize_t  writen(int filedes, void *bug, size_t  nbytes);

成功:返回已读的字节数;出错:返回-1;

实现:文件(fd)<-内存buf中

原因:管道、FIFO以及某些设备,特别是终端、网络和STREAMS设备有下列两种性质:一是,一次read操作所返回的数据可能少于所要求的数据,即使还没达到文件尾端也可能是这样的。这不是一个错误,应当继续读该设备。二是,一次write操作所返回的值也可能少于所指定输出的字节数,这可能是由若干因素造成的。这些也不是错误,也应当继续写余下的数据至该设备。通常只对非阻塞描述符,或捕捉到一个信号时,才发生这种write的中途返回。但是在读写磁盘时,很少遇到这样的情况。所以这个函数其实是按需要多次调用read 和write直至读、写了N个字节数据,即我们称之为:直到集齐了再返回。

补充:

  1. IPC中:

消息队列中:

(1)       msgrcv()

格式:#include <sys/msg.h>

ssize_t  msgrcv(int  msqid, void *ptr, size_t nbytes, long type, int flag);

成功:返回消息的数据部分长度;出错:-1;

实现:消息队列->内存消息结构体(由ptr指向)

原因:

补充:nbytes说明数据缓冲区的长度。用来构造mymesg。若返回的消息大于nbytes,而且在flag中设置了MSG_NOERROR,则该消息被截短。如果没有设置这一标志,而消息又太长,则出错返回E2BIG(消息仍留在队列中。参数type我们可以指定想要哪一种消息。可以指定flag值为IPC_NOWAIT,使操作不阻塞。这使得如果没有所指定类型的消息,则msgrcv返回-1,errno设置为ENOMSG。

 

(2)       msgsnd()

格式:#include <sys/msg.h>

int  msgsnd(int  msqid, const void *ptr, size_t nbytes, long type, int flag);

成功:返回0;出错:-1;

实现:消息队列<-内存消息结构体(由ptr指向)

原因:

补充:每个消息都由三部分组成,它们是:正长整型类型字段、实际数据字节(这两个对就myseq结构体)、非负长度(nbytes)。消息总是放在队列尾端。ptr参数指向一个长整型数,它包含了正的整型消息类型,在其后紧跟着消息数据。可以定义如下结构:struct myseq{ long mtype; char mtex[512];}  于是ptr就是一个指向mymesg结构的指针。接收者可以用消息类型以非先进先出的次序取消息。

 

 

 

 

SOCKET中:

(1)       revc()

格式:#include <sys/socket.h>

ssize_t  recv(int sockfd, void *buf, size_t  nbytes, int flags);

成功:以字节计数的消息长度;出错:-1;无可用消息或对方已经按序结束:0;

实现:网络sockfd-取消息msg->内存buf中。

原因:

补充:

(2)       recvfrom()

格式:#include <sys/socket.h>

ssize_t  recvfrom( int sockfd, void *restrict  buf, size_t  len, int flags, struct sockaddr *restrict addr, socklen_t *restrict  addrlen);

成功:以字节计数的消息长度;出错:-1;无可用消息或对方已经按序结束:0;

实现:网络sockfd-取消息msg->内存buf中。

原因:

补充:如果addr非空,它将包含数据发送者的套接字端点地址,当调用recvfrom时,需要设置addrlen参数指向一个包含addr所指的套接字缓冲区字节大小的整数。返回时,该整数设为该地址的实际字节大小。因为可以获得发送者的地址,recvfrom通常用于无连接套接字。

 

(3)       recvmsg()

格式:#include <sys/socket.h>

ssize_t  recvmsg( int sockfd, struct msghdr *msg, int flags);

成功:以字节计数的消息长度;出错:-1;无可用消息或对方已经按序结束:0;

实现:网络sockfd-取消息msg->内存buf中。

原因:

补充:结构msghdr被recvmsg用于指定接收数据的输入缓冲区。

 

(4)       send()

格式:#include <sys/socket.h>

ssize_t  send(int sockfd, const  void *buf, size_t  nbytes, int flags);

成功:返回发送的字节数;出错:-1;

实现:网络sockfd<-取消息msg-内存buf中。

原因:

补充:如果send成功,并不必然表示连接另一端的进程接收数据。所保证的仅是当send成功返回时,数据已经无错误的发送到网络上。

(5)       sendto()

格式:#include <sys/socket.h>

ssize_t  sendto( int sockfd, const void *restrict  buf, size_t nbytes, int flags,  const struct sockaddr *dest addr, socklen_t * addrlen);

成功:返回发送的字节数;出错:-1;

实现:网络sockfd<-取消息msg-内存buf中。

原因:

补充:适用于无连接的套接字,不能使用send,除非调用connect时预先设定了目标地址,或者采用了sendto来提供另外一种报文发送方式。

 

(6)       sendmsg()

格式:#include <sys/socket.h>

ssize_t  sendmsg( int sockfd,  const struct msghdr *msg, int flags);

成功:返回发送的字节数;出错:-1;

实现:网络sockfd<-取消息msg-内存buf中。

原因:

补充:可以调用带有msghdr结构的sendmsg来指定多重缓冲区传输数据,和writev很像。

 

传送文件描述符(略,自行实现)

(1)       recv_fd()

(2)       send_fd()

(3)       send_err()

 

总算整差不多了,虽然花了点时间,但是希望能对大家有帮助,最后谢谢阅读!

转载 http://blog.chinaunix.net/uid-11861796-id-2813415.html

文章分类 后端

发表评论

电子邮件地址不会被公开。

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

在线交流

数百位业内高手和同行在等你交流
Reboot运维开发分享