6.1 pipe函数
pipe函数可以用来创建一个单向管道,来实现进程间的通信。
arduino
#include <unistd.h>
int pipe(int fd[2])
pipe创建的管道是单向且阻塞的 。该函数成功时返回0,并将一对打开的文件描述符填入其参数指向的数组。如果失败则返回-1,并设置errno。
如果管道的写入端描述符的引用计数为0时,那么针对该管道的读取端描述符返回0,即读取到了文件结束标记;如果管道的读取端描述符的引用计数为0,那针对该管道的写入端描述符的write操作将失败,并引发SIGPIPE信号。
管道是字节流,默认大小为65536,且可以通过fcntl函数来修改管道容量。
而socketpair函数可以很方便的创建双向管道。
arduino
#include <sys/types.h>
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int fd[2]);
6.2 dup函数和dup2函数
该函数可以标准输入重定向到一个文件,或者把标准输出重定向到一个网络连接。
arduino
#include <unistd.h>
int dup(int file_descriptor)
int dup2(int file_descriptor_one, int file_descriptor_tow)
dup函数重新创建一个新的文件描述符,该新文件描述符和原有文件描述符file_descriptor指向相同的文件、管道或者网络连接。但是它们返回的文件描述符数值不太一样。
服务器发送数据代码:
ini
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
struct sockaddr_in address;
bzero( &address, sizeof( address ) );
address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &address.sin_addr );
address.sin_port = htons( port );
int sock = socket( PF_INET, SOCK_STREAM, 0 );
assert( sock >= 0 );
int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );
ret = listen( sock, 5 );
assert( ret != -1 );
struct sockaddr_in client;
socklen_t client_addrlength = sizeof( client );
int connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
if ( connfd < 0 )
{
printf( "errno is: %d\n", errno );
}
else
{
/*关闭标准输入文件符*/
close( STDOUT_FILENO );
dup( connfd );
printf( "abcd\n" );
close( connfd );
}
close( sock );
return 0;
}
客户端接收数据代码:
arduino
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
struct sockaddr_in server_address;
bzero( &server_address, sizeof( server_address ) );
server_address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &server_address.sin_addr );
server_address.sin_port = htons( port );
int sockfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( sockfd >= 0 );
if ( connect( sockfd, ( struct sockaddr* )&server_address, sizeof( server_address ) ) < 0 )
{
printf( "connection failed\n" );
}
else
{
/*由客户端来接收服务端重定向到网络socket的数据*/
char buffer[1024];
memset(buffer, '\0', 1024);
int ret = recv(sockfd, buffer, 1023, 0);
printf("the dup data: %s", buffer);
}
close( sockfd );
return 0;
}
客户端接收数据实验截图:
6.3 readv函数和writev函数
readv函数将数据从文件描述符读到分散的内存块中,即分散读;writev函数则将多块分散的内存数据一并写入文件描述符中,即集中写。
arduino
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec* vector, int count)
ssize_t writev(int fd, const struct iovec* vector, int count)
- fd参数是被操作的目标文件描述符;
- vector是iovec结构数据;
readv和writev成功时返回读取/写入fd的字节数,失败则返回-1。
ini
struct iovec iv[2];
iv[0].iov_base = header_buf;
iv[0].iov_len = strlen(header_buf);
iv[1].iov_base = file_buf;
iv[1].iov_len = file_stat.st_size;
ret = writev(connfd, iv, 2);
6.4 sendfile函数
sendfile函数直接在两个文件描述符之间传递数据(完全在内核中操作),从而避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,这被称为零拷贝。
arduino
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count)
- in_fd参数是待读出内容的文件描述符;
- out_fd参数是代写入内容的文件描述符;
- offset参数指定从读入文件流的哪个位置开始读,如果为空,则使用读入文件流默认的起始位置;
- count参数指定在文件描述符in_fd和out_fd之间传输的字节数;
sendfile成功时返回传输的字节数,失败则返回-1并设置errno。
in_fd必须是一个支持类似mmap函数的文件描述符,即它必须指向真实的文件,不能是socket和管道;而out_fd则必须是一个socket
服务端发送文件代码:
c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/sendfile.h>
int main( int argc, char* argv[] )
{
if( argc <= 3 )
{
printf( "usage: %s ip_address port_number filename\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
const char* file_name = argv[3];
int filefd = open( file_name, O_RDONLY );
assert( filefd > 0 );
/*struct stat用来描述一个Linux系统文件系统中的文件属性的结构*/
struct stat stat_buf;
/*fstat获取文件的信息*/
fstat( filefd, &stat_buf );
struct sockaddr_in address;
bzero( &address, sizeof( address ) );
address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &address.sin_addr );
address.sin_port = htons( port );
int sock = socket( PF_INET, SOCK_STREAM, 0 );
assert( sock >= 0 );
int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );
ret = listen( sock, 5 );
assert( ret != -1 );
struct sockaddr_in client;
socklen_t client_addrlength = sizeof( client );
int connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
if ( connfd < 0 )
{
printf( "errno is: %d\n", errno );
}
else
{
/*直接从内核缓冲区拷贝数据*/
sendfile( connfd, filefd, NULL, stat_buf.st_size );
close( connfd );
}
close( sock );
return 0;
}
以下是stat结构体的信息:
arduino
#include <sys/stat.h>
struct stat {
dev_t st_dev; // 文件所在设备号
ino_t st_ino; // I节点号
mode_t st_mode; // 文件类型和权限
nlink_t st_nlink; // 硬链接数量
uid_t st_uid; // 所有者ID
gid_t st_gid; // 所有者的组ID
dev_t st_rdev; // 设备号(仅特殊文件)
off_t st_size; // 总字节大小
blksize_t st_blksize; // 文件系统I/O的块大小
blkcnt_t st_blocks; // 分配的512B块数量
struct timespec st_atim; // 文件数据的最后访问时间
struct timespec st_mtim; // 文件数据的最后修改时间
struct timespec st_ctim; // i节点状态的最后更改时间
#define st_atime st_atim.tv_sec
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};
服务端实验截图:
客户端实验结果:
6.5 mmap函数和munmap函数
mmap函数用于申请一段内存空间。我们可以将这段内存作为进程间通信的共享内存,也可以将文件直接映射到其中。munmap函数则释放由mmap创建的这段内存空间。
[linux共享内存] zhuanlan.zhihu.com/p/633054182
arduino
#include <sys/mman.h>
void mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset)
int munmap(void* start, size_t length)
-
start参数允许用户使用某个特定的地址作为这段内存的起始地址,默认为NULL,则由系统自动分配;
-
length参数指定内存段的长度;
-
prot参数设置内存段的访问权限;
- PROT_READ,内存段可读
- PROT_WRITE,内存段可写
- PROT_EXEC,内存段可执行
- PROT_NONE,内存段不能被访问
-
flags参数控制内存段内容被修改后程序的行为
常用值 含义 MAP_SHARED 在进程间共享内存。对该内存段的修改将反映到被映射的文件中。它提供了进程间共享内存的POSIX方法 MAP_PRIVATE 内存段为调用进程所私有 MAP_ANONYMOUS 这段内存不是从文件映射而来的,其内容被初始化全为0 MAP_FIXED 内存段必须位于start参数指定的地址处,start必须是内存页面大小(4096字节)的整数倍 MAP_HUGETLB 按照大内存页面来分配空间 -
fd参数是被映射文件对应的文件描述符,一般通过open获得
-
offset参数设置从文件的何处开始映射
6.6 splice函数
splice函数用于在两个文件描述符之间移动数据,也是零拷贝操作。
arduino
#include <fcntl.h>
ssize_t splice(int fd_in, loff_t* off_in, int fd_out, loff_t* off_out, size_t len, unsigned int flags)
-
fd_in参数是待输入数据的文件描述符,如果fd_in是一个管道文件描述符,那么off_in参数必须被设置为NULL;
-
len参数指定移动数据的长度;
-
flags参数则控制数据如何移动,它可以被设置为下标中某些值得按位或
常用值 含义 SPLICE_F_MOVE SPLICE_F_NONBLOCK 非阻塞的splice操作,实际效果收到文件描述符本身的阻塞状态的影响 SPLICE_F_MORE SPLICE_F_GIFT 使用splice函数时,fd_in和fd_out必须至少有一个是管道文件描述符
回射服务器代码:
ini
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
struct sockaddr_in address;
bzero( &address, sizeof( address ) );
address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &address.sin_addr );
address.sin_port = htons( port );
int sock = socket( PF_INET, SOCK_STREAM, 0 );
assert( sock >= 0 );
int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
assert( ret != -1 );
ret = listen( sock, 5 );
assert( ret != -1 );
struct sockaddr_in client;
socklen_t client_addrlength = sizeof( client );
int connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
if ( connfd < 0 )
{
printf( "errno is: %d\n", errno );
}
else
{
int pipefd[2];
assert( ret != -1 );
ret = pipe( pipefd );
ret = splice( connfd, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
assert( ret != -1 );
ret = splice( pipefd[0], NULL, connfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
assert( ret != -1 );
close( connfd );
}
close( sock );
return 0;
}
客户端发送和接收数据:
c
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main( int argc, char* argv[] )
{
if( argc <= 2 )
{
printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
return 1;
}
const char* ip = argv[1];
int port = atoi( argv[2] );
struct sockaddr_in server_address;
bzero( &server_address, sizeof( server_address ) );
server_address.sin_family = AF_INET;
inet_pton( AF_INET, ip, &server_address.sin_addr );
server_address.sin_port = htons( port );
int sockfd = socket( PF_INET, SOCK_STREAM, 0 );
assert( sockfd >= 0 );
if ( connect( sockfd, ( struct sockaddr* )&server_address, sizeof( server_address ) ) < 0 )
{
printf( "connection failed\n" );
}
else
{
char buf[1024];
memset(buf, '\0', 1024);
const char* normal_data = "hello, world!";
printf("send %zu bytes of data %s \n", strlen(normal_data), normal_data);
send( sockfd, normal_data, strlen( normal_data ), 0 );
int ret = recv(sockfd, buf, 1023, 0);
printf("got %d bytes of data %s \n", ret, buf);
close(sockfd);
}
close( sockfd );
return 0;
}
服务端实验截图:
客户端实验截图:
6.7 tee函数
tee函数在两个管道文件描述符之间复制数据,也是零拷贝操作。并且它不消耗数据,因此源文件描述符上的数据仍然可以用于后续的读操作。
ini
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
int main( int argc, char* argv[] )
{
if ( argc != 2 )
{
printf( "usage: %s <file>\n", argv[0] );
return 1;
}
int filefd = open( argv[1], O_CREAT | O_WRONLY | O_TRUNC, 0666 );
assert( filefd > 0 );
int pipefd_stdout[2];
int ret = pipe( pipefd_stdout );
assert( ret != -1 );
int pipefd_file[2];
ret = pipe( pipefd_file );
assert( ret != -1 );
/*将标准输入内容输入管道pipefd_stdout*/
ret = splice( STDIN_FILENO, NULL, pipefd_stdout[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
assert( ret != -1 );
/*将pipefd_stdout的内容拷贝到pipefd_file中*/
ret = tee( pipefd_stdout[0], pipefd_file[1], 32768, SPLICE_F_NONBLOCK );
assert( ret != -1 );
/*将pipefd_file中的内容输出到文件描述符filefd中*/
ret = splice( pipefd_file[0], NULL, filefd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
assert( ret != -1 );
/*将pipefd_stdout中的内容输出到标准输出*/
ret = splice( pipefd_stdout[0], NULL, STDOUT_FILENO, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE );
assert( ret != -1 );
close( filefd );
close( pipefd_stdout[0] );
close( pipefd_stdout[1] );
close( pipefd_file[0] );
close( pipefd_file[1] );
return 0;
}
实验结果截图:
6.8 fcntl函数
该函数提供了对文件描述符的各种控制操作。
arduino
#include <fcntl.h>
int fcntl(int fd, int cmd, ...);
- fd参数为被操作的文件描述符;
- cmd参数指定执行何种类型的操作;
- 根据操作类型不同,函数可能需要第三个可选参数arg
在网络编程中,fcntl函数通常用来将一个文件描述符设置为非阻塞的,代码清单如下:
perl
int setnonblocking(int fd) {
int old_option = fcntl(fd, F_GETFL); /*获取文件描述符旧的状态标志*/
int new_option = old_option | O_NONBLOCK; /*设置非阻塞标志*/
fcntl(fd, F_SETFL, new_option);
return old_option; /*返回文件描述符旧的状态标志*/
}
7. Linux服务器程序规范
7.1 Linux系统日志
7.2 用户信息
7.2.1 UID、EUID、GID和EGID
某些进程在运行的时候,它可能需要一些特权操作。比如大部分服务器必须以root身份启动,但不能以root身份运行。
arduino
#include <sys/types.h>
#include <unistd.h>
uid_t getuid() /*获取真实用户ID*/
uid_t geteuid() /*获取有效用户ID*/
gid_t getgid() /*获取真实组ID*/
gid_t getegid() /*获取有效组ID*/
int setuid(uid_t uid) /*设置真实用户ID*/
int seteuid(uid_t uid) /*设置有效用户ID*/
int setgid(gid_t gid) /*设置真实组ID*/
int setegid(gid_t gid) /*设置有效组ID*/
一个进程拥有两个用户ID:UID和EUID 。UID就是进程的启动用户,而EUID是进程的运行权限用户。它使得运行程序的用户拥有该程序的有效用户的权限。
下列程序可以测试进程的UID和和EUID的区别。
arduino
#include <unistd.h>
#include <stdio.h>
int main()
{
uid_t uid = getuid();
uid_t euid = geteuid();
printf( "userid is %d, effective userid is: %d\n", uid, euid );
return 0;
}
实验结果:
8.1 服务器模型------CS模型
绑定完 IP 地址和端口后,就可以调用 listen()
函数进行监听,此时对应 TCP 状态图中的 listen
,如果我们要判定服务器中一个网络程序有没有启动,可以通过 netstat
命令查看对应的端口号是否有被监听。
在 TCP 连接的过程中,服务器的内核实际上为每个 Socket 维护了两个队列:
- 一个是「还没完全建立」连接的队列,称为 TCP 半连接队列 ,这个队列都是没有完成三次握手的连接,此时服务端处于
syn_rcvd
的状态; - 一个是「已经建立」连接的队列,称为 TCP 全连接队列 ,这个队列都是完成了三次握手的连接,此时服务端处于
established
状态;
8.2 服务器编程框架
模块 | 功能描述 |
---|---|
I/O处理单元 | 处理用户连接,读写网络数据 |
逻辑单元 | 业务进程或线程 |
网络存储单元 | 本地数据库、文件或缓存 |
请求队列 | 各模块之间的通信方式 |
8.3 I/O模型
阻塞与非阻塞 :这两个概念能应用于所有文件描述符,而不仅仅是socket。我们称阻塞的文件描述符为阻塞I/O,非阻塞的文件描述符为非阻塞I/O。针对阻塞I/O执行的系统调用可能因为无法立即完成而被操作系统挂起,直到等待的时间发生为止。比如accept、connect、send、recv都有可能发生阻塞。 针对非阻塞I/O的执行的系统调用总是立即返回,而不管事件是否已经发生。如果事件没有立即发生,这些系统调用就返回-1,和出错的情况一样。此时我们必须通过errno来区分这两种情况。
显然,我们需要在事件发生的情况下操作非阻塞I/O才能提高效率。因此非阻塞式I/O通常要与其他的I/O通知机制一起使用,比如I/O复用和SIGIO信号。
通俗来说,非阻塞式I/O会立即返回结果。比如非阻塞式read,如果有数据就返回数据,没有数据就返回一个错误。此时我们就需要轮询来获取数据。这样效率很低,因为依然占用了CPU时间来等待数据准备好。
于是,我们需要引入I/O多路复用 ,这里的复用是指多个I/O事件复用一个线程。如果每个socket都创建一个线程来处理的话,线程数量过度,CPU大部分时间都在切换上下文,利用率低。而通过多路复用,即一个线程来检查多个文件描述符。比如select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪则返回,否则阻塞直到超时。得到就绪状态之后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如线程池)。
需要注意的是:select 实现多路复用的方式是,将已连接的 Socket 都放到一个 文件描述符集合 ,然后调用 select 函数将文件描述符集合 拷贝到内核里,让内核来检查是否有网络事件产生,检查的方式很粗暴,就是通过 遍历文件描述符集合的方式,当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把整个文件描述符集合 拷贝回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 Socket,然后再对其处理。
所以,对于 select 这种方式,需要进行 2 次「遍历」文件描述符集合 ,一次是在内核态里,一个次是在用户态里 ,而且还会发生 2 次「拷贝」文件描述符集合,先从用户空间传入内核空间,由内核修改后,再传出到用户空间中。
I/O复用函数 | 区别 |
---|---|
select | 使用固定长度的BitsMap,表示文件描述符集合 |
poll | 使用动态数组,以链表形式组织 |
epoll | 使用红黑树来储存跟踪文件描述符 |
理论上来说,阻塞I/O、I/O复用和信号驱动I/O都是同步I/O模型
同步I/O模型与异步I/O模型
同步I/O模型 | 异步I/O模型 |
---|---|
同步I/O模型要求用户代码自行执行I/O操作(将数据从内核缓冲区读入用户缓冲区,或将数据从用户缓冲区读入内核缓冲区) | 异步I/O机制则由内核来执行I/O操作(数据在内核缓冲区和用户缓冲区之间的移动式内核在"后台"完成的) |
同步I/O向应用程序通知的是I/O就绪事件 | 异步I/O向应用程序通知的是I/O完成事件 |
8.4 两种高效的事件处理模型
8.5.1 半同步/半异步模式
此处的同步/异步和I/O模型中的同步/异步是完全不同的概念。在I/O模型中,"同步"和"异步"的区别是内核向应用进程通知的是何种I/O事件(是就绪事件还是完成事件),以及该由谁来完成I/O读写(是应用程序和内核)
在并发模式中,"同步"指的是程序完全按照代码序列的顺序执行;"异步"指的是程序的执行需要由系统事件来驱动。常见的系统事件包括中断、信号等。
这里的同步/异步其实和阻塞/非阻塞是相对应的。比如对于read系统调用,同步方式运行必须等待数据准备完成;而异步方式运行则不关心调用是否完成直接执行下一行代码。那么异步调用下调用方怎么知道调用函数是否执行完成呢?分为两种情况:
- 调用方不关心执行结果
- 调用方需要知道执行结果
第一种情况比较简单,第二种情况则需要一种通知机制,也就是说当任务执行完成后发送信号通知调用方任务完成。