linux 网络编程常用函数总结

文章目录

建立连接

网络字节序转换函数htonl、htons、ntohl、ntohs

主要用于将主机字节序转换为网络字节序或者将网络字节序转换为主机字节序。一般来说,长整型函数通常用来转换IP地址,短整型函数用来转换端口号。

#include<netinet/in.h>
//host to network long
unsigned long int htonl(unsigned long int hostlong);
//host to network short
unsigned short int htons(unsigned short int hostshort);
//network to host long
unsigned long int ntohl(unsigned long int netlong);
//network to host short
unsigned short int ntohs(unsigned short int netshort);
socket 通用地址 sockaddr

地址结构体:

#include<bits/socket.h>
struct sockaddr
{
	sa_family_t sa_family; //地址协议族(AF_UNIX、AF_INET、AF_INET6)
	char sa_data[14];//socket 地址值
}

sa_data 类型值
在这里插入图片描述因为 sa_data 空间不够容纳地址值,因而 Linux 定义了新的通用地址结构:

#include<bits/socket.h>
struct sockaddr_storage
{
	sa_family_t sa_family;
	unsigned long int__ss_align;//对齐字节
	char__ss_padding[128-sizeof(__ss_align)];
}
unix专用 socket 专用地址结构体sockaddr_un
#include<sys/un.h>
struct sockaddr_un
{
	sa_family_t sin_family;/*地址族:AF_UNIX*/
	char sun_path[108];/*文件路径名*/
};
TCP/IP专用地址数据结构 sockaddr_in
//IPv4地址结构
struct sockaddr_in
{
	sa_family_t sin_family;/*地址族:AF_INET*/
	u_int16_t sin_port;/*端口号,要用网络字节序表示*/
	struct in_addr sin_addr;/*IPv4地址结构体,见下面*/
};
struct in_addr
{
	u_int32_t s_addr;/*IPv4地址,要用网络字节序表示*/
};
//IPv6地址结构
struct sockaddr_in6
{
	sa_family_t sin6_family;/*地址族:AF_INET6*/
	u_int16_t sin6_port;/*端口号,要用网络字节序表示*/
	u_int32_t sin6_flowinfo;/*流信息,应设置为0*/
	struct in6_addr sin6_addr;/*IPv6地址结构体,见下面*/
	u_int32_t sin6_scope_id;/*scope ID,尚处于实验阶段*/
};
struct in6_addr
{
	unsigned char sa_addr[16];/*IPv6地址,要用网络字节序表示*/
};

所有专用socket地址(以及sockaddr_storage)类型的变量在实际使
用时都需要转化为通用socket地址类型sockaddr(强制转换即可),因
为所有socket编程接口使用的地址参数的类型都是sockaddr。

IP 地址和网络字节序的转换 inet_addr、inet_aton、inet_ntoa
#include<arpa/inet.h>
//点分十进制的ip地址转换为网络字节序,失败时返回INADDR_NONE。
in_addr_t inet_addr(const char*strptr);
//点分十进制的 ip 地址(cp指向)转换为网络字节序存储于 inp,失败时返回 0。
int inet_aton(const char*cp,struct in_addr*inp);
//网络字节序转换为点分十进制并返回,返回值为静态变量指向,函数不可重入。
char*inet_ntoa(struct in_addr in);

/*
更新后同时适用于 IPv4 和 IPv6 的函数,推荐使用
*/
//将 src 指向的 IP 地址转换为网络字节序存放于 dst 中。
//参数 af 指定 AF_INET 或者 AF_INET_6,成功返回 1, 错误吴返回 0, 并设置errno。
int inet_pton(int af,const char*src,void*dst);
//前三个参数的含义与inet_pton的参数相同,最后一个参数cnt指定目标存储单元的大小。
//cnt 取值为 INET_ADDRSTRLEN 或者 INET6_ADDRSTRLEN。
const char*inet_ntop(int af,const void*src,char*dst,socklen_t cnt);
socket

创建一个 socket ,并返回其文件描述符。

#include<sys/types.h>
#include<sys/socket.h>
/*
参数 domain:AF_INET、PF_INET、AF_UNIX等
参数 type: 有SOCK_STREAM(TCP)、SOCK_DGRAM(UDP),
			*| SOCK_NONBLOCK(非阻塞方式) ,
			*|SOCK_CLOEXEC(子进程中关闭)。
参数 protocol:更具体的协议, 一般为 0。
返回值:套接字对应的文件文件描述符。
*/
int socket(int domain,int type,int protocol);
bind

将一个套接字描述符和特定的地址进行绑定。当执行失败时会设置 errno 并返回 0,erron 为 EACCES 表示绑定受保护的地址erron 为EADDRINUSE 表示绑定到已用端口。

/*
参数 socket:带待绑定的文件描述符
参数 my_addr: 绑定地址
参数 addrlen: 地址长度
*/
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd,
	const struct sockaddr*my_addr,
	socklen_t addrlen);
listen

sockfd参数指定被监听的socket。backlog参数提示内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端也将收到ECONNREFUSED错误信息。

#include<sys/socket.h>
int listen(int sockfd,int backlog);
accept

从listen监听队列中接受一个连接。accept只是从监听队列中取出连接,而不论连接处于何种状态(如上面的ESTABLISHED状态和CLOSE_WAIT状态),更不关心任何网络状况的变化。

/*
sockfd: 用于监听的套接字的文件描述符。
addr:被接收方的的地址
addrlen:被接收方地址的长度
返回值:成功时返回一个用于和客户端通信的文件描述符。
*/
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd,struct sockaddr*addr,socklen_t*addrlen);
connect

客户端主动发起连接调用的系统API, 失败时设置 errno。
两种常见的errno
ECONNREFUSED:目标端口不存在,连接被拒绝。
ETIMEDOUT:连接超时。

#include<sys/types.h>
#include<sys/socket.h>
/*
sockfd:用于和服务器通信的文件描述符,由 socket 系统调用返回
serv_addr:服务器端地址
addrlen:服务器端地址长度
返回值:成功返回 0,错误返回 -1, 并设置 errno 
*/
int connect(int sockfd,const struct sockaddr*serv_addr,socklen_t
addrlen);
close

关闭一个连接实际上就是关闭该连接对应的socket。

/*
fd参数是待关闭的socket。不过,close系统调用并非总是立即关闭
一个连接,而是将fd的引用计数减1。只有当fd的引用计数为0时,才真
正关闭连接。多进程程序中,一次fork系统调用默认将使父进程中打开
的socket的引用计数加1,因此我们必须在父进程和子进程中都对该
socket执行close调用才能将连接关闭。
*/
#include<unistd.h>
int close(int fd);
shutdown

如果无论如何都要立即终止连接(而不是将socket的引用计数减1),关闭方式的选择。
在这里插入图片描述shutdown成功时返回 0,失败则返回 -1 并设置errno。

#include<sys/socket.h>
/*
howto : 指定关闭的方式。
*/
int shutdown(int sockfd,int howto);

网络通信

对文件的读写操作read和write同样适用于socket。但是socket编程接
口提供了几个专门用于socket数据读写的系统调用,它们增加了对数据
读写的控制。

TCP 数据流的读写 recv、send
#include<sys/types.h>
#include<sys/socket.h>
/*
recv读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置
和大小,flags参数的含义见后文,通常设置为0即可。成功时返回
实际读取到的数据的长度,recv返回0,表示对方已经关闭连接,
recv出错时返回-1并设置errno。
*/
ssize_t recv(int sockfd,void*buf,size_t len,int flags);
/*
send往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置
和大小。send成功时返回实际写入的数据的长度,失败则返回-1并设置
errno。
*/
ssize_t send(int sockfd,const void*buf,size_t len,int flags);

flag 参数的含义:
在这里插入图片描述

UDP数据包的读写 recvfrom、sendto
#include<sys/types.h>
#include<sys/socket.h>
/*
UDP通信没有连接的概念,所以我们每次读取数据都
需要获取发送端的socket地址,即参数src_addr所指的内容,addrlen参
数则指定该地址的长度。
*/
ssize_t recvfrom(int sockfd,void*buf,size_t len,int flags,struct
	sockaddr*src_addr,socklen_t*addrlen);
	
/*
dest_addr参数指定接收端的socket地址,addrlen参数则指定该
地址的长度。
*/
ssize_t sendto(int sockfd,const void*buf,size_t len,int
	flags,const struct sockaddr*dest_addr,socklen_t addrlen);

这两个系统调用的flags参数以及返回值的含义均与send/recv系统调
用的flags参数及返回值相同。

通用数据读写 recvmsg、sendmsg

既能用于 TCP 又能用于 UDP。

#include<sys/socket.h>
ssize_t recvmsg(int sockfd,struct msghdr*msg,int flags);
ssize_t sendmsg(int sockfd,struct msghdr*msg,int flags);

