网络编程笔记
网络编程笔记: Note of Network-Programming
open()
打开文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *path, int flag);
/**
* path: 文件名字符串地址
* flag: 文件打开模式
*/
成功则返回文件描述符,失败则返回-1
| 打开模式 | 含义 |
|---|---|
| O_CREAT | 必要时创建文件 |
| O_TRUNC | 删除全部现有数据 |
| O_APPEND | 维持现有数据,保存到其后面 |
| O_RDONLY | 只读打开 |
| O_WRONLY | 只写打开 |
| O_RDWR | 读写打开 |
close()
关闭文件
#include <unistd.h>
int close(int fd);
/**
* fd: 需要关闭的文件或套接字的文件描述符
*/
成功时返回0,失败则返回-1
write()
将数据写入文件
#include <unistd.h>
ssize_t write(inf fd, const void* buf, size_t nbytes);
/**
* fd: 需要关闭的文件或套接字的文件描述符
* buf: 保存要传输数据的缓冲地址值
* nbytes: 要传输数据的字符数
*/
成功时返回写入的字节数,失败则返回-1
read()
读取文件中的数据
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
/**
* fd: 需要关闭的文件或套接字的文件描述符
* buf: 保存要传输数据的缓冲地址值
* nbytes: 要接收数据的最大字节数
*/
成功时返回接收的字节数(但遇到文件结尾则返回0),失败则返回-1
socket()
创建套接字
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
/**
* domain: 套接字中使用的协议族
* type: 套接字数据传输类型信息(SOCK_STREAM, SOCK_DGRAM)
* protocol: 计算机间通信中使用的协议信息(决定最终采用的协议)
*/
成功则返回文件描述符,失败则返回-1
协议族
| domain | 协议族 |
|---|---|
| PF_INET | IPv4协议族 |
| PF_INET6 | IPv6协议族 |
| PF_LOCAL | 本地UNIX协议族 |
| PF_PACKET | 底层套接字协议族 |
| PF_IPX | IPX Novell协议族 |
地址信息表示
struct sockaddr_in
{
sa_family_t sin_family; // 地址族
uint16_t sin_port; // 16位TCP/UDP端口号
struct in_addr sin_addr; // 32位IP地址
char sin_zero[8] // 不使用
};
struct in_addr
{
in_addr_t s_addr; // 32位IPv4地址
};
| 地址族 | 含义 |
|---|---|
| AF_INET | IPv4网络协议使用的地址族 |
| AF_INET6 | IPv6网络协议使用的地址族 |
| AF_LOCAL | 本地通信中采用的UNIX协议的地址族 |
struct sockaddr
{
sa_family_t sin_family; // 地址族
char sa_data; // 地址信息
};
其中,成员sa_data保存的地址信息中需包含IP地址和端口号,剩余部分填充0,这也是bind函数要求的。
关于sin_family
sockaddr_in是保存IPv4地址信息的结构体。那为何还需要通过sin_family单独制定地址族信息呢?这与之前讲过的sockaddr有关。结构体sockaddr并非只为IPv4设计,为了与sockaddr保持一致,sockaddr_in要求在sin_family中指定地址族信息。
字节序转换
在实际代码编写中,字节序转换过程是自动的。除了向sockaddr_in结构体变量填充数据外,其他情况均无需考虑字节序问题。
inet_addr
#include <arpa/inet.h>
in_addr_t inet_addr(const char* string);
成功则返回32位大端序整数值,失败则返回INADDR_NONE.
inet_aton
#include <arpa/inet.h>
int inet_aton(const char *string, struct in_addr *addr);
成功则返回1(true),失败则返回0(false)
inet_ntoa
#include <arpa/inet.h>
char * inet_ntoa(struct in_addr addr);
成功则返回转换的字符串地址,失败则返回-1
INADDR_ANY
每次创建服务器端套接字都要输入IP地址会有些繁琐,此时我们可以利用常数INADDR_ANY分配服务器端的IP地址。此法,可自动获取运行服务器的计算机IP地址,不必亲自输入。
TCP服务端
进入等待连接请求状态
#include <sys/socket.h>
int listen(int sock, int backlog);
/**
* sock: 希望进入等待连接请求状态的套接字文件描述符
* backlog: 连接请求等待队列的长度
*/
成功则返回0,失败则返回-1
受理客户端连接请求
#include <sys/socket.h>
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
/**
* sock: 服务器套接字的文件描述符
* addr: 保存发起连接请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量参数填充客户端地址信息
* addrlen: 第二个参数addr结构体的长度
*/
成功则返回创建的套接字文件描述符,失败则返回-1
TCP客户端
请求连接
#include <sys.socket.h>
int connect(int sock, struct sockaddr *addr, socklen_t addrlen);
/**
* sock: 服务器套接字的文件描述符
* addr: 保存目标服务器端地址信息的变量地址值
* addrlen: 第二个参数servaddr结构体的长度
*/
成功则返回0,失败则返回-1
基于TCP的服务端/客户端函数调用关系
该图总体流程如下:服务器端创建套接字后调用bind、listen函数进入等待状态,客户端通过调用connect函数发起连接请求。需注意的是,客户端只能等到服务器端调用listen函数后才能调用connect函数。同时要清楚,客户端调用connect函数前,服务器端有可能率先调用accept函数。当然,此时服务器端在调用accept函数时进入阻塞(blocking)状态,知道客户端调用connect为止。
基于UDP的数据I/O函数
#include <sys/socket.h>
ssize_t sendto(int sock, void *buff, size_t nbytes, int flags,
struct sockaddr *to, socklen_t addrlen);
ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags,
struct sockaddr *to, socklen_t addrlen);
/**
* `sock`: 用于传输数据的UDP套接字文件描述符
* `buff`: 保存待传输数据的缓冲地址值
* `nbytes`: 待传输的数据长度,亿字节为单位
* `flags`: 可选项参数,若没有则传递0
* `to`: 存有目标地址信息的sockaddr结构体的地址
* `addrlen`: 传递给参数`to`的地址值结构体变量长度
*/
成功时返回传输的字节数,失败则返回-1
半关闭
#include <sys.socket.h>
int shutdown(int sock, int howto);
/**
* sock: 虚断开的套接字文件描述符
* howto: 传递方式信息
*/
成功时返回0,失败则返回-1
howto:
- SHUT_RD: 断开输入流
- SHUT_WR: 断开输出流
- SHUT_RDWR: 同时断开I/O流
Get host by name
#include <netdb.h>
struct hostent *gethostbyname(const char *hostname);
成功则返回hostent结构体地址值,失败则返回NULL
struct hostent
{
char *h_name; // office name
char **h_aliases; // alias list
int h_addrtype; // host address type
int h_length; // address length
ubt **h_addr_list; // address list
}
Get host by address
#include <netdb.h>
struct hostent *gethostaddr(const char *addr, socklen_t len, int family);
/**
* addr: 含IP地址信息的`in_addr`结构体指针。为了同时传递IPv4地址外的其他信息,该变量的类型声明为char指针
* len: 向第一个参数传递地址信息的字节数,IPv4时为4,IPv6时为6
* family: 传递地址族信息,IPv4时为AF_INET,IPv4时为AF_INET6
*/
成功则返回hostent结构体地址值,失败则返回NULL
套接字的多种可选项
套接字的多种可选项
套接字可选项分为IPPROTO_IP、IPPROTO_TCP、SOL_SOCKET三层,各层的含义为:
IPPROTO_IP:IP 协议相关事项;IPPROTO_TCP:TCP 协议相关事项;SOL_SOCKET:套接字相关的通用可选项。
getsockopt & setsockopt
#include <sys/socket.h>
int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
/**
* sock: 要查看的套接字文件描述符
* level: 要查看的可选项的协议层
* optname: 要查看的可选项名
* optval: 保存查看结果的缓冲地址值
* optlen: 第四个参数`optval`传递的缓冲区大小
*/
int setsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
/**
* sock: 用于更改可选项的套接字文件描述符
* level: 要更改的可选项的协议层
* optname: 要更改的可选项名
* optval: 保存要更改的选项信息的缓冲地址值
* optlen: 第四个参数`optval`传递的缓冲区大小
*/
成功时返回0,失败则返回-1
创建子进程
#include <unistd.h>
pid_t fork(void);
创建成功:
- 父进程返回子进程
pid - 子进程返回0
失败则返回
-1
销毁僵尸进程
wait()
#include <sys/wait.h>
pid_t wait(int *statloc);
成功则返回子进程pid,失败则返回-1
WIFEXITED子进程正常终止时返回trueWEXITSTATUS返回子进程的返回值
waitpid()
wait函数会引起程序阻塞,还可以考虑调用waitpid函数,可以防止阻塞
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statloc, int options);
/**
* pid: 等待终止的目标子进程`pid`,若传递`-1`,则与`wait()`相同
* statloc: 同`wait()`的`statloc`
* options: 传递`<sys/wait.h>`中声明的常量`WNOHANG`,即使没有终止的子进程也不会进入阻塞态,而是返回0
*/
成功则返回子进程pid(或0),失败则返回-1
信号处理
signal()
#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
为了在产生信号时调用,返回之前注册的函数指针
返回类型为函数指针
上述函数中,第一个参数为特殊情况信息,第二个参数为特殊情况下将要调用的函数指针。
特殊情况:
SIGNALRM: 已到通过调用alarm函数注册的时间SIGINT: 输入CTRL+CSIGCHLD: 子进程终止
alarm()
#include <unistd.h>
unsigned int alarm(unsigned int second);
返回0或以秒为单位的距SIGALRM信号发生所剩时间
sigaction()
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
/**
* `signo`: 与`signal`函数相同
* `act`: 对应于第一个参数的信号处理函数信息
* `oldact`: 通过此参数获取之前注册的信号处理函数指针,若不需要则传递0
*/
创建成功返回0,失败则返回-1
struct sigaction
struct sigaction
{
void (*sa_handler)(int); // 信号处理函数的指针
// 现在我们主要是防止僵尸进程,以下两个成员变量初始化为0即可,可暂时不用管
sigset_t sa_mask;
int sa_flags;
};
管道实现进程间通信
#include <unistd.h>
int pipe(int filedes[2]);
/**
* filedes[0]: 管道接受数据时使用的文件描述符,管道出口
* filedes[1]: 管道传输数据时使用的文件描述符,管道入口
*/
成功返回0,失败返回-1
消息队列
消息队列
消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。 每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。
Linux 用宏 MSGMAX 和 MSGMNB 来限制一条消息的最大长度和一个队列的最大长度。
在 Linux 中使用消息队列
Linux 提供了一系列消息队列的函数接口来让我们方便地使用它来实现进程间的通信。它的用法与其他两个 System V PIC 机制,即信号量和共享内存相似。
msgget() 函数
该函数用来创建和访问一个消息队列。它的原型为:
int msgget(key_t, key, int msgflg);
与其他的 IPC 机制一样,程序必须提供一个键来命名某个特定的消息队列。msgflg 是一个权限标志,表示消息队列的访问权限,它与文件的访问权限一样。msgflg 可以与 IPC_CREAT 做或操作,表示当 key 所命名的消息队列不存在时创建一个消息队列,如果 key 所命名的消息队列存在时,IPC_CREAT 标志会被忽略,而只返回一个标识符。
它返回一个以 key 命名的消息队列的标识符(非零整数),失败时返回 -1.
msgsnd() 函数
该函数用来把消息添加到消息队列中。它的原型为:
int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
-
msgid是由msgget函数返回的消息队列标识符。 -
msg_ptr是一个指向准备发送消息的指针,但是消息的数据结构却有一定的要求,指针msg_ptr所指向的消息结构一定要是以一个长整型成员变量开始的结构体,接收函数将用这个成员来确定消息的类型。所以消息结构要定义成这样:
struct my_message {
long int message_type;
/* The data you wish to transfer */
};
-
msg_sz是msg_ptr指向的消息的长度,注意是消息的长度,而不是整个结构体的长度,也就是说msg_sz是不包括长整型消息类型成员变量的长度。 -
msgflg用于控制当前消息队列满或队列消息到达系统范围的限制时将要发生的事情。
如果调用成功,消息数据的一分副本将被放到消息队列中,并返回 0,失败时返回 -1.
msgrcv() 函数
该函数用来从一个消息队列获取消息,它的原型为
int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg);
msgid, msg_ptr, msg_st 的作用也函数 msgsnd() 函数的一样。
-
msgtype可以实现一种简单的接收优先级。如果msgtype为 0,就获取队列中的第一个消息。如果它的值大于零,将获取具有相同消息类型的第一个信息。如果它小于零,就获取类型等于或小于msgtype的绝对值的第一个消息。 -
msgflg用于控制当队列中没有相应类型的消息可以接收时将发生的事情。
调用成功时,该函数返回放到接收缓存区中的字节数,消息被复制到由 msg_ptr 指向的用户分配的缓存区中,然后删除消息队列中的对应消息。失败时返回-1。
msgctl() 函数
该函数用来控制消息队列,它与共享内存的 shmctl 函数相似,它的原型为:
int msgctl(int msgid, int command, struct msgid_ds *buf);
-
command是将要采取的动作,它可以取3个值,IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖msgid_ds的值。IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值IPC_RMID:删除消息队列
-
buf是指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构。msgid_ds结构至少包括以下成员:
struct msgid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
成功时返回 0,失败时返回 -1.
select函数
使用select函数时,可以将多个文件描述符集中到一起统一监视,项目如下:
- 是否存在套接字接收数据?
- 无需阻塞传输数据的套接字有哪些?
- 哪些套接字发生了异常?
使用步骤:
- 步骤1:
- 设置文件描述符
- 指定监视范围
- 设置超时
- 步骤2:
- 调用
select函数
- 调用
- 步骤3:
- 查看结果
设置文件描述符
FD_ZERO(fd_set *fdset); // 从`fdset`所有位初始化为0
FD_SET(int fd, fd_set *fdset); // 从`fdset`指向的变量中注册文件描述符`fd`的信息
FD_CLR(int fd, fd_set *fdset); // 从`fdset`指向的变量中清除文件描述符`fd`的信息
FD_ISSET(int fd, fd_set *fdset); // 若`fdset`指向的变量中包含文件描述符`fd`的信息,则返回“真”
上述函数中,FD_ISSET用于验证select函数的调用结果。
设置检查(监视)范围及超时
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exeption, const struct timeval *timeout);
/**
* maxfd: 监视对象文件描述符数量
* readset: 将所有关注“是否存在带读取数据”的文件描述符注册到`fd_set`
* writeset: 将所有关注“是否可传输无阻塞数据”的文件描述符注册到`fd_set`
* exception: 将所有关注“是否发生异常”的文件描述符注册到`fd_set`
* timeout: 调用`select`后,为防止陷入无限阻塞的状态,传递超时(time-out)信息
*/
成功返回大于0的值,失败则返回-1,超时返回0
select函数用来验证3种监视项的变化情况
struct timeval
{
long tv_sec; // second
long tv_usec; // microsecond
}
send & recv
include <sys.socket.h>
ssize_t send(int sockfd, const void *buf, ssize_t nbytes, int flags);
ssize_t recv(int sockfd, const void *buf, ssize_t nbytes, int flags);
/**
* sockfd: 表示数据接收对象的连接的套接字文件描述符
* buf: 保存接收数据的缓冲区地址
* nbytes: 可接受的最大字节数
* flags: 接收数据时可选项信息
*/
成功时返回接收的字节数(收到EOF则返回0),失败则返回-1
flags | 含义 | send | recv |
|---|---|---|---|
MSG_OOB | 用于传输带外数据(Out-of-band data) | · | · |
MSG_PEEK | 验证缓冲区中是否存在接收的数据 | · | |
MSG_DONTROUTE | 数据传输过程中不参照路由(Routing)表,在本地(Local)网络中寻找目的地 | · | |
MSG_DONTWAIT | 调用I/O函数不阻塞,用于使用非阻塞(Non-blocking)I/O | · | · |
MSG_WAITALL | 防止函数返回,直到接受全部请求的字节数 | · |
MSG_OOB的真正意义在于督促数据接收对象尽快处理数据。这是紧急模式的全部内容,而且tcp“保持传输顺序”的传输特性依然成立。
readv & writev
对数据进行整合传输及发送的函数。也就是说,writev可以将分散保存在多个缓冲区中的数据一并发送,通过readv函数可以由多个缓冲分别接收。
#include <sys/uio.h>
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);
ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);
/**
* filedes: 表示数据传输对象的套接字文件描述符(或者像`read`函数那样,传递文件描述符或标准输出描述符)
* iov: iovec结构体数组的地址值
* iovcnt: 第二个参数的数组长度
*/
成功时返回发送的字节数,失败则返回-1
Example: example: writev.c example: readv.c
struct iovec
{
void *iov_base; //缓冲地址
size_t iov_len; //缓冲大小
};
多播
多播(multicast)是基于UDP完成的,可以同时向多个主机传输数据
设置TTL
TTL(Time to Live生存时间),可参考Chapter-09-README
code: news_sender.c
int send_sock;
int time_alive = 64;
...
send_sock = socket(PF_INET, SOCK_DGRAM, 0);
setsockopt(send_sock, IPPROTO_IP, IP_MULTICAST_TTL, (void *)&time_alive, sizeof(time_alive));
...
加入多播组
setsockopt可参考Chapter-09-README
code: news_receiver.c
int recv_sock;
struct ip_mreq join_addr;
...
recv_sock = socket(PF_INET, SOCK_DGRAM, 0);
...
join_addr.imr_multiaddr.s_addr = "多播组地址信息";
join_addr.imr_interface.s_addr = "加入多播组的主机地址信息";
...
setsockopt(send_sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (void *)&time_alive, sizeof(time_alive));
...
struct ip_mreq
{
struct in_addr imr_multiaddr;
struct in_addr imr_interface;
};
广播
广播(Boardcast)在“一次性向多个主机发送数据”这一点与多播有些类似,但传输范围有区别。多播即使在跨越不同网络下,只要加入了多播组就能接收数据。
相反,广播只能向同一个网络中的主机传输数据。
使用标准I/O函数
利用fdopen函数转换为FILE结构体
#include <stdio.h>
FILE *fdopen(int fildes, const char *mode);
/**
* fildes: 需要转换的文件描述符
* mode: 将要创建的FILE结构体的模式(mode)信息
*/
成功则返回转换的FILE结构体指针,失败则返回NULL
Example: desto.c
利用fdopen函数转换为文件描述符
#include <stdio.h>
int fileno(FILE *stream);
成功则返回转换的文件描述符,失败则返回-1
Example: todes.c
文件描述符的半关闭
针对任意一个FILE指针调用fclose时都会关闭文件描述符,也就是终止套接字
要销毁所有文件描述符后才能销毁套接字
此时调用fclose后还剩一个文件描述符,因此没有销毁套接字。然而,并不意味着进入了半关闭状态,这只是准备好了半关闭的环境。要进入真正的半关闭状态需要进行特殊处理。
文件描述符的复制
#include <unistd.h>
int dup(int fildes);
int dup2(int fildes, int fildes2);
/**
* fildes: 需要复制的文件描述符
* fildes2: 明确指定的文件描述符整数值
*/
成功则返回复制的文件描述符,失败则返回-1
dup2函数的fildes2,想起传递大于0且小于进程能生成的最大文件描述符值时,改制将成为复制后得到的文件描述符值。example
epoll理解及应用
epoll_event
epoll函数将通过如下结构体epoll_event,将发生变化的(发生事件的)文件描述符单独集中到一起。
struct epoll_event
{
__uint32_t events;
epoll_data_t data;
};
typedef union epoll_data
{
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
epoll_create
epoll_create: 创建保存epoll文件描述符的空间
#include <sys/epoll.h>
int epoll_create(int size);
/*
size: `epoll`实例大小
*/
成功时返回epoll文件描述符,失败时返回-1
调用epoll_create函数创建的文件描述符保存空间称为“epoll例程”,但有些情况不同。
通过参数size传递的值决定epoll例程的大小,但该值只是向操作系统提的建议。
换言之,size并非用来决定epoll例程大小,而仅供操作系统参考。(Linux 2.6.8后内核会完全忽略size参数)
epoll_ctl
epoll_ctl: 向空间注册并注销文件描述符
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
epfd: 用于注册监视对象的`epoll`例程文件描述符。
op: 用于指定监视对象的增删改等操作
fd: 需要注册的监视对象文件描述符
event: 监视对象的事件
*/
成功时返回epoll文件描述符,失败时返回-1
参数op的可取值:
EPOLL_CTL_ADD:将文件描述符注册到epoll例程。EPOLL_CTL_DEL:从epoll例程中删除文件描述符。如果fd取此值,则第四个参数event应为NULL。EPOLL_CTL_MOD:更改注册的文件描述符的关注事件发生情况。
参数event的使用:这里用到的是epoll_event结构体的events成员,events成员用来指明关注的事件类型。
events可以指明的事件类型不止3种,它的可取值如下:
EPOLLIN: 表示对应的文件描述符可以读(包括对端SOCKET正常关闭)。EPOLLOUT: 表示对应的文件描述符可以写。EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)。EPOLLDHUP: 表示对应的文件描述符被挂断,在边缘触发模式下很有用。EPOLLERR: 表示对应的文件描述符发生错误。EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。发送一次事件后,相应的文件描述符不再收到事件通知。因此需要向epoll_ctl函数的第二个参数传递EPOLL_CTL_MOD,再次设置事件。
可以通过位运算同时传递多个上述参数。
epoll_wait
epoll_wait: 与select函数类似,等待文件描述符发生变化
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
/*
功能:监视发生事件的文件描述符
参数:
`epfd`: `epoll`例程的文件描述符;
`events`: 保存发生事件的文件描述符集合的结构体地址;
`maxevents`: 最多监听的事件数,必须大于 0;
`timeout`: 超时时间,以`ms`为单位。如果`timeout`为 -1,则一直等待到事件发生。(注意类型为 int,和 select 函数中的不同)
返回值:成功时返回发生事件的文件描述符数量,失败时返回 -1。
*/
条件触发和边沿触发
epoll中有两种触发方式:条件触发LT(Level Trigger)和边缘触发ET(Edge Trigger)。
- 条件触发:只要输入缓冲中有数据就一直通知该事件。
- 边缘触发:只有当输入缓冲收到数据时注册一次该事件,之后即使输入缓冲中还有数据也不会再注册。
epoll默认以条件触发方式工作。select函数也是以条件触发方式工作的。
条件触发
该示例与echo_epollserv.c之间的差异如下:
边沿触发
将文件(套接字)改为非阻塞模式
#include <fcntl.h>
int fcntl(int filedes, int cmd, ...);
// 功能:更改或读取文件属性。
// 参数:filedes:要更改属性的文件描述符;cmd:指明函数调用的目的;...:可变参数,根据 cmd 的不同值会有不同情况。
成功时返回cmd参数相关值,失败时返回-1
cmd的可取值(包括但不限于以下):
F_GETFL:取此值时,fcntl函数用于获取文件描述符filedes的属性(会返回相应值)。F_SETFL:更改文件描述符属性。
将文件(套接字)改为非阻塞模式要使用如下的两条语句:
int flag = fcntl(fd, F_GETFL, 0); // 获取之前的属性
fcntl(fd, F_SETFL, flag|O_NONBLOCK); // 在此基础上添加非阻塞`O_NONBLOCK`标志。
线程概念
多进程模型的缺点:
- 创建进程的开销大。
- 进程间通信困难。
- 进程切换开销大(最主要的缺点)。
由于进程的以上缺点,引入了线程。
线程相比进程的优点:
- 创建线程更快。
- 线程间通信方便快捷。
- 线程切换开销小。
线程创建及运行
功能:创建线程并设置该线程的入口函数,然后运行该线程
#include <pthread.h>
int pthread_create(
pthread_t *restrict thread, const pthread_attr_t *restrict attr,
void *(* start_routinue)(void *), void *restrict arg
);
/*
`thread`: 用于保存新创建的线程`ID`;
`attr`: 用于传递线程属性,当`attr`值为`NULL`,创建默认属性的线程;
`start_routine`: 相当于线程的`main`函数;
`arg`: 传递`start_routine`函数的参数信息。
*/
返回值:成功时返回0,失败时返回其他值。
restrict关键字
restrict是C99引入的一种类型限定符,它告诉编译器,对象已经被指针所引用,不能通过除该指针外所有其他直接或间接的方式修改该对象的内容。
注意:由于pthread库不是Linux系统默认的库,链接时需要使用静态库libpthread.a,所以当使用了线程相关的函数后,在编译时要加-lpthread选项以声明需要连接线程库,这样才能调用头文件pthread.h中声明的函数。
否则会报错:对pthread_create未定义的引用。
控制线程的执行流
#include <pthread.h>
int pthread_join(pthread_t thread, void **status);
/*
`thread`: 该参数值`ID`的线程终止后才会从该函数返回
`status`: 保存线程的`main`函数返回值的指针变量地址值
*/
返回值:成功时返回0,失败时返回其他值。
调用pthread_join函数的进程或线程会进入等待状态,直到ID为thread(第一个参数)的线程终止。
进程同步
互斥量
#include <pthread.h>
// 功能: 创建互斥量。
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
/*
mutex: 创建互斥量时传递保存互斥量变量的地址值,销毁时传递要销毁的互斥量地址值
attr: 传递即将创建的互斥量属性,没有特别需要指定的属性时则传递`NULL`
*/
// 功能: 销毁互斥量。
int pthread_mutex_destroy(pthread_mutex_t *mutex);
/*
mutex: 保存将要销毁的互斥量;
*/
返回值:成功时返回0,失败时返回其他值。
// 若`pthread_mutex_init`第二个参数想传递`NULL`,可以使用宏来直接初始化互斥量。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
加锁和解锁
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t* mutex); // 加锁 返回值:成功时返回`0`,失败时返回其他值。
int pthread_mutex_unlock(pthread_mutex_t* mutex); // 解锁 返回值:成功时返回`0`,失败时返回其他值。
信号量
信号量与互斥量原理相似,但功能更多。
信号量除了以可解决同时访问内存的问题,还可以用来控制线程顺序。
信号量的创建和销毁
#include <semaphore.h>
int sem_init(sem_t* sem, int pshared, unsigned int value);
/*
sem: 用于保存创建的信号量
pshared: 取`0`时,创建只允许一个进程内部使用的信号量,取其他值时,创建可由多个进程共享的信号量
value: 要创建的信号量的初始值;
*/
int sem_destory(sem_t* sem);
/*
sem: 保存将要销毁的信号量;
*/
返回值:成功时返回 0,失败时返回其他值。
信号量的post和wait
信号量的wait和post类似互斥量中lock和unlock函数。
信号量的使用原理:当信号量为0时,sem_wait函数会阻塞,直到其他线程调用sem_post函数使信号量值变为1为止。
#include <semaphore.h>
int sem_wait(sem_t* sem); // 将信号量值减`1`
int sem_post(sem_t* sem); // 将信号量值加`1`
销毁线程的两种方法
Linux并不会自动销毁由线程创建的内存空间,要使用如下两种方法来明确销毁线程:
- 调用
pthread_join函数。此函数不仅会等待指定的线程终止,还会引导线程销毁。 - 调用
pthread_detach函数。此函数会将主线程与指定的子线程分离,分离后的子线程执行结束时,资源会自动回收。
理解:pthread有joinable和unjoinable两种状态:
joinable状态:默认状态。当线程函数执行结束时或pthread_exit时不会释放线程所占用堆栈和线程描述符等资源。只有当调用了pthread_join之后这些资源才会被释放。unjoinable状态:线程占用的资源会在线程函数退出时或pthread_exit时自动释放。pthread_detach()函数就是分离线程,即将线程状态转换为unjoinable状态,以保证资源的释放。
此外unjoinable属性也可以在pthread_create时指定。
pthread_detach
#include <pthread.h>
int pthread_detach(pthread_t thread);
// 功能: 分离线程,将线程的状态转换为`unjoinable`状态。
// 参数: `thread`:需要分离的线程`ID`
// 返回值: 成功时返回`0`,失败时返回其他值。