struct msghdr
{
	void*msg_name;/*socket地址,指定通信对方的socket地址,TCP设置为NULL*/
	socklen_t msg_namelen;/*socket地址的长度*/
	struct iovec * msg_iov;/*分散的内存块,见后文*/
	int msg_iovlen;/*分散内存块的数量*/
	void*msg_control;/*指向辅助数据的起始位置*/
	socklen_t msg_controllen;/*辅助数据的大小*/
	int msg_flags;/*复制函数中的flags参数,并在调用过程中更新*/
};
struct iovec
{
void*iov_base;/*内存起始地址*/
size_t iov_len;/*这块内存的长度*/
};

对于recvmsg而言,数据将被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度则由msg_iov指向的数组指定,这称为分散读(scatter read);对于sendmsg而言msg_iovlen块分散内存中的数据将被一并发送这称为集中(gatherwrite)。recvmsg/sendmsg的flags参数以及返回值的含义均与send/recv的flags参数及返回值相同。

带外标记 sockatmark

sockatmark判断sockfd是否处于带外标记,即下一个被读取到的数据是否是带外数据。如果是,sockatmark返回1,此时我们就可以利用带MSG_OOB标志的recv调用来接收带外数据。如果不是,则sockatmark返回0。

#include<sys/socket.h>
int sockatmark(int sockfd);
地址信息函数 getsockname、getpeername
/*
获取sockfd对应的本端socket地址,并将其存储于address参数指定的内存中,该socket地址的长度则存储于address_len参数指向的变量中。getsockname成功时返回0,失败返回-1并设置errno。
*/
#include<sys/socket.h>
int getsockname(int sockfd,struct
	sockaddr*address,socklen_t*address_len);
/*
获取sockfd对应的远端socket地址,其参数及返回值的含义与getsockname的参数及返回值相同。
*/
int getpeername(int sockfd,struct
	sockaddr*address,socklen_t*address_len);

socket 选项 getsockopt、setsockopt

/*
level参数指定要操作哪个协议的选项(即属性),比如IPv4、IPv6、TCP等。
option_name参数则指定选项的名字, option_value和option_len参数分别是被操作选项的值和长度。getsockopt和setsockopt这两个函数成功时返回0,失败时返回-1并设置errno。
*/
#include<sys/socket.h>
int getsockopt(int sockfd,int level,int
	option_name,void*option_value,socklen_t*restrict option_len);
int setsockopt(int sockfd,int level,int option_name,const
	void*option_value,socklen_t option_len);

在这里插入图片描述

SO_REUSEADDR 选项

TCP连接的TIME_WAIT状态,并提到服务器程序可以通过设置socket选项SO_REUSEADDR来强制使用被处于TIME_WAIT状态的连接占用的socket地址。

int reuse=1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse));
SO_RCVBUF 和 SO_SNDBUF选项

SO_RCVBUF和SO_SNDBUF选项分别表示TCP接收缓冲区和发送缓冲区的大小。当我们用setsockopt来设置TCP的接收缓冲区和发送缓冲区的大小时,系统都会将其值加倍,并且不得小于某个最小值。

int sendbuf=atoi(argv[3]);
int len=sizeof(sendbuf);
/*先设置TCP发送缓冲区的大小,然后立即读取之*/
setsockopt(sock,SOL_SOCKET,SO_SNDBUF,&sendbuf,sizeof(sendbuf));
getsockopt(sock,SOL_SOCKET,SO_SNDBUF,&sendbuf,(socklen_t*)&len);
/*先设置TCP接收缓冲区的大小,然后立即读取之*/
setsockopt(sock,SOL_SOCKET,SO_RCVBUF,&recvbuf,sizeof(recvbuf));
getsockopt(sock,SOL_SOCKET,SO_RCVBUF,&recvbuf,(socklen_t*)&len);
SO_RCVLOWAT 和 SO_SNDLOWAT选项

SO_RCVLOWAT和SO_SNDLOWAT选项分别表示TCP接收缓冲区和发送缓冲区的低水位标记。它们一般被I/O复用系统调用,用来判断socket是否可读或可写。当TCP接收缓冲区中可读数据的总数大于其低水位标记时,I/O复用系统调用将通知应用程序可以从对应的socket上读取数据;当TCP发送缓冲区中的空闲空间大于其低水位标记时,I/O复用系调用将通知应用程序可以往对应的socke上写入数据。

网络信息API

gethostbyname 和 gethostbyaddr
#include<netdb.h>
/*
根据主机名称获取主机的完整信息
name 参数指定目标主机的主机名
*/
struct hostent* gethostbyname(const char*name);
/*
根据IP地址获取主机的完整信息
addr参数指定目标主机的IP地址,
len参数指定addr所指IP地址的长度,
type参数指定addr所指IP地址的类型,其合法取值包括 AF_INET、AF_INET6
*/
struct hostent* gethostbyaddr(const void*addr,size_t len,int type);

struct hostent
{
char*h_name;/*主机名*/
	char**h_aliases;/*主机别名列表,可能有多个*/
	int h_addrtype;/*地址类型(地址族)*/
	int h_length;/*地址长度*/
	char**h_addr_list/*按网络字节序列出的主机IP地址列表*/
};
getservbyname和getservbyport
#include<netdb.h>
/*
根据名称获取某个服务的完整信息,
name参数指定目标服务的名字
proto参数指定服务类型,给它传递“tcp”表示获取流服务,
给它传递“udp”表示获取数据报服务,
给它传递NULL则表示获取所有类型的服务。
*/
struct servent * getservbyname(const char*name,const char*proto);
/*
根据端口号获取某个服务的完整信息
port参数指定目标服务对应的端口号
proto同上getservbyname
*/
struct servent * getservbyport(int port,const char*proto);

struct servent
{
	char* _name;/*服务名称*/
	char** s_aliases;/*服务的别名列表,可能有多个*/
	int s_port;/*端口号*/
	char*s_proto;/*服务类型,通常是tcp或者udp*/
}

需要指出的是,上面讨论的4个函数都是不可重入的,即非线程安
全的。不过netdb.h头文件给出了它们的可重入版本。正如Linux下所有
其他函数的可重入版本的命名规则那样,这些函数的函数名是在原函
数名尾部加上_r(re-entrant)。

getaddrinfo

getaddrinfo 函数既能通过主机名获得IP地址(内部使用的是 gethostbyname 函数),也能通过服务名获得端口号(内部使用的是 getservbyname 函数)。它是否可重入取决于其内部调用的 gethostbyname和 getservbyname 函数是否是它们的可重入版本。

#include<netdb.h>
/*
hostname 参数可以接收主机名,也可以接收字符串表示的IP地址。
service 参数可以接收服务名,也可以接收字符串表示的十进制端口号。
hints参数可以被设置为NULL,表示允许 getaddrinfo 反馈任何可用的结果。
result参数指向一个链表,该链表用于存储getaddrinfo反馈的结果。
*/
int getaddrinfo(const char*hostname, const char*service,
     const struct addrinfo*hints,struct addrinfo**result);
     
struct addrinfo
{
	int ai_flags;/*见后文*/
	int ai_family;/*地址族*/
	int ai_socktype;/*服务类型,SOCK_STREAM 或 SOCK_DGRAM*/
	int ai_protocol;/*ai_protocol成员是指具体的网络协议,其含义和socket系统调用的第三个参数相同,它通常被设置为0*/
	socklen_t ai_addrlen;/*socket地址ai_addr的长度*/
	char*ai_canonname;/*主机的别名*/
	struct sockaddr*ai_addr;/*指向socket地址*/
	struct addrinfo*ai_next;/*指向下一个sockinfo结构的对象*/
};

ai_flag 成员为以下相与的结果
在这里插入图片描述当使用hints参数的时候,可以设置其ai_flags,ai_family,ai_socktype和ai_protocol四个字段,其他字段则必须被设置为NULL。
getaddrinfo将隐式地分配堆内存,所以,getaddrinfo调用结束后,我们必须使用如下配对函数来释放这块内存:

#include<netdb.h>
void freeaddrinfo(struct addrinfo*res);
getnameinfo

getnameinfo函数能通过socket地址同时获得以字符串表示的主机名(内部使用的是gethostbyaddr函数)和服务名(内部使用的是getservbyport函数)。

#include<netdb.h>
/*
主机名存储在host参数指向的缓存中,
将服务名存储在serv参数指向的缓存中,
hostlen和servlen参数分别指定这两块缓存的长度。
*/
int getnameinfo(const struct sockaddr*sockaddr,socklen_taddrlen,char*host,
				socklen_thostlen,char*serv,socklen_t servlen,int flags);

flags参数控制getnameinfo的行为:
在这里插入图片描述
getaddrinfo和getnameinfo函数成功时返回0,失败则返回错误码, 如下:
在这里插入图片描述下列函数将错误码转换为其字符串形式:

#include<netdb.h>
const char*gai_strerror(int error);

高级 I/O 函数

pip 和 socketpair

pipe函数可用于创建一个管道,以实现进程间通信。如果管道的写端文件描述符fd[1]
的引用计数减少至0,即没有任何进程需要往管道中写入数据,则针对该管道的读端文件描述符fd[0]的read操作将返回0,即读取到了文件结束标记(End Of File,EOF);反之,如果管道的读端文件描述符fd[0]的引用计数减少至0,即没有任何进程需要从管道读取数据,则针对该管道的写端文件描述符fd[1]的write操作将失败,并引发 SIGPIPE 信号。

#include<unistd.h>
/*
参数是一个包含两个 int 型整数的数组指针,两个文件描述符fd[0]和fd[1]分别构成管道的两端,fd[1] 只能写 fd[0] 只能读,单向传输。
该函数成功时返回0,并将一对打开的文件描述符值填入其参数指向的数组。
*/
int pipe(int fd[2]);

socket的基础API中有一个socketpair函数。它能够方便地创
建双向管道。

#include<sys/types.h>
#include<sys/socket.h>
/*
前三个参数和 socket 相同,第四个参数 fd 和pip相同,但是是双向通信的。
*/
int socketpair(int domain,int type,int protocol,int fd[2]);
dup函数和dup2函数

复制文件描述符

#include<unistd.h>
/*
创建一个新的文件描述符,该新文件描述符和原有文件描述符 file_descriptor 指向相同的文件、管道或者网络连接。并且 dup 返回的文件描述符总是取系统当前可用的最小整数值。
*/
int dup(int file_descriptor);
/*
和 dup 相同,只是返回的文件描述符为第一个不小于 file_descriptor_two 的整数值。
*/
int dup2(int file_descriptor_one,int file_descriptor_two);

CGI 服务器的基本原理:

.......
.......
int connfd=accept(sock,(struct sockaddr*)&client,&
client_addrlength);
if(connfd<0)
{
	printf("errno is:%d\n",errno);
}
else
{
	close(STDOUT_FILENO);//实际 STDOUT_FILENO == 1
	dup(connfd);//此时返回的为系统中最小的 fd, 即为 1;
	printf("abcd\n");//此时输出到标准输出的数据将输出到客户端,而非stdout。
	close(connfd);
}
close(sock);
return 0;
readv 函数和 writev 函数

readv函数将数据从文件描述符读到分散的内存块中,即分散读;writev函数则将多块分散的内存数据一并写入文件描述符中,即集中写。

#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);

struct iovec
{
void*iov_base;/*内存起始地址*/
size_t iov_len;/*这块内存的长度*/
};
sendfile 函数

sendfile函数在两个文件描述符之间直接传递数据(完全在内核中操作),从而避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,这被称为零拷贝。

#include<sys/sendfile.h>
ssize_t sendfile(int out_fd,int in_fd,off_t*offset,size_t count);

in_fd必须是一个支持类似mmap函数的文件描述符,即它必须指向真实的文件,不能是socket和管道;而out_fd则必须是一个socket。offset参数指定从读入文件流的哪个位置开始读,如果为空,则使用读入文件流默认的起始位置。count 参数指定在文件描述符 in_fd 和 out_fd 之间传输的字节数,offset 为文件的偏移。

mmap 函数 和 munmap 函数

mmap函数用于申请一段内存空间。我们可以将这段内存作为进程间通信的共享内存,也可以将文件直接映射到其中。munmap函数则释放由mmap创建的这段内存空间。mmap函数成功时返回指向目标内存区域的指针,失败则返回MAP_FAILED((void*)-1)并设置errno。munmap函数成功时返回0,失败则返回-1并设置errno。

#include<sys/mman.h>
/*
start: 允许用户使用某个特定的地址作为这段内存的起始地址,
		  如果它被设置成NULL,则系统自动分配一个地址。
length:指定内存段的长度。
prot: 用来设置内存段的访问权限,可以是以下值按位 |。
			❑PROT_READ,内存段可读。
			❑PROT_WRITE,内存段可写。
			❑PROT_EXEC,内存段可执行。
			❑PROT_NONE,内存段不能被访问。
flag: 控制内存段内容被修改后程序的行为,具体见后文。
fd:  被映射文件对应的文件描述符。
offset: 文件的何处开始映射
*/
void *mmap(void*start,size_t length,int prot,int flags,int fd,off_t offset);
int munmap(void*start,size_t length);

在这里插入图片描述

splice函数

splice函数用于在两个文件描述符之间移动数据,也是零拷贝操作。

#include<fcntl.h>
/*
fd_in : 待输入数据的文件描述符。
off_in: 输入数据流读取数据起始偏移位置,管道文件描述符则该参数为 NULL。
fd_out : 输出数据文件描述符
off_out : 同 off_in。
flag : 控制数据如何移动,具体值见后文。

*/
ssize_t splice(int fd_in,loff_t*off_in,int fd_out,loff_t*off_out,size_t len,unsigned int flags);

flag 参数的取值
在这里插入图片描述使用splice函数时,fd_in和fd_out必须至少有一个是管道文件描述符。splice函数调用成功时返回移动字节的数量。它可能返回0,表示没有数据需要移动,这发生在从管道中读取数据(fd_in是管道文件描述符)而该管道没有被写入任何数据时。splice函数失败时返回-1并设置errno。
在这里插入图片描述

tee函数

tee函数在两个管道文件描述符之间复制数据,也是零拷贝操作。它不消耗数据,因此源文件描述符上的数据仍然可以用于后续的读操作。

#include<fcntl.h>
/*
fd_in 和 fd_out必须都是管道文件描述符。
flag 和上述的 splice 的参数相同。
*/
ssize_t tee(int fd_in,int fd_out,size_t len,unsigned int flags);
fcntl函数

fcntl函数,正如其名字(file control)描述的那样,提供了对文件描述符的各种控制操作。

#include<fcntl.h>
/*
fd参数是被操作的文件描述符,cmd参数指定执行何种类型的操作。
*/
int fcntl(int fd,int cmd,);

参数指定方式:
在这里插入图片描述
将一个文件描述符设置为非阻塞

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;/*返回文件描述符旧的状态标志,以便日后恢复该状态标志*/
}

日志

syslog函数

应用程序通过 syslog 函数与 rsyslogd 守护进程通信。以记录Linux相关的日志信息。

#include<syslog.h>
/*
priority: 设施值与日志级别的按位或,默认值是 LOG_USER 。
第二个参数 message 和第三个参数… 来结构化输出。
*/
void syslog(int priority,const char*message,...);
// 日志级别的定义
#define LOG_EMERG 0/*系统不可用*/
#define LOG_ALERT 1/*报警,需要立即采取动作*/
#define LOG_CRIT 2/*非常严重的情况*/
#define LOG_ERR 3/*错误*/
#define LOG_WARNING 4/*警告*/
#define LOG_NOTICE 5/*通知*/
#define LOG_INFO 6/*信息*/
#define LOG_DEBUG 7/*调试*/
openlog

改变syslog的默认输出方式,进一步结构化日志内容。

#include<syslog.h>
/*
ident : 指定的字符串将被添加到日志消息的日期和时间之后。
logopt : 对后续syslog调用的行为进行配置。
facility : 用来修改syslog函数中的默认设施值。
*/
void openlog(const char*ident,int logopt,int facility);

/*
logopt 的取值和意义
*/
#define LOG_PID 0x01/*在日志消息中包含程序PID*/
#define LOG_CONS 0x02/*如果消息不能记录到日志文件,则打印至终端*/
#define LOG_ODELAY 0x04/*延迟打开日志功能直到第一次调用syslog*/
#define LOG_NDELAY 0x08/*不延迟打开日志功能*/
setlogmask

程序在开发阶段可能需要输出很多调试信息,而发布之后我们又需要将这些调试信息关闭。解决这个问题的方法并不是在程序发布之后删除调试代码(因为日后可能还需要用到),而是简单地设置日志掩码,使日志级别大于日志掩码的日志信息被系统忽略。

#include<syslog.h>
/*
maskpri : 日志掩码值
*/
int setlogmask(int maskpri);
closelog

用于关闭日志功能的函数

#include<syslog.h>
void closelog();

用户信息

UID、EUID、GID和EGID
#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*/
getpgid、setpgid

Linux下每个进程都隶属于一个进程组,因此它们除了PID信息外,还有进程组ID(PGID)

#include<unistd.h>
/*
该函数成功时返回进程pid所属进程组的PGID,失败则返回-1并设置errno。
*/
pid_t getpgid(pid_t pid);
/*
该函数将PID为pid的进程的PGID设置为pgid。一个进程只能设置自己或者其子进程的PGID。
并且,当子进程调用exec系列函数后,我们也不能再在父进程中对它设置PGID。
*/
int setpgid(pid_t pid,pid_t pgid);
setsid

部分进程组将形成一个会话(session)。

#include<unistd.h>
/*
该函数不能由进程组的首领进程调用,否则将产生一个错误。
❑调用进程成为会话的首领,此时该进程是新会话的唯一成员。
❑新建一个进程组,其PGID就是调用进程的PID,调用进程成为该组的首领。
❑调用进程将甩开终端。
*/
pid_t setsid(void);
/*Linux进程并未提供所谓会话ID(SID)的概念,但Linux系统认为它等于会话首领所在的进程组的PGID*/
pid_t getsid(pid_t pid);
getrlimit、setrlimit

Linux上运行的程序都会受到资源限制的影响,比如物理设备限制(CPU数量、内存数量等)、系统策略限制(CPU时间等),以及具体实现的限制(比如文件名的最大长度)。

#include<sys/resource.h>
/*
resource :指定资源限制类型
*/
int getrlimit(int resource,struct rlimit*rlim);
int setrlimit(int resource,const struct rlimit*rlim);

struct rlimit
{
	rlim_t rlim_cur;//软限制
	rlim_t rlim_max;//硬限制
};

resource 取值类型。
在这里插入图片描述

getcwd、chdir

获取进程当前工作目录和改变进程工作目录。

#include<unistd.h>
/*
 buf参数指向的内存用于存储进程当前工作目录的绝对路径名,其大小由size参数指定。
 getcwd函数成功时返回一个指向目标存储区(buf指向的缓存区或是getcwd在内部动态创建的缓存区)的指针,失败则返回NULL并设置errno。
*/
char* getcwd(char*buf,size_t size);
/*
path参数指定要切换到的目标目录。
*/
int chdir(const char*path);
daemon
#include<unistd.h>
/*
	nochdir参数用于指定是否改变工作目录,如果给它传递0,则工作目录将被设置为“/”(根目录),否则继续使用当前工作目录。noclose参数为0时,标准输入、标准输出和标准错误输出都被重定向到/dev/null文件,否则依然使用原来的设备。
*/
int daemon(int nochdir,int noclose);

I/O 复用

select
#include<sys/select.h>
/*
nfds: 指定被监听的文件描述符的总数。它通常被设置为
select监听的所有文件描述符中的最大值加1,因为文件描述符是从0开
始计数的。
readfds、writefds和exceptfds参数分别指向可读、可写和异常
等事件对应的文件描述符集合。
timeout参数用来设置select函数的超时时间。
*/
int select(int nfds,fd_set* readfds,
		   fd_set*writefds, fd_set*exceptfds,
		   struct timeval*timeout);
/*
 用于操作文件描述符集合的宏
*/
FD_ZERO(fd_set*fdset);/*清除fdset的所有位*/
FD_SET(int fd,fd_set*fdset);/*设置fdset的位fd*/
FD_CLR(int fd,fd_set*fdset);/*清除fdset的位fd*/
int FD_ISSET(int fd,fd_set*fdset);/*测试fdset的位fd是否被设置*/
poll
#include<poll.h>
/*
fds参数是一个pollfd结构类型的数组,它指定所有我们感兴趣
的文件描述符上发生的可读、可写和异常等事件。
nfds参数指定 fds 的数目
timeout参数用来设置select函数的超时时间。
*/
int poll(struct pollfd*fds,nfds_t nfds,int timeout);
struct pollfd
{
	int fd;/*文件描述符*/
	short events;/*注册的事件*/
	short revents;/*实际发生的事件,由内核填充*/
};

poll 中注册的事件类型:
在这里插入图片描述

epoll

它在实现和使用上与select、poll有很大差异。首先,epoll使用一组函数来完成任务,而不是单个函数。其次,epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和poll那样每次调用都要重复传入文件描述符集或事件集。但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。

#include<sys/epoll.h>
/*
 创建一个 epoll 的事件表,size参数现在并不起作用,只是给内核一个提示,
 告诉它事件表需要多大。 该函数返回的文件描述符将用作其他所有epoll系统调用的
第一个参数,以指定要访问的内核事件表。
*/
int epoll_create(int size);

/*
fd 参数是要操作的文件描述符。
op 参数则指定操作类型,有如下 3 种
	❑EPOLL_CTL_ADD,往事件表中注册fd上的事件。
	❑EPOLL_CTL_MOD,修改fd上的注册事件。
	❑EPOLL_CTL_DEL,删除fd上的注册事件。
event 参数见后
*/
int epoll_ctl(int epfd,int op,int fd,struct epoll_event*event);
struct epoll_event
{
	__uint32_t events;/*epoll事件*/
	epoll_data_t data;/*用户数据*/
};
typedef union epoll_data
{
	void*ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
}epoll_data_t;

/*
epoll系列系统调用的主要接口是epoll_wait函数。它在一段超时时间内等待一组文件描述符上的事件。
maxevents 参数指定最多监听多少个事件,它必须大于0。
events 将所有就绪的事件从内核事件表(由epfd参数指定)中复制到它的第二个参数events指向的数组中。
*/
int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);
LT和ET模式

LT模式是默认的工作模式,这种模式下 epoll 相当于一个效率较高的poll。当有就绪文件事件时 epoll 将会多次触发相应的事件。ET模式是 epoll 的高效工作模式,当有相应的事件发生时,该事件应该被立即处理,因为后续该事件将不会再次被触发,如果该次触发未被处理。每个使用ET模式的文件描述符都应该是非阻塞的。如果文
件描述符是阻塞的,那么读或写操作将会因为没有后续的事件而一直处于阻塞状态(饥渴状态)。
代码示例:

#include<sys / types.h>
#include<sys / socket.h>
#include<netinet / in.h>
#include<arpa / inet.h>
#include<assert.h>
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<fcntl.h>
#include<stdlib.h>
#include<sys / epoll.h>
#include<pthread.h>
#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 10
/*将文件描述符设置成非阻塞的*/
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;
}
/*将文件描述符fd上的EPOLLIN注册到epollfd指示的epoll内核事件表中,参数
enable_et指定是否对fd启用ET模式*/
void addfd(int epollfd, int fd, bool enable_et)
{
	epoll_event event;
	event.data.fd = fd;
	event.events = EPOLLIN;
	if (enable_et)
	{
		event.events |= EPOLLET;
	}
	epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
	setnonblocking(fd);
}
/*LT模式的工作流程*/
void lt(epoll_event* events, int number, int epollfd, int listenfd)
{
	char buf[BUFFER_SIZE];
	for (int i = 0; i<number; i++)
	{
		int sockfd = events[i].data.fd;
		if (sockfd == listenfd)
		{
			struct sockaddr_in client_address;
			socklen_t client_addrlength = sizeof(client_address);
			int connfd = accept(listenfd, (struct sockaddr*)&client_address,
				&client_addrlength);
			addfd(epollfd, connfd, false);/*对connfd禁用ET模式*/
		}
		else if (events[i].events&EPOLLIN)
		{
			/*只要socket读缓存中还有未读出的数据,这段代码就被触发*/
			printf("event trigger once\n");
			memset(buf, '\0', BUFFER_SIZE);
			int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);
			if (ret< = 0)
			{
				close(sockfd);
				continue;
			}
			printf("get%d bytes of content:%s\n", ret, buf);
		}
		else
		{
			printf("something else happened\n");
		}
	}
}
/*ET模式的工作流程*/
void et(epoll_event* events, int number, int epollfd, int listenfd)
{
	char buf[BUFFER_SIZE];
	for (int i = 0; i<number; i++)
	{
		int sockfd = events[i].data.fd;
		if (sockfd == listenfd)
		{
			struct sockaddr_in client_address;
			socklen_t client_addrlength = sizeof(client_address);
			int connfd = accept(listenfd, (struct sockaddr*)&client_address, &
				client_addrlength);
			addfd(epollfd, connfd, true);/*对connfd开启ET模式*/
		}
		else if (events[i].events&EPOLLIN)
		{
			/*这段代码不会被重复触发,所以我们循环读取数据,以确保把socket读缓存中的所
			有数据读出*/
			printf("event trigger once\n");
			while (1)
			{
				memset(buf, '\0', BUFFER_SIZE);
				int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);
				if (ret<0)
				{
					/*对于非阻塞IO,下面的条件成立表示数据已经全部读取完毕。此后,epoll就能再次
					触发sockfd上的EPOLLIN事件,以驱动下一次读操作*/
					if ((errno == EAGAIN) || (errno == EWOULDBLOCK))
					{
						printf("read later\n");
						break;
					}
					close(sockfd);
					break;
				}
				else if (ret == 0)
				{
					close(sockfd);
				}
				else
				{
					printf("get%d bytes of content:%s\n", ret, buf);
				}
			}
		}
		else
		{
			printf("something else happened\n");
		}
	}
}
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]);
	int ret = 0;
	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 listenfd = socket(PF_INET, SOCK_STREAM, 0);
	assert(listenfd> = 0);
	ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
	assert(ret != -1);
	ret = listen(listenfd, 5);
	assert(ret != -1);
	epoll_event events[MAX_EVENT_NUMBER];
	int epollfd = epoll_create(5);
	assert(epollfd != -1);
	addfd(epollfd, listenfd, true);
	while (1)
	{
		int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
		if (ret<0)
		{
			printf("epoll failure\n");
			break;
		}
		lt(events, ret, epollfd, listenfd);/*使用LT模式*/
		//et(events,ret,epollfd,listenfd);/*使用ET模式*/
	}
	close(listenfd);
	return 0;
}
EPOLLONESHOT事件

即使我们使用ET模式,一个socket上的某个事件还是可能被触发多次。这在并发程序中就会引起一个问题。比如一个线程(或进程,下同)在读取完某个socket上的数据后开始处理这些数据,而在数据的处理过程中该socket上又有新数据可读(EPOLLIN再次被触发),此时另外一个线程被唤醒来读取这些新的数据。于是就出现了两个线
程同时操作一个socket的局面。EPOLLONESHOT 事件中的文件描述只被触发一次,因而处理完应该立刻为其设置 EPOLLONESHOT 事件。
代码示例:

#include<sys / types.h>
#include<sys / socket.h>
#include<netinet / in.h>
#include<arpa / inet.h>
#include<assert.h>
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<fcntl.h>
#include<stdlib.h>
#include<sys / epoll.h>
#include<pthread.h>
#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 1024
struct fds
{
	int epollfd;
	int sockfd;
};
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;
}
/*将fd上的 EPOLLIN 和 EPOLLET 事件注册到 epollfd 指示的 epoll 内核事件表中,参
数 oneshot 指定是否注册 fd 上的 EPOLLONESHOT 事件*/
void addfd(int epollfd, int fd, bool oneshot)
{
	epoll_event event;
	event.data.fd = fd;
	event.events = EPOLLIN | EPOLLET;
	if (oneshot)
	{
		event.events |= EPOLLONESHOT;
	}
	epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
	setnonblocking(fd);
}
/*重置 fd 上的事件。这样操作之后,尽管 fd 上的 EPOLLONESHOT 事件被注册,但是操
作系统仍然会触发 fd 上的 EPOLLIN 事件,且只触发一次*/
void reset_oneshot(int epollfd, int fd)
{
	epoll_event event;
	event.data.fd = fd;
	event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
	epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}
/*工作线程*/
void* worker(void* arg)
{
	int sockfd = ((fds*)arg) - >sockfd;
	int epollfd = ((fds*)arg) - >epollfd;
	printf("start new thread to receive data on fd:%d\n", sockfd);
	char buf[BUFFER_SIZE];
	memset(buf, '\0', BUFFER_SIZE);
	/*循环读取sockfd上的数据,直到遇到EAGAIN错误*/
	while (1)
	{
		int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);
		if (ret == 0)
		{
			close(sockfd);
			printf("foreiner closed the connection\n");
			break;
		}
		else if (ret<0)
		{
			if (errno == EAGAIN)
			{
				reset_oneshot(epollfd, sockfd);
				printf("read later\n");
				break;
			}
		}
		else
		{
			printf("get content:%s\n", buf);
			/*休眠5s,模拟数据处理过程*/
			sleep(5);
		}
	}
	printf("end thread receiving data on fd:%d\n", sockfd);
}
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]);
	int ret = 0;
	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 listenfd = socket(PF_INET, SOCK_STREAM, 0);
	assert(listenfd> = 0);
	ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
	assert(ret != -1);
	ret = listen(listenfd, 5);
	assert(ret != -1);
	epoll_event events[MAX_EVENT_NUMBER];
	int epollfd = epoll_create(5);
	assert(epollfd != -1);
	/*注意,监听socket listenfd上是不能注册EPOLLONESHOT事件的,否则应用程序
	只能处理一个客户连接!因为后续的客户连接请求将不再触发listenfd上的EPOLLIN事件
	*/
	addfd(epollfd, listenfd, false);
	while (1)
	{
		int ret = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
		if (ret<0)
		{
			printf("epoll failure\n");
			break;
		}
		for (int i = 0; i<ret; i++)
		{
			int sockfd = events[i].data.fd;
			if (sockfd == listenfd)
			{
				struct sockaddr_in client_address;
				socklen_t client_addrlength = sizeof(client_address);
				int connfd = accept(listenfd, (struct sockaddr*)&client_address, &
					client_addrlength);
				/*对每个非监听文件描述符都注册E POLLONESHOT 事件*/
				addfd(epollfd, connfd, true);
			}
			else if (events[i].events&EPOLLIN)
			{
				pthread_t thread;
				fds fds_for_new_worker;
				fds_for_new_worker.epollfd = epollfd;
				fds_for_new_worker.sockfd = sockfd;
				/*新启动一个工作线程为sockfd服务*/
				pthread_create(&thread, NULL, worker, (void*)&fds_for_new_worker);
			}
			else
			{
				printf("something else happened\n");
			}
		}
	}
	close(listenfd);
	return 0;
}

信号

kill
#include<sys/types.h>
#include<signal.h>
/*
该函数把信号 sig 发送给目标进程;目标进程由 pid 参数其取值如下表
*/
int kill(pid_t pid,int sig);

在这里插入图片描述

signal
#include<signal.h>
/*
sig参数指出要捕获的信号类型。_handler参数是_sighandler_t类型
的函数指针,用于指定信号sig的处理函数。返回值是前一次调用signal函数时传入的函数指针,
或者是信号sig对应的默认处理函数指针SIG_DEF。
*/
_sighandler_t signal(int sig, _sighandler_t_handler);
typedef void(*__sighandler_t)(int);
sigaction
#include<signal.h>
/*
sig参数指出要捕获的信号类型,act参数指定新的信号处理方式,oact参数则输出信号先前的处理方式(如果不为NULL的话)。
*/
int sigaction(int sig,const struct sigaction*act,struct
						sigaction*oact);

struct sigaction {
			   //信号处理函数
          void     (*sa_handler)(int);
          //信号执行的相应的动作
         void     (*sa_sigaction)(int, siginfo_t *, void *);
         //信号阻塞集,捕捉信号时将会产生信号的捕捉
         sigset_t   sa_mask;
         //sa_flages决定信号执行sa_handler还是执行sa_sigaction。具体见后文
         int        sa_flags;
         //保留字段,很少用
         void     (*sa_restorer)(void);
    };

sagaction中的 flag 取值:
在这里插入图片描述

信号集函数sigemptyset、sigfillset、sigdelset、sigismember
#include<bits/sigset.h>
#define_SIGSET_NWORDS(1024/(8*sizeof(unsigned long int)))
typedef struct
{
unsigned long int__val[_SIGSET_NWORDS];
}__sigset_t;

int sigemptyset(sigset_t*_set)/*清空信号集*/
int sigfillset(sigset_t*_set)/*在信号集中设置所有信号*/
int sigaddset(sigset_t*_set,int_signo)/*将信号_signo添加至信号集中*/
int sigdelset(sigset_t*_set,int_signo)/*将信号_signo从信号集中删除*/
int sigismember(_const sigset_t*_set,int_signo)/*测试_signo是否在信号集中*/
sigprocmask

设置或查看进程的信号掩码

#include<signal.h>
/*
_set参数指定新的信号掩码,_oset参数则输出原来的信号掩码;
如果_set为NULL,则进程信号掩码不变,此时我们仍然可以利用_oset参数来获得进程当前的信号掩码。
how参数指定设置进程信号掩码的方式, 可选值见后文。
*/
int sigprocmask(int_how,_const sigset_t*_set,sigset_t*_oset);

在这里插入图片描述

sigpending
#include<signal.h>
/*
set参数用于保存被挂起的信号集。
*/
int sigpending(sigset_t*set);

进程

fork
#include<sys/types.h>
#include<unistd.h>
/*
该函数的每次调用都返回两次,在父进程中返回的是子进程的PID,在子进程中则返回0。fork调用失败时返回-1,并设置errno。
*/
pid_t fork(void);
exec系列系统调用

有时我们需要在子进程中执行其他程序, 我们使用 exec 函数:

#include<unistd.h>
/*
path参数指定可执行文件的完整路径,file参数可以接受文件名,
该文件的具体位置则在环境变量PATH中搜寻。arg接受可变参数,
argv则接受参数数组,它们都会被传递给新程序(path或file指定的程序)
的main函数。envp参数用于设置新程序的环境变量。如果未设置它,
则新程序将使用由全局变量environ指定的环境变量。一般情况下,exec函数是不返回的,除非出错。它出错时返回-1,并设置errno。如果没出错,则原程序中exec调用之后的代码都
不会执行,因为此时原程序已经被exec的参数指定的程序完全替换。
exec函数不会关闭原程序打开的文件描述符,除非该文件描述符
被设置了类似SOCK_CLOEXEC的属性。
*/
extern char** environ;
int execl(const char*path,const char*arg,...);
int execlp(const char*file,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[]);
int execve(const char*path,char*const argv[],char*const envp[]);
wait 和 waitpid

这对函数在父进程中调用,以等待子进程的结束,并获取子进程的返回信息,从而避免了僵尸进程的产生,或者使子进程的僵尸态立即结束。

#include<sys/types.h>
#include<sys/wait.h>
/*
wait函数将阻塞进程,直到该进程的某个子进程结束运行为止。
它返回结束运行的子进程的PID,并将该子进程的退出状态信息存储于
stat_loc参数指向的内存中。
*/
pid_t wait(int*stat_loc);


/*
waitpid只等待由pid参数指定的子进程。如果pid取值
为-1,那么它就和wait函数相同,即等待任意一个子进程结束。
options参数可以控制waitpid函数的行为。该参数最常用的取值是WNOHANG。当options的取值是
WNOHANG时,waitpid调用将是非阻塞的:如果pid指定的目标子进程
还没有结束或意外终止,则waitpid立即返回0;如果目标子进程确实正
常退出了,则waitpid返回该子进程的PID。waitpid调用失败时返回-1并
设置errno。当一个进程结束时,它将给其父进程发送一个SIGCHLD信号。因此,我们可以在父进程中捕获SIGCHLD信号,并在信号处理函数中调用waitpid函数以“彻底结束”一个子进程
*/
pid_t waitpid(pid_t pid,int*stat_loc,int options);

ys/wait.h头文件中定义了几个宏来帮助解释子进程的退出状态信息。
在这里插入图片描述

信号量

semget
#include<sys/sem.h>
/*
key: 参数是一个键值,用来标识一个全局唯一的信号量集,就像文
件名全局唯一地标识一个文件一样。
num_sems: 参数指定要创建/获取的信号量集中信号量的数目。如果
是创建信号量,则该值必须被指定;如果是获取已经存在的信号量,
则可以把它设置为0。
sem_flags参数指定一组标志。它低端的9个比特是该信号量的权限,
其格式和含义都与系统调用open的mode参数相同。
semget成功时返回一个正整数值,它是信号量集的标识符;semget
失败时返回-1,并设置errno。
*/
int semget(key_t key,int num_sems,int sem_flags);
semop

semop系统调用改变信号量的值,即执行P、V操作。
与每个信号量关联的一些重要的内核变量

/*信号量的值*/
unsigned short semval;
/*等待信号量值变为0的进程数量*/
unsigned short semzcnt;
/*等待信号量值增加的进程数量*/
unsigned short semncnt;
/*最后一次执行semop操作的进程ID*/
pid_t sempid;

semop对信号量的操作实际上就是对这些内核变量的操作。

#include<sys/sem.h>
/*
sem_id参数是由semget调用返回的信号量集标识符,用以指定被操
作的目标信号量集。
num_sem_ops指定要执行的操作个
数,即sem_ops数组中元素的个数。
*/
int semop(int sem_id, struct sembuf*sem_ops, size_t num_sem_ops);
struct sembuf{
	unsigned short int sem_num;//信号量集中信号量的编号,从 0 开始
	short int sem_op;// 指定操作类型,其可选值为正整数、0和负整数。
	short int sem_flg;//见后文
}
/*
sem_op 和 sem_flg 两者的行为共同作用来影响 semop 的行为
❑如果sem_op大于0,则semop将被操作的信号量的值semval增加
	sem_op。该操作要求调用进程对被操作信号量集拥有写权限。此时若
	设置了SEM_UNDO标志,则系统将更新进程的semadj变量(用以跟踪
	进程对信号量的修改情况)。
❑如果sem_op等于0,该操作要求调用进程对被操作信号量集拥有读权限。如果此时信
	号量的值是0,则调用立即成功返回。如果信号量的值不是0,则semop
	失败返回或者阻塞进程以等待信号量变为0。在这种情况下,当
	IPC_NOWAIT标志被指定时,semop立即返回一个错误,并设置errno为
	EAGAIN。如果未指定IPC_NOWAIT标志,则信号量的semzcnt值加1。
❑如果sem_op小于0,则表示对信号量值进行减操作,即期望获得
	信号量。如果信号量的值 semval 小于 sem_op 的绝对值,则 semop 失
	败返回或者阻塞进程以等待信号量可用。
*/
semctl

semctl系统调用允许调用者对信号量进行直接控制。

#include<sys/sem.h>
/*
sem_id 参数是由 semget 调用返回的信号量集标识符,用以指定被操
作的信号量集。
sem_num 参数指定被操作的信号量在信号量集中的编号。
command参数指定要执行的命令。见后文;
有的命令需要调用者传递第 4 个参数。
*/
int semctl(int sem_id,int sem_num,int command, ...);
//第四个参数推荐
union semun
{
	int val;/*用于SETVAL命令*/
	struct semid_ds*buf;/*用于IPC_STAT和IPC_SET命令*/
	unsigned short*array;/*用于GETALL和SETALL命令*/
	struct seminfo*__buf;/*用于IPC_INFO命令*/
};
struct seminfo
{
	int semmap;/*Linux内核没有使用*/
	int semmni;/*系统最多可以拥有的信号量集数目*/
	int semmns;/*系统最多可以拥有的信号量数目*/
	int semmnu;/*Linux内核没有使用*/
	int semmsl;/*一个信号量集最多允许包含的信号量数目*/
	int semopm;/*semop一次最多能执行的sem_op操作数目*/
	int semume;/*Linux内核没有使用*/
	int semusz;/*sem_undo结构体的大小*/
	int semvmx;/*最大允许的信号量值*/
	/*最多允许的UNDO次数(带SEM_UNDO标志的semop操作的次数)*/
	int semaem;
};

semctl支持的所有命令在这里插入图片描述注意 这些操作中,GETNCNT、GETPID、GETVAL、GETZCNT 和 SETVAL操作的是单个信号量,它是由标识符 sem_id 指定的信号量集中的第 sem_num 个信号量;而其他操作针对的是整个信号量集,此时 semctl 的参数 sem_num 被忽略。

特殊键值 IPC_PRIVATE

semget的调用者可以给其key参数传递一个特殊的键值IPC_PRIVATE(其值为0),这样无论该信号量是否已经存在,semget 都将创建一个新的信号量。使用该键值创建的信号量并非像它的名字声称的那样是进程私有的。其他进程,尤其是子进程,也有方法来访问这个信号量。父子进程通过 IPC_PRIVATE 信号量同步示例:

#include<sys / sem.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys / wait.h>
union semun
{
	int val;
	struct semid_ds* buf;
	unsigned short int* array;
	struct seminfo* __buf;
};
/*op为-1时执行P操作,op为1时执行V操作*/
void pv(int sem_id, int op)
{
	struct sembuf sem_b;
	sem_b.sem_num = 0;
	sem_b.sem_op = op;
	sem_b.sem_flg = SEM_UNDO;
	semop(sem_id, &sem_b, 1);
}
int main(int argc, char* argv[])
{
	int sem_id = semget(IPC_PRIVATE, 1, 0666);//创建信号量
	union semun sem_un;
	sem_un.val = 1;
	semctl(sem_id, 0, SETVAL, sem_un);//设置信号量初值为 sem_un.val 
	pid_t id = fork();
	if (id<0)
	{
		return 1;
	}
	else if (id == 0)
	{
		printf("child try to get binary sem\n");
		/*在父、子进程间共享IPC_PRIVATE信号量的关键就在于二者都可以操作该信号量的标
		识符sem_id*/
		pv(sem_id, -1);
		printf("child get the sem and would release it after 5
			seconds\n");
			sleep(5);
		pv(sem_id, 1);
		exit(0);
	}
	else
	{
		printf("parent try to get binary sem\n");
		pv(sem_id, -1);
		printf("parent get the sem and would release it after 5
			seconds\n");
			sleep(5);
		pv(sem_id, 1);
	}
	waitpid(id, NULL, 0);//回收子进程,防止僵尸进程;
	semctl(sem_id, 0, IPC_RMID, sem_un);/*删除信号量*/
	return 0;
}

POSIX 信号量函数 sem_ *

#include<semaphore.h>
/*
pshared参数指定信号量的类
型。如果其值为0,就表示这个信号量是当前进程的局部信号量,否则
该信号量就可以在多个进程之间共享。value参数指定信号量的初始
值。
*/
int sem_init(sem_t*sem,int pshared,unsigned int value);
int sem_destroy(sem_t*sem);
int sem_wait(sem_t*sem);
int sem_trywait(sem_t*sem);
/*
sem_post函数以原子操作的方式将信号量的值加1。当信号量的值
大于0时,其他正在调用sem_wait等待信号量的线程将被唤醒。
*/
int sem_post(sem_t*sem);

共享内存

shmget

shmget系统调用创建一段新的共享内存,或者获取一段已经存在的共享内存。

#include<sys/shm.h>
/*
key 参数是一个键值,用来标识一段全局唯一的共享内存。
size 参数指定共享内存的大小,单位是字节。创建新的共享内存,则size值必须被指定
shmflg 参数的使用和含义与 semget 系统调用的 sem_flags 参数相同。支持两个额外标志,见后文;
shmflg 支持的两个额外的标志
	❑SHM_HUGETLB,类似于 mmap 的 MAP_HUGETLB 标志,系统将使用“大页面”来为共享内存分配空间。
	❑SHM_NORESERVE,类似于 mmap 的 MAP_NORESERVE 标志,不为共享内存保留交换分区(swap空间)。这样,当物理内存不足的时候,对该共享内存执行写操作将触发 SIGSEGV 信号。
shmget成功时返回一个正整数值,它是共享内存的标识符。shmget失败时返回-1,并设置errno。
*/
int shmget(key_t key,size_t size,int shmflg);

//当共享内存被创建时其有关的数据结构将被创建并初始化
struct shmid_ds
{
	struct ipc_perm shm_perm;/*共享内存的操作权限*/
	size_t shm_segsz;/*共享内存大小,单位是字节*/
	__time_t shm_atime;/*对这段内存最后一次调用shmat的时间*/
	__time_t shm_dtime;/*对这段内存最后一次调用shmdt的时间*/
	__time_t shm_ctime;/*对这段内存最后一次调用shmctl的时间*/
	__pid_t shm_cpid;/*创建者的PID*/
	__pid_t shm_lpid;/*最后一次执行shmat或shmdt操作的进程的PID*/
	shmatt_t shm_nattach;/*目前关联到此共享内存的进程数量*/
	/*省略一些填充字段*/
};
shmat、shmdt

共享内存被创建/获取之后,我们不能立即访问它,而是需要先将它关联到进程的地址空间中。使用完共享内存之后,我们也需要将它从进程地址空间中分离。

#include<sys/shm.h>
/*
shm_id 参数是由shmget调用返回的共享内存标识符;
shm_addr参数指定将共享内存关联到进程的哪块地址空间, 推荐为 NULL。
shmflg参数:
❑SHM_RDONLY。进程仅能读取共享内存中的内容。
❑SHM_REMAP。如果地址shmaddr已经被关联到一段共享内存上,则重新关联。
❑SHM_EXEC。它指定对共享内存段的执行权限。
shmat成功时返回共享内存被关联到的地址,失败则返回 (void*)-1 并设置errno。
*/
void*shmat(int shm_id,const void*shm_addr,int shmflg);

/*
shmdt函数将关联到shm_addr处的共享内存从进程中分离。
它成功时返回0,失败则返回-1并设置errno。
*/
int shmdt(const void*shm_addr);
shmctl

shmctl系统调用控制共享内存的某些属性。

#include<sys/shm.h>
/*
shm_id 参数是由shmget调用返回的共享内存标识符。
command 参数指定要执行的命令。
shmctl 成功时的返回值取决于command参数。
*/
int shmctl(int shm_id,int command,struct shmid_ds*buf);

shmctl 支持的命令
在这里插入图片描述

shm_open、shm_unlink

Linux提供了另外一种利用mmap在无关进程之间共享内存的方式。这种方式无须任何文件的支持,但它需要先使用如下函数来创建或打开一个POSIX共享内存对象。shm_open的使用方法与open系统调用完全相同。

#include<sys/mman.h>
#include<sys/stat.h>
#include<fcntl.h>
/*
name 参数指定要创建/打开的共享内存对象。
oflag参数指定创建方式:
	❑O_RDONLY。以只读方式打开共享内存对象。
	❑O_RDWR。以可读、可写方式打开共享内存对象。
	❑O_CREAT。如果共享内存对象不存在,则创建之。
	❑O_EXCL。和O_CREAT一起使用,如果由name指定的共享内存对象已经存在,则shm_open调用返回错误,否则就创建一个新的共享内存对象。
	❑O_TRUNC。如果共享内存对象已经存在,则把它截断,使其长度为0。
shm_open调用成功时返回一个文件描述符。该文件描述符可用于后续的mmap调用,从而将共享内存关联到调用进程。
*/
int shm_open(const char*name,int oflag,mode_t mode);
/*
和打开的文件最后需要关闭一样,由shm_open创建的共享内存对象使用完之后也需要被删除。
*/
int shm_unlink(const char*name);

如果代码中使用了上述POSIX共享内存函数,则编译的时候需要指定链接选项-lrt。

消息队列

msgget

msgget系统调用创建一个消息队列,或者获取一个已有的消息队列。

#include<sys/msg.h>
/*
key 参数是一个键值,用来标识一个全局唯一的消息队列。
msgflg 参数的使用和含义与semget系统调用的sem_flags参数相同。
msgget 成功时返回一个正整数值,它是消息队列的标识符。msgget 失败时返回-1,并设置errno。
*/
int msgget(key_t key,int msgflg);
/*
如果msgget用于创建消息队列,则与之关联的内核数据结构msqid_ds将被创建并初始化。
*/
struct msqid_ds
{
	struct ipc_perm msg_perm;/*消息队列的操作权限*/
	time_t msg_stime;/*最后一次调用msgsnd的时间*/
	time_t msg_rtime;/*最后一次调用msgrcv的时间*/
	time_t msg_ctime;/*最后一次被修改的时间*/
	unsigned long__msg_cbytes;/*消息队列中已有的字节数*/
	msgqnum_t msg_qnum;/*消息队列中已有的消息数*/
	msglen_t msg_qbytes;/*消息队列允许的最大字节数*/
	pid_t msg_lspid;/*最后执行msgsnd的进程的PID*/
	pid_t msg_lrpid;/*最后执行msgrcv的进程的PID*/
};
msgsnd

msgsnd系统调用把一条消息添加到消息队列中。

#include<sys/msg.h>
/*
msqid 参数是由msgget调用返回的消息队列标识符。
msg_ptr 参数指向一个准备发送的消息。
msg_sz 参数是消息的数据部分(mtext)的长度。
msgflg 参数控制msgsnd的行为。它通常仅支持IPC_NOWAIT标志,
即以非阻塞的方式发送消息。默认情况下,发送消息时如果消息队列
满了,则msgsnd将阻塞。
msgsnd成功时返回0,失败则返回-1并设置errno。msgsnd成功时将
修改内核数据结构msqid_ds的部分字段
*/
int msgsnd(int msqid,const void*msg_ptr,size_t msg_sz,int msgflg);
//消息类型
struct msgbuf
{
	long mtype;/*消息类型*/
	char mtext[512];/*消息数据*/
};

处于阻塞状态的msgsnd调用可能被如下两种异常情况所中断:
❑消息队列被移除。此时msgsnd调用将立即返回并设置 errno 为 EIDRM。
❑程序接收到信号。此时msgsnd调用将立即返回并设置 errno 为 EINTR。

msgrcv

msgrcv 系统调用从消息队列中获取消息。

#include<sys/msg·h>
/*
msqid 参数是由 msgget 调用返回的消息队列标识符。
msg_ptr 参数用于存储接收的消息。
msg_sz 参数指的是消息数据部分的长度。
msgtype 参数指定接收何种类型的消息。以下方式指定消息类型:
	❑msgtype等于0。读取消息队列中的第一个消息。
	❑msgtype大于0。读取消息队列中第一个类型为msgtype的消息
	❑msgtype小于0。读取消息队列中第一个类型值比msgtype的绝对值小的消息。
参数msgflg控制msgrcv函数的行为。
	❑IPC_NOWAIT。如果消息队列中没有消息,则 msgrcv 调用立即返回并设置 errno 为 ENOMSG。
	❑MSG_EXCEPT。如果 msgtype 大于 0,则接收消息队列中第一个非 msgtype 类型的消息。
	❑MSG_NOERROR。如果消息数据部分的长度超过了msg_sz,就将它截断。
msgrcv成功时返回0,失败则返回-1并设置errno。
*/
int msgrcv(int msqid,void*msg_ptr,size_t msg_sz,long int msgtype, int msgflg);

处于阻塞状态的 msgrcv 调用还可能被如下两种异常情况所中断:
❑消息队列被移除。此时 msgrcv 调用将立即返回并设置 errno 为 EIDRM。
❑程序接收到信号。此时 msgrcv 调用将立即返回并设置 errno 为 EINTR。

msgctl

msgctl系统调用控制消息队列的某些属性。

#include<sys/msg.h>
/*
msqid 参数是由 msgget 调用返回的共享内存标识符。command 参数指定要执行的命令。
command 参数指定要执行的命令。
msgctl 成功时的返回值取决于command参数
*/
int msgctl(int msqid,int command,struct msqid_ds*buf);

command 命令:
在这里插入图片描述

多线程

pthread_create
#include<pthread.h>
//线程标识符
typedef unsigned long int pthread_t;
/*
thread 参数是新线程的标识符,后续pthread_*函数通过它来引用新线程。
attr 参数用于设置新线程的属性。给它传递 NULL 表示使用默认线程属性。
start_routine 和 arg 参数分别指定新线程将运行的函数及其参数。
pthread_create成功时返回0,失败时返回错误码。
*/
int pthread_create(pthread_t*thread, const
pthread_attr_t*attr, void*(*start_routine)(void*), void*arg);
pthread_exit

线程一旦被创建好,内核就可以调度内核线程来执行start_routine函数指针所指向的函数了。线程函数在结束时最好调用如下函数,以确保安全、干净地退出。

#include<pthread.h>
/*
pthread_exit函数通过retval参数向线程的回收者传递其退出信息。它执行完之后不会返回到调用者,而且永远不会失败。
*/
void pthread_exit(void*retval);
pthread_join

一个进程中的所有线程都可以调用 pthread_join 函数来回收其他线程。

#include<pthread.h>
/*
thread 参数是目标线程的标识符,retval 参数则是目标线程返回的退出信息。
该函数会一直阻塞,直到被回收的线程结束为止。该函数成功时返回0,失败则返回错误码。
*/
int pthread_join(pthread_t thread, void**retval);

该函数可能引发的错误码:
在这里插入图片描述

pthread_cancel
#include<pthread.h>
/*
thread参数是目标线程的标识符。
*/
int pthread_cancel(pthread_t thread);

接收到取消请求的目标线程可以决定是否允许被取消以及如何取消,这分别由如下两个函数完成:

#include<pthread.h>
/*
state 参数为取消状态(是否允许取消)
	❑PTHREAD_CANCEL_ENABLE,允许线程被取消。它是线程被创建时的默认取消状态。
	❑PTHREAD_CANCEL_DISABLE,禁止线程被取消。
oldstate 参数为原来的状态
*/
int pthread_setcancelstate(int state,int*oldstate);
/*
type 参数为取消类型:
	❑PTHREAD_CANCEL_ASYNCHRONOUS,线程随时都可以被取消。
		它将使得接收到取消请求的目标线程立即采取行动。
	❑PTHREAD_CANCEL_DEFERRED,允许目标线程推迟行动,直
		到它调用了下面几个所谓的取消点函数中的一个:pthread_join、
		pthread_testcancel、pthread_cond_wait、pthread_cond_timedwait、
		sem_wait和sigwait。根据POSIX标准,其他可能阻塞的系统调用,比如
		read、wait,也可以成为取消点。
oldtype 参数为以前的类型;
*/
int pthread_setcanceltype(int type,int*oldtype);
pthread_mutex_ *
#include<pthread.h>
/*初始化互斥锁*/
int pthread_mutex_init(pthread_mutex_t*mutex,const
										pthread_mutexattr_t*mutexattr);
/*互斥锁销毁*/
int pthread_mutex_destroy(pthread_mutex_t*mutex);
/*上锁*/
int pthread_mutex_lock(pthread_mutex_t*mutex);
/* 非阻塞上锁, 如果上锁失败则将返回错误码 EBUSY */
int pthread_mutex_trylock(pthread_mutex_t*mutex);
/*释放锁*/
int pthread_mutex_unlock(pthread_mutex_t*mutex);

这些函数的第一个参数 mutex 指向要操作的目标互斥锁,互斥锁的
类型是 pthread_mutex_t 结构体。
更常见的互斥锁初始化方式如下:

/*宏PTHREAD_MUTEX_INITIALIZER实际上只是把互斥锁的各个
 字段都初始化为0。*/
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutexattr_t

互斥锁属性折设置函数

#include<pthread.h>
/*初始化互斥锁属性对象*/
int pthread_mutexattr_init(pthread_mutexattr_t*attr);
/*销毁互斥锁属性对象*/
int pthread_mutexattr_destroy(pthread_mutexattr_t*attr);
/*获取和设置互斥锁的pshared属性*/
int pthread_mutexattr_getpshared(const pthread_mutexattr_t*attr,int*pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t*attr,int pshared);
/*获取和设置互斥锁的type属性*/
int pthread_mutexattr_gettype(const
	pthread_mutexattr_t*attr,int*type);
int pthread_mutexattr_settype(pthread_mutexattr_t*attr,int
type);

两种常用属性 pshared 和 type。
pshared 指定是否允许跨进程共享互斥锁:
❑PTHREAD_PROCESS_SHARED,互斥锁可以被跨进程共享。
❑PTHREAD_PROCESS_PRIVATE,互斥锁只能被和锁的初始化线程隶属于同一个进程的线程共享。
互斥锁属性type指定互斥锁的类型。
❑PTHREAD_MUTEX_NORMAL,普通锁。
❑PTHREAD_MUTEX_ERRORCHECK,检错锁。一个线程如果对一个已经加锁的检错锁再次加锁,则加锁操作返回EDEADLK。对一个已经被其他线程加锁的检错锁解锁,或者对一个已经解锁的检错锁再次解锁,则解锁操作返回EPERM。
❑PTHREAD_MUTEX_RECURSIVE,嵌套锁。
❑PTHREAD_MUTEX_DEFAULT,默认锁。

pthread_cond_ *

如果说互斥锁是用于同步线程对共享数据的访问的话,那么条件变量则是用于在线程之间同步共享数据的值。条件变量提供了一种线程间的通知机制:当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程。

#include<pthread.h>
/*
初始化条件变量
cond_attr 参数指定条件变量的属性;
*/
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t*cond,const
	pthread_condattr_t*cond_attr);
/*销毁条件变量*/
int pthread_cond_destroy(pthread_cond_t*cond);
/*
以广播的方式唤醒所有等待目标条件变量的线程。
*/
int pthread_cond_broadcast(pthread_cond_t*cond);
/*
于唤醒一个等待目标条件变量的线程。至于哪个线程将被唤醒,则取决于线程的优先级和调度策略。
*/
int pthread_cond_signal(pthread_cond_t*cond);
/*
pthread_cond_wait函数用于等待目标条件变量。mutex参数是用于保护条件变量的互斥锁,以确保pthread_cond_wait操作的原子性。
*/
int
pthread_cond_wait(pthread_cond_t*cond,pthread_mutex_t*mutex);

这些函数的第一个参数cond指向要操作的目标条件变量,条件变量的类型是pthread_cond_t结构体。

pthread_atfork

父进程中已经被加锁的互斥锁在子进程中也是被锁住的。这就引起了一个问题:子进程可能不清楚从父进程继承而来的互斥锁的具体状态(是加锁状态还是解锁状态)。这个互斥锁可能被加锁了,但并不是由调用fork函数的那个线程锁住的,而是由其他线程锁住的。如果是这种情况,则子进程若再次对该互斥锁执行加锁操作就会导致死锁。pthread提供了一个专门的函数pthread_atfork,以确保fork调用后父进程和子进程都拥有一个清楚的锁状态。

#include<pthread.h>
/*
prepare 函数指针,用于在创建子进程之前将进程的所有的互斥锁锁住。
parent 函数指针,用于在创建出子进程后将所有父进程的互斥锁释放。
child 函数指针,用于在创建出子进程后将所有子进程中的互斥锁释放;
*/
int pthread_atfork(void(*prepare)(void),void(*parent) (void),void(*child)(void));
void prepare()
{
	pthread_mutex_lock(&mutex);
}
void infork()
{
	pthread_mutex_unlock(&mutex);
}
pthread_atfork(prepare,infork,infork);
pthread_sigmask、sigwai、pthread_kill

在多线程环境下我们应该使用如下所示的pthread版本的sigprocmask函数来设置线程信号掩码 :

#include<pthread.h>
#include<signal.h>
int pthread_sigmask(int how,const sigset_t*newmask,sigset_t*oldmask);

由于进程中的所有线程共享该进程的信号,所以线程库将根据线程掩码决定把信号发送给哪个具体的线程。因此,如果我们在每个子线程中都单独设置信号掩码,就很容易导致逻辑错误。此外,所有线程共享信号处理函数。也就是说,当我们在一个线程中设置了某个信号的信号处理函数后,它将覆盖其他线程为同一个信号设置的信号处理函数。这两点都说明,我们应该定义一个专门的线程来处理所有的信号。
我们可以明确地将一个信号发送给指定的线程:

#include<signal.h>
int pthread_kill(pthread_t thread,int sig);
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值