TCP/IP网络编程基础阅读笔记
TCP/IP网络编程基础阅读笔记
open函数
该函数打开一个文件(linux下任何皆文件),返回文件描述符,失败返回-1
int open(const char* pathname,int flags,mode_t mode)
flags:文件打开方式的标志 O_RDONLY:只读方式打开 O_WRONLY:只写方式打开 O_RDWR: 可读可写方式打开 O_CREAT:若打开的文件不存在则创建文件 O_APPEND:追加方式打开 O_TRUNC:若文件存在且可写方式打开则将文件长度清0 mode:打开文件的存取权限,只有在创建文件时才有效。也就是创建文件时文件的权限
close函数
关闭文件描述符打开的文件
#include<unistd.h>
int close(int fd)
read函数
从文件读取指定数量的字节数据存入传入的buf区域
#include<unistd.h>
ssize_t read(int fd, void *buf, size_t count)
fd:文件描述符 buf:指定用来存储所读数据的缓冲区 count:指定要读取的字节数 返回实际读取的字节数,返回0代表到达文件结尾,返回-1调用出错
write函数
#include<unistd.h>
ssize_t write(int fd,void *buf, size_t count)
同write函数
lseek函数
off_t lseek(int fd, off_t offset, int whence)
offset:偏移量,可正可负,指相对于当前的偏移量 whence:表示当前基点位置 SEEK_SET:基点为当前文件的开头 SEEK_CUR:基点为当前文件指针的位置 SEEK_END:基点为当前文件的文件末
ioctl函数
函数用于设置I/O的特性
#include<sys/ioctl.h>
int ioctl(int fd,int cmd,...)
通用地址存储结构
struct sockaddr{
u_char sa_len;//sockaddr的长度
u_short sa_family;//地址族
char sa_data[14];//14字节协议端点信息
};
#include<netinet/in.h>
struct sockaddr_in{
short int sin_family;//地址族
unsigned short int sin_port;//端口号
struct in_addr sin_addr;//存储ip地址的结构
unsigned char sin_zero[8];//填充0以保持sockaddr相同大小
};
ipv4地址结构
struct in_addr{
unsigned long s_addr;//ip地址
};
htons函数
将主机字节顺序转换为网络字节顺序(host-to-network-for type short)
uint16_t htons(uint16_t hostshort)
htonl函数
将主机字节顺序转换为网络字节顺序(host-to-network-for type long)
uint32_t htonl(uint32_t hostlong)
ntohs函数
将网络字节顺序转换为主机字节顺序(network-to-host-for type short)
uint16_t ntohs(uint16_t netshort)
ntohl函数
将网络字节顺序转换为主机字节顺序(network-to-host-for type long)
uint32_t ntohl(uint32_t netlong)
inet_addr函数
#include<arpa/inet.h>
in_addr_t inet_addr(const char *cp)
将点分十进制的ip地址转换为32位二进制表示的网络字节顺序的地址。出错返回-1
#include<arpa/inet.h>
int inet_aton(const char*cp, struct in_addr *inp)
将一个用点分十进制的in_addr结构转换为二进制后存储在inp中 inp:一个用二进制表示的32位的ip地址结构
#include<arpa/inet.h>
char * inet_ntoa(struct in_addr *inp)
将二进制表示的ip地址转换为点分十进制的地址
gethostbyname函数
通过域名查询ip地址
#include<netdb.h>
struct hostent * gethostbyname(const char *name)
struct hostenv{
char *h_name;//主机名
char **h_aliases;//主机别名
char h_addrtype;//主机ip地址类型ipv4(AF_INET),ipv6(AF_INET6)
char h_length;//ip地址长度
char ** h_addr_list;//以网络字节顺序存储主机ip地址列表(一个主机可能有多个ip地址)
};
getservbyname函数
通过服务名查找端口号
#include<netdb.h>
struct servent * getservbyname(const char *name,const char * proto)
struct servent{
char *s_name;//主机名
char * *s_aliases;//主机别名
short s_port;//端口
char * s_proto;//协议名
};
getprotobyname函数
根据协议名查找协议号
#include<netdb.h>
struct protoenv * getprotobyname(const char *name)
struct protoenv{
char *p_name;//协议名
char * * s_aliases;//主机别名
int p_proto;//协议号
};
套接字API
socket函数 TCP/UDP
创建套接字,返回套接字描述符。
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain, int type, int protocol)
domain:协议族。通常赋值为PF_INET,表示TCP/IP协议族 AF_INET:ipv4协议(和PF_ANET同值) AF_INET6:ipv6协议 AF_LOCAL:Unix域协议 AF_ROUTR:路由套接字 AF_KEY:**套接字 type:SOCK_STREAM (TCP)、SOCK_DGRAM (UDP)、SOCK_RAW (原始套接字) protocol:协议号。通常赋值为0。在前两种无法区分协议时通过这个参数来区分
connect函数 TCP
用于配置socket并与远端服务器建立一个TCP连接
#include<sys/types.h>
#include<sys/socket.h>
int connect(int sockfd, struct sockaddr * serv_addr, int addrlen)
serv_addr:服务端地址 addrlen:sockaddr结构的长度 sizeof运算符计算
bind函数 TCP/UDP
用于将socket和本地端点地址相关联,调用成功返回一个整型数值,失败返回-1
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr,int addrlen)
my_addr:指向本地端点地址的结构。 addrlen:sizeof(struct sockaddr) 计算得到
listen函数 TCP
用于将socket处于被动的监听状态,并为该socket建立一个输入数据队列。
#include<sys/types.h>
#include<sys/socket.h>
int listen(int sockfd, int backlog)
backlog:指定允许在等待队列中所允许的连接数。
accept函数 TCP
从等待队列中抽取第一个连接,并为该连接创建一个新的套接字。成功返回一个新的套接字描述符,失败返回-1。若为阻塞模式,accept函数将等到等待队列中有连接时才返回。
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd, struct sockaddr * addr, socklen_t *addrlen)
addr:用于存储接受到连接的套接字地址 addrlen:远端地址的长度。(注意时传入的指针)
send函数 TCP
用于给TCP连接的另一端发送数据。成功返回实际发送的字节数,失败返回-1
#include<sys/types.h>
#include<sys/socket.h>
ssize_t send(int sockfd,const void * buf, size_t len, int flags)
buf:要发送的数据的buf len:发送的数据长度 flags:调用方式,一般传0
recv函数 TCP
从TCP连接的另一端获取传过来的数据。成功返回读取的实际字节数,失败返回-1。
#include<sys/types.h>
#include<sys/socket.h>
int recv(int sockfd, void *buf, int len, unsigned int flags)
同send
sendto函数 UDP
用于UDP发送数据。成功返回实际发送的字节数,失败返回-1
#include<sys/types.h>
#include<sys/socket.h>
int sendto(int sockfd,const void * msg,int len,unsigned int flags,const struct sockaddr * to,int to_len)
msg:发送的数据的缓冲区 len:缓冲区长度 flags:一般设置为0 to:对端端点地址 to_len:对端端点地址的长度
recvfrom函数 UDP
用于UDP接收数据。成功返回实际接收的字节数,失败返回-1
#include<sys/types.h>
#include<sys/socket.h>
int recvfrom(int sockfd, void *buf,int len,unsigned int flags,struct sockaddr * from,int fromlen)
同sendto
close函数 TCP/UDP
关闭套接字。成功返回0,失败返回-1
#include<unistd.h>
#include<sys/socket.h>
int close(int sockfd)
shutdown函数
用于套接字某个方向上关闭数据传输,而另一个方向上的传输任然可以继续进行
#include<sys/types.h>
#include<sys/socket.h>
int shutdown(int sockfd,int howto)
howto: 0:仅关闭读。套接字不再接收任何数据,且丢弃当前缓冲区的所有数据 1:仅关闭写。套接字将缓冲区的数据发送完后,进程将不能够再对该套接字调用写函数 2:同时关闭读写。和close函数类似。与close不同的是当多个进程共享套接字的时候如果调用shutdown函数,那么所有的进程都会受到影响。close函数则只会影响调用的那个进程
getpeername函数
获取连接的远端对等套接字的名称。成功返回0,失败返回-1
#include<sys/types.h>
#include<sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, int * addrlen)
addr:用于存储远端对等套接字的端点地址 addrlen:sizeof(struct sockaddr)获得
setsockopt函数
设置套接字的相关参数,成功返回0,失败返回-1
#include<sys/types.h>
#include<sys/socket.h>
int setsockopt(int sockfd,int level, int optname, const void *optval,socklen_t optlen)
level:指定选项所在协议层。为了设置套接字层选项,设置为SOL_SOCKET。 optname:选项名。如下表 optval:选项的值 optlen:选项值的长度
选项名称 | 说明 | 数据类型 |
---|---|---|
SO_BROADCAST | 允许发送广播数据 | int |
SO_DEBUG | 允许调试 | int |
SO_DONTROUTE | 不查找路由 | int |
SO_ERROR | 获得套接字错误 | int |
SO_KEEPALIVE | 保持连接 | int |
SO_LINGER | 延迟关闭连接 | struct linger |
SO_OOBINLINE | 带外数据放入正常数据流 | int |
SO_RCVBUF | 接收缓冲区大小 | int |
SO_SNDBUF | 发送缓冲区大小 | int |
SO_RCVLOWAT | 接收缓冲区下限 | int |
SO_SNDLOWAT | 发送缓冲区下限 | int |
SO_RCVTIMEO | 接收超时 | struct timeval |
SO_SNDTIMEO | 发送超时 | struct timeval |
SO_REUSERADDR | 允许重用本地地址和端口 | int |
SO_TYPE | 获得套接字类型 | int |
SO_BSDCOMPAT | 与BSD系统兼容 | int |
注意:设置套接字缓冲区大小时要注意函数调用顺序,因为设置的参数生效时是在套接字建立连接的时候,建立连接就需要协商两边的窗口等信息。所以客户端要在connect函数之前调用,服务端要在listen函数之前调用
getsockopt函数
获取套接字的参数信息,成功返回0,出错返回-1
#include<sys/types.h>
#include<sys/socket.h>
int getsockopt(int sockfd,int level, int optname, const void *optval,socklen_t optlen)
同setsockopt函数
相关辅助函数
memset函数
对一段内存进行赋值或者清空
#include<mem.h>
void * memset(void *desmem,int val,size_t n)
desmem:操作的内存地址 val:要被赋予的值 n:要赋值的长度,单位字节
atoi函数
将字符串转化为整型数值,成功返回转换后的值,失败返回0
#include<stdlib.h>
int atoi(const char *str)
time函数
获取当前时间戳。成功返回GTM自1970年1月1日 00:00:00以来的秒数,失败返回0
#include<time.h>
time_t time(time_t *time)
time:用来存储获取到的结果的指针。也可用返回值来接收结果
ctime函数
用于将日历时间转换为字符串形式的本地时间
#include<time.h>
char *ctime(const time_t * timer)
timer:存储当前日历时间的指针,一般由time()函数获得。
strerror函数
返回错误编号对应的错误原因的字符串描述结果
#include<string.h>
char * strerror(int errnum)
errnum:错误编号
可变参数函数
#include<stdarg.h>
void va_start(va_list ap,argN)
void va_copy(va_list *dest,va_list src)
type va_arg(va_list ap,type)
void va_end(va_list ap)
使用范例
#include<stdarg.h>
#include<stdio.h>
int add(int a1,...);
int main()
{
int a1 = 1,a2 = 2,a3 = 3,a4 = 4,end = -1;
printf("sum is %d\n",add(a1,a2,a3,a4,end));
}
int add(int a1, ...)
{
va_list args;
int sum = a1;
va_start(args,a1);
int num = 0;
for (;;)
{
num = va_arg(args, int);
if(num != -1){
sum += num;
}else{
break;
}
}
va_end(args);
return sum;
}
C/S通信模型
UDP通信模型
graph TB
socket-->bind
bind-->recvfrom
recvfrom-->阻塞等待客户数据
阻塞等待客户数据-->处理请求
处理请求-->sendto
sendto-->close
socket1-->sendto1
sendto1-->recvfrom1
recvfrom1-->close1
sendto1-.服务请求.->阻塞等待客户数据
sendto-.服务应答.->recvfrom1
TCP模型
graph TB
socket-->bind
bind-->listen
listen-->accept
accept-->阻塞等待客户数据
阻塞等待客户数据-->read
read-->处理请求
处理请求-->write
write-->close
socket1-->connect
connect-->write1
write1-->read1
read1-->close1
connect-.建立连接.->阻塞等待客户数据
write1-.请求数据.->read
write-.应答数据.->read1
UDP循环服务器
- 调用socket()函数创建UDP套接字
- 调用bind()函数将套接字绑定到本地可用端点地址
- while(1)死循环
- 循环体内
- 调用recvfrom()函数读取客户请求
- 处理数据
- 调用sendto()函数返回数据给客户
TCP循环服务器
- 调用socket()函数创建TCP套接字
- 调用bind()函数将套接字绑定到本地可用端点地址
- 调用listen()函数将套接字设为被动模式
- while(1)死循环
- 调用accept()函数接收客户请求并建立一个处理该连接的临时套接字
- 调用recv/send()函数进行数据相互传递
- 交互完毕,关闭临时套接字(accept返回的套接字)
linux下的服务器并发机制
进程相关
fork函数
linux下用于创建进程。失败返回-1,成功在父进程中返回子进程的实际pid,在子进程中返回0。
#include<unistd.h>
int fork();
示例
#include<unistd.h>
int main()
{
int a = 0;
pid_t p;
p = fork();
if(p == -1){
fprintf(stderr,"创建进程失败\n");
}else{
if(p == 0){
a += 2;
fprintf(stdout,"子进程:%d",a);
}else{
a += 4;
fprintf(stdout,"父进程:%d",a);
}
}
return 0;
}
getpid函数
获取当前进程的id号
getppid函数
获取当前进程的父进程id号
#include<unistd.h>
pid_t getpid(void);
pid_t getppid(void);
僵尸进程的避免
- 父进程调用wait()或waitpid()等函数等待子进程结束,但是这会使父进程挂起(进入阻塞或等待状态)
- 如果父进程很忙不能被挂起,可以调用signal()函数为SIGCHLD信号安装handler来避免。当子进程结束后,内核发送SIGCHLD信号给其父进程,父进程收到信号后则可在handler中调用wait()函数来进行回收。
- 如果父进程不关心子进程何时结束,则可调用signal(SIGCHLD,SIG_IGN)函数通知内核,让子进程在结束时由内核自动回收,且内核不会给父进程发送SIGCHLD信号。
- 当父进程结束后,子进程就成了孤儿进程。从而会被过继给1号进程init。init进程主要负责系统启动服务以及子进程的清理回收。所以当过继给init进程的子进程结束后也会自动回收。
wait()函数
进程一旦调用wait()函数就会立即阻塞自己。由wait()函数自动分析当前进程的某个子进程已经退出,如果找到这样一个已经变成僵尸的子进程,waith()函数就会收集该子进程的信息并进行回收,如果没有找到这样的子进程,那么就会一直阻塞,直到出现为止。调用成功返回被收集的子进程的进程ID,失败返回-1
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int * status)
status:用来保存子进程退出时的状态。可以通过调用一下宏来判断子进程结束状况: WIFEXITED(status):子进程正常结束该宏返回非0值 WEXITSTATUS(status):若子进程正常结束,用这个宏可以获得子进程由exit()返回的结束代码 WIFSIGNALED(status):子进程因为信号而结束则该宏返回非0值 WTERMSIG(status):若子进程因为信号结束则该宏可以获得子进程中止信号代码 WIFSTOPPEN(status):子进程处于暂停执行状态则返回非0值 WSTOPSIG(status):若子进程处于暂停执行状态怎该宏获得引发暂停状态的信号代码
waitpid()函数
waitpid()会像wait()一样阻塞父进程直到子进程退出。waitpid()正常的时候返回子进程pid,如果设置了WNHAND选项,如果调用waitpid时没有子进程可收集,那么返回0。出错返回-1
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid,int *status, int options)
pid:需要等待的那个子进程号 pid>0:只有等待到的子进程号等于pid时,waitpid()才停止阻塞父进程。 pid=-1:等待任何一个子进程退出就停止阻塞。此时等价wait() pid=0:等待同一组中的任何子进程。 pid<-1:等待指定组中的任何一个子进程,组id为与pid的绝对值。 status:被收集的子进程退出状态 options:主要含有以下参数,多个参数可以相与。 WNOHANG:子进程没有退出,waitpid也立即返回 WUNTRACED:当子进程处于暂停状态waitpid立即返回 0:相当于不设特殊参数
signal()函数
用于绑定收到指定信号的处理函数
#include<signal.h>
void (* signal(int signum,void (*handler)(int)))(int)
signum:信号编号 handler: void (*)(int)类型的函数名,当收到signum信号时执行handler函数 SIG_IGN:忽略信号 SIG_DFL:恢复成系统信号的默认处理
线程相关
gcc编译时需要连接上线程库 gcc -lpthread xxx.c -o xxx
pthread_create()函数
创建一个新线程,成功返回0,失败返回非0。
#include<pthread.h>
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(* start_routine)(void *), void *arg)
thread:创建的线程标识符 attr:线程运行属性的结构体 start_routine:参数类型时void *返回值也是void *的函数指针。这个是线程执行的函数体。 arg:传递给线程体函数的参数 typedef struct{ int detachstate; //分离状态 int schedpolicy; //线程调度策略 struct sched_param schedparam; //线程调度参数 int inheritsched; //线程继承性 int scope; //线程作用域 size_t guardsize;//线程堆栈保护区大小 int stackaddr_set;//线程堆栈地址集 void * stackaddr;//线程堆栈地址 size_t stacksize;//线程堆栈大小 }pthread_attr_t;
LINUX下可以通过下面这些函数设置线程的运行状态。
#include<pthread.h>
int pthread_attr_init(pthread_attr_t * attr)//初始化结构体
int pthread_attr_destroy(pthread_attr_t * attr)//去初始化结构体
//设置/获取线程分离状态
int pthread_attr_getdetachstate(const pthread_attr_t *attr,int *detachstate)
int pthread_attr_setdetachstate(const pthread_attr_t *attr,int detachstate)
//成功返回0,失败返回-1
/***
detachstate:
PTHREAD_CREATE_DETACHED:以分离状态运行
PTHREAD_CREATE_JOINABLE:非分离状态运行(默认)
*/
其他的set,get函数类似
pthread_exit()函数
终止一个进程
#include<pthread.h>
int pthread_exit(void * value_ptr)
int pthread_join(pthread_t thread, void ** value_ptr)
exit中的value_ptr:线程返回值指针,该返回值将被传递给另一个线程,可以通过调用pthread_join()函数获取 thread:等待结束的进程标识符 join中value_ptr:如果不为NULL,那么线程thread的返回值将存储在指针指向的位置。
pthread_self()函数
获取线程标识符
#include<pthread.h>
pthread_t phread_self()
pthread_detach()函数
将线程设置成分离线程。成功返回0,失败返回错误号
int pthread_detach(pthread_t thread)
不固定数量的进程并发模型
- 主进程创建套接字msock并绑定到熟知端口
- 主进程调用accept()函数等待客户连接的到达
- 当有客户连接时,主进程建立与客户端的通信连接,同时accept()函数返回新的套接字ssock
- 主进程创建新的从进程来处理ssock
- 主进程调用close()函数将ssock的引用计数减一
- 主进程返回步骤2
- 从进程关闭msock(close()函数,减少引用计数,shutdown()函数才是直接关闭套接字的)
- 从进程调用recv/send函数进行客户端的数据处理
- 从进程处理完毕,关闭ssock,结束进程。
固定进程数的并发模型
- 父进程
- 主进程创建主套接字msock,并绑定到熟知端口
- 主进程创建给定的数量的从进程
- 主进程调用wait()函数等待从进程结束,一旦有从进程退出,则调用fork()创建新的进程,以保持数量不变
- 从进程
- 从进程调用accept()等待客户连接到达
- 当有客户连接时,从进程建立与客户端的通信连接,同时accept()函数返回新的套接字ssock
- 从进程调用recv/send函数处理
- 处理完毕,从进程关闭套接字
互斥锁
互斥锁就是排他锁,一个时间只有一个线程能拥有该锁
互斥锁有三种:快速互斥锁,递归互斥锁,检错互斥锁。
快速互斥锁:调用的线程会阻塞直到解锁为止。
递归互斥锁:能成功的返回并增加调用线程在互斥锁上的加锁次数
检错互斥锁:调用的线程不会被阻塞,它会立刻返回一个错误信息
操作步骤
graph TB
定义互斥锁变量pthread_mutex_t-->定义互斥锁属性pthread_mutexattr_t
定义互斥锁属性pthread_mutexattr_t-->初始化互斥锁属性变量pthread_mutexattr_init
初始化互斥锁属性变量pthread_mutexattr_init-->设置互斥锁属性pthread_mutexattr_setXXX
设置互斥锁属性pthread_mutexattr_setXXX-->初始化互斥锁变量pthread_mutex_init
初始化互斥锁变量pthread_mutex_init-->互斥锁上锁pthread_mutex_lock
互斥锁上锁pthread_mutex_lock-->互斥锁判断上锁pthread_mutex_trylock
互斥锁判断上锁pthread_mutex_trylock-->互斥锁解锁pthread_mutex_unlock
互斥锁解锁pthread_mutex_unlock-->消除互斥锁pthread_mutex_destroy
相关函数原型
#include<pthread.h>
/**
初始化互斥锁变量
*/
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *mutexattr)
/**
初始化互斥锁属性变量
*/
int pthread_mutexattr_init(pthread_mutexattr_t *mattr)
/**
释放互斥锁属性变量
*/
int pthread_mutexattr_destroy(pthread_mutexattr_t *mattr)
/**
成功返回0,失败返回错误编号
*/
int pthread_mutex_lock(pthread_mutex_t *mutex)
/**
在互斥锁被其他线程锁住时立刻返回,不会阻塞当前进程。其他情况作用和pthread_mutex_lock()函数一样
*/
int pthread_mutex_trylock(pthread_mutex_t * mutex)
/***
解锁互斥锁
*/
int pthread_mutex_unlock(pthread_mutex_t * mutex)
/***
释放互斥锁变量。成功返回0,失败返回错误编号
*/
int pthread_mutex_destroy(pthread_mutex_t * mutex)
/**
互斥锁的共享属性
*/
int pthread_mutexattr_setshared(const pthread_attr_t *mattr, int pshared)
int pthread_mutexattr_getshared(const pthread_attr_t *mattr, int * pshared)
//pshared : PTHREAD_PROCESS_PRIVATE 和 PTHREAD_PROCESS_SHARED
/**
互斥锁类型
*/
int pthread_mutexattr_settype(pthread_mutexattr_t *mattr, int type)
int pthread_mutexattr_gettype(pthread_mutexattr_t *mattr, int *type)
//type: PTHREAD_MUTEX_NOMAL 快速互斥锁 PTHREAD_MUTEX_RECURSIVE 递归互斥锁 PTHREAD_MUTEX_ERRORCHECK 检错互斥锁
信号量
有名信号量,保存在文件中可以多线程同步和多进程同步
无名信号量,保存在内存,用于同一进程的不同线程同步。
无名信号量
sem_init()函数
初始化信号量。成功返回0,失败返回-1
#include<semaphore.h>
int sem_init(sem_t * sem,int pshared, unsigned int value)
sem:实质是一个int类型指针 pshared:决定能否在几个进程间共享。为0代表只能本进程的线程共享。不为0代表在多个进程间共享 value:初始化信号量的值
sem_wait()函数
阻塞当前线程直到信号量sem大于0。成功返回0,sem减1,出错返回-1
#include<semaphore.h>
int sem_wait(sem_t *sem)
sem_trywait()
sem_wait()的非阻塞版本,成功返回0,sem减1,失败立即返回-1;
#include<semaphore.h>
int sem_trywait(sem_t * sem)
sem_post()函数
回退资源。成功返回0,将sem加1;出错返回-1;
#include<semaphore.h>
int sem_post(sem_t *sem)
sem_getvalue()
用于获取信号量数量。成功返回0,出错返回-1.
#include<semaphore.h>
int sem_getvalue(sem_t *sem,int *sval)
sem_destroy()函数
归还信号量所占的资源。成功返回0,出错返回-1
#include<semaphore.h>
int sem_destroy(sem_t *sem)
有名信号量
有名信号量和无名信号量共用sem_wait(),sem_trywait(),sem_post()函数
有名信号量用sem_open()函数初始化,结束时需要调用sem_close()和sem_unlink()函数(有名信号量使用的是文件存储,所以需要关闭还要删除)
sem_open()函数
创建或打开已存在的有名信号量。成功返回信号量指针,出错返回SEM_FAILED
#include<semaphore.h>
sem_t * sem_open(const char* name,int oflag, mode_t mode,unsigned int value)
name:信号量的外部名称。信号量创建的文件都在/dev/shm目录下,指定名字时不能包含路径。 oflag:O_CREATE信号量不存在时则创建,且此时mode和value必须有效;若存在时则打开信号量,自动忽略mode和value。O_CREATE | EXCL若信号量不存在则和O_CREATE时一样,若存在将返回一个错误 mode:同文件权限 value:信号量初始值
sem_close()、sem_unlink()函数
sem_close函数用于关闭信号量,并释放资源。sem_unlink函数用于信号量关闭后删除所值的信号量。成功返回0,出错返回-1
#include<semaphore.h>
int sem_close(sem_t *sem)
int sem_unlink(const char *name)
条件变量
条件变量是一种同步机制,允许线程挂起,直到共享数据上某些条件得到满足。条件变量是利用线程间的全局变量进行同步的一种机制。一个线程等待条件变量的条件成立,另一个线程使条件成立并发出条件成立信号。
条件变量一般要和互斥锁一起使用,基本操作步骤:
- 声明pthread_cond_t条件变量,使用pthread_cond_init()函数初始化
- 声明pthread_mutex_t变量,调用pthread_mutex_init()函数初始化
- 调用pthread_cond_signal()函数发出信号,如果此时有线程在等待该信号,则线程被唤醒,否则忽略该信号。如果想让所有等待该信号的线程都唤醒,则调用pthread_cond_broadcast()函数
- 调用pthread_cond_wait()/pthread_cond_timedwait()等待信号。如果没有信号就阻塞。在调用之前必须先获得互斥量,如果线程阻塞则释放互斥量
- 调用pthread_cond_destroy()函数销毁条件变量,释放所占的资源。
相关函数原型:
#include<pthread.h>
/***
attr:赋值为NULL,还没有定义相关结构体
*/
int pthread_cond_init(pthread_cond_t * cond,const pthread_condattr_t *attr)
/****
发出条件变量,表示满足条件成立。唤醒一个线程
*/
int pthread_cond_signal(pthread_cond_t *cond)
/***
唤醒所有等待cond信号条件的线程
*/
int pthread_cond_broadcast(pthread_cond_t *cond)
/***
等待条件满足,成功返回0,出错返回错误编号
*/
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
/***
计时等待,在给定时间内等待,如果还是不满足则返回ETIMEOUT,否则成功返回0,失败返回错误编号。
abstime:和time()函数返回值代表的意义相同。GTM时间
*/
int pthread_cond_timedwait(pthread_cond_t * cond,pthread_mutex_t * mutex, const struct timespec * abstime)
/***
销毁指定条件变量,成功返回0,失败返回错误编号
*/
int pthread_cond_destroy(pthread_cond_t *cond)
示例程序:
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void * thread1(void *);
void *thread2(void *);
int i = 1;
int main()
{
pthread_t t_a;
pthread_t t_b;
pthread_create(&t_a,NULL,thread1,(void *)NULL);
pthread_create(&t_b,NULL,thread2,(void *)NULL);
pthread_join(t_a,NULL);
pthread_join(t_b,NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
void * thread1(void *arg)
{
for(i = 1;i <= 9 ;i++){
pthread_mutex_lock(&mutex);
if(i % 3 == 0){
pthread_cond_signal(&cond);
}else{
printf("thread 1:%d\n",i);
}
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
void * thread2(void *arg)
{
while(i < 9){
pthread_mutex_lock(&mutex);
if(i % 3 != 0){
pthread_cond_wait(&cond,&mutex);
}
printf("thread2:%d\n",i);
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
结果: thread1:1 thread1:2 thread2:3 thread1:4 thread1:5 thread2:6 thread2:6 thread1:7 thread1:8 thread2:9
基于单线程的并发服务器
SELECT事件驱动模型 (windows,linux)
select()函数
提供异步io。让单线程/进程等待指定集合中任意一个文件描述符就绪。当没有设备准备就绪时,select()函数阻塞,若集合中任意一个设备准备就绪,select()函数返回。正常情况下select()返回就绪的文件描述符个数,如果timeout时间后还没有就绪的,那么返回0。如果select()被某个信号中断,返回-1,。调用出错返回-1。
#include<pthread.h>
int select(int maxfdp, fd_set *readfds, fd_set* writefds, fd_set* errorfds, struct timeval * timeout)
maxfdp:指集合中所有文件描述符的范围。即所有文件描述符的最大值加1,该参数通常使用getdtablesize()函数获得 readfds:fd_set结构指针。存放可读文件描述符集合。如果集合中的文件有一个可读,那么select()就会返回大于0的值,如果没有就会根据timeout的值判断超时,超时返回0,出错返回-1 writefds:存放可写文件描述符集合。类似readfds。 errorfds:用来监控文件错误异常 timeout:timeval结构体指针,取值如下 NULL:select()处于阻塞模式,且永不超时 0秒0毫秒:select()函数处于非阻塞模式 大于0:设定定时器 fd_set结构体的操作宏 FD_ZERO(&fdset);//将fdset清零,清空fdset和文件句柄之间的关系 FD_SET(fd,&fdset);//将fd加入fdset FD_CLR(fd,&fdset);//将fd从fdset中删除 FD_ISSET(fd,&fdset);//判断fd是否在fdset中就绪 struct timeval{ long tv_sec;//秒 long tv_usec;//毫秒 };
示例程序片段:
...
#include<pthread.h>
int main()
{
...
fd_set rfds;//可读集合
fd_set afds;//保存所有的文件描述符集合
msock = socket(...);
bind...
listen...
int nfds = getdtablesize();//获取最大描述符个数
FD_ZERO(&rfds);
FD_SET(msock,&afds);//将msock加入可读集合
...
while(1){//死循环
memcpy(&rfds,&afds,sizeof(rfds));
if(select(nfds,&rfds,(fd_set*)0,(fd_set*)0,(struct timeval *)0) < 0)
{
errexit("select 出错:%s\n",strerror(errno));
}
if(FD_ISSET(msock,rfds))
{
...
ssock = accept(...)
if(ssock < 0){
errexit("accept 出错:%s\n",strerror(errno));
}
FD_SET(ssock,rfds);
}
for(fd = 0;fd < nfds,++fd){
if(fd != msock && FD_ISSET(fd,rfds))
{
...
recv(...)
...
send(...)
close(fd);
FD_CLR(fd,&afds);
}
}
}
}
线程池
预先创建线程,在线程不足的情况下再创建新的。适用于线程任务时间短需要的线程多的情况,避免大量的时间浪费在创建线程上。
风险:同步错误,死锁,池的死锁,资源不足,线程泄漏等。
基于Epoll模型的并发 (linux)
相对于select模型,epoll模型具有更大的文件描述符数量。select由FD_SETSIZE设置,其默认大小为2048,要修改只能修改后重新编译内核,而epoll则支持的数量远远大于该值,其最大值跟内存大小有关系,可以通过 cat /proc/sys/fs/file -max 查看。其次是选择就绪的文件的方式,select采用的是遍历的方式,epoll采用的是基于事件的选择。选择效率比select的遍历方式高。
相关API
epoll_create()函数
创建epoll句柄。返回一个文件描述符,记得用完之后使用close()函数关闭,否则可能导致文件描述符耗尽。
#inlcude<sys/epoll.h>
int epoll_create(int size);
size:用来告诉内核监听的数目一共有多大。
epoll_ctl()函数
为epoll的事件注册函数
#include<sys/epoll.h>
int epoll_ctl(int epfd,int op, int fd, struct epoll_event * event)
epfd:epoll_create()函数返回的句柄。 op: EPOLL_CTL_ADD:注册新的fd到epfd中 EPOLL_CTL_MOD:修改已经注册的fd的监听事件 EPOLL_CTL_DEL:从epfd中删除fd event:告诉内核需要监听什么事件。 struct epoll_event{ __uint32_t events;//事件 epoll_data_t data;//用户变量数据 }; events可以是一下宏的集合: EPOLLIN:对应文件描述符可以读(包括对端socket正常关闭) EPOLLOUT:对应文件描述符可以写 EPOLLPRI:对应文件描述符有紧急数据可读 EPOLLERR:对应文件描述符发生错误 EPOLLHUP:对应文件描述符被挂起 EPOLLET:将epoll设为边缘触发模式 EPOLLONESHOT:只监听一次事件,监听完后如果还想再次监听,则需要再次加入epoll队列
epoll_wait()函数
等待事件产生。返回需要处理事件的数目,返回0表示已经超时。
#include<sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
events:用来从内核得到事件集合 maxevents:告诉内核events有多大。不能大于epoll_create()函数传入的size的大小。 timeout:超时时间(毫秒)。0 立即返回,-1 永久阻塞
触发模式
水平触发(LT),边缘触发(ET)
ET模式事件效率高,但是编程复杂,需要程序员仔细处理事件,否则容易漏事件。
LT模式效率比ET模式低,但编程容易。
示例代码:
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/wait.h>
#include<unistd.h>
#include<arpa/inet.h>
//#include<openssl/ssl.h>
//#include<openssl/err.h>
#include<fcntl.h>
#include<sys/epoll.h>
#include<sys/time.h>
#include<sys/resource.h>
#define MAXBUF 1024
#define MAXEPOLLSIZE 10000
int setnonblocking(int sockfd)
{
if(fcntl(sockfd,F_SETFL,fcntl(sockfd,F_GETFD,0)|O_NONBLOCK) == -1){
return -1;
}
return 0;
}
int handle_message(int new_fd){
char buf[MAXBUF+1];
int len;
bzero(bug, MAXBUF+1);
len = recv(new_fd, buf,MAXBUF,0);
if(len > 0){
printf("%d接收消息成功:'%s',共%d字节的数据",new_fd,buf,len);
}else{
if(len < 0){
printf("接受消息失败!错误代码:%d,错误信息:%s\n",errno,strerror(errno));
}
close(new_fd);
return -1;
}
return len;
}
int main(int argc,char *argv[])
{
int listener,new_fd,kdpfd,nfds,n,ret,curfds;
socklen_t len;
struct sockaddr_in my_addr,their_addr;
unsigned int myport,lisnum;
struct epoll_event ev;
struct epoll_event events[MAXEPOLLSIZE];
struct rlimit rt;
myport = 5000;
lisnum = 2;
rt.rlim_max = rt.rlim_cur = MAXEPOLLSIZE;
if(setrlimit(RLIMIT_NOFILE,&rt) == -1){
perror("setrlimit");
exit(1);
}else{
printf("设置系统参数成功!\n");
}
if((listener = socket(PF_INET,SOCK_STREAM,0))== -1){
perror("socket");
exit(1);
}else{
printf("socket创建成功!");
}
setnonblocking(listener);
bzero(&my_addr,sizeof(my_addr));
my_addr.sin_family = PF_INET;
my_addr.sin_port = htons(myport);
my_addr.sin_addr.s_addr = INADDR_ANY;
if(bind(listener,(struct sockaddr *)&my_addr,sizeof(struct sockaddr)) == -1){
perror("bind");
exit(1);
}else{
printf("ip地址端口绑定成功\n");
}
if(listen(listener,lisnum) == -1){
perror("listen");
exit(1);
}else{
printf("开启服务成功\n");
}
kdpfd = epoll_create(MAXEPOLLSIZE);
len = sizeof(struct sockaddr_in);
ev.events = EPOLLIN|EPOLLET;
ev.data.fd = listener;
if(epoll_ctl(kdpfd,EPOLL_CTL_ADD,listener,&ev)< 0){
fprintf(stderr,"epoll set insertion error:fd = %d\n",listener);
return -1;
}else{
printf("监听socket加入epoll成功\n");
}
curfds = 1;
while(1){
nfds = epoll_wait(kdpfd,events,curfds,-1);
if(nfds == -1){
perror("epoll_wait");
break;
}
for(n = 0;n < nfds;n++){
if(events[n].data.fd == listerner){
new_fd = accept(listener,(struct sockaddr *)&their_addr,&len);
if(new_fd < 0){
perror("accept");
continue;
}else{
printf("有连接来自:%d:%d,分配的socket为:%d\n",inet_ntoa(their_addr.sin_addr),ntohs(their_addr.sin_port),new_fd);
}
setnonblocking(new_fd);
ev.events = EPOLLIN|EPOLLET;
ev.data.fd = new_fd;
if(epoll_ctl(kdpfd,EPOLL_CTL_ADD,new_fd,&ev) < 0){
printf(stderr,"吧socket %d 加入epoll失败! %s",new_fd,strerror(errno));
return -1;
}
curfds++;
}else{
ret = handle_message(events[n].data.fd);
if(ret < 1 && errno != 11){
epoll_ctl(kdpfd,EPOLL_CTL_DEL,events[n].data.fd,&ev);
curfds--;
}
}
}
}
close(listener);
return 0;
}
死锁
原因
- 竞争资源
- 进程推进顺序不当
产生死锁的必要条件
- 互斥条件:存在独占资源。
- 请求和保持条件:占有资源后提出新的资源请求,没有得到满足但是又不释放自己已经获得的资源。
- 不剥夺条件:进程已经获得的资源在未使用完之前不能被剥夺。
- 环路等待条件:指进程的资源需求形成了环形。A->B->C->A
存在死锁的示例程序:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<ctype.h>
#include<pthread.h>
#define LOOP_TIMES 10000
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
void *thread_worker(void *);
void critical_sesstion(int thread_num,int i);
int main(void)
{
int rtn,i;
pthread_t pthread_id = 0;
rtn = pthread_create(&pthread_id,NULL,thread_worker,NULL);
if(rtn != 0){
printf("pthread_create error!\n");
return -1;
}
for(i = 0;i < LOOP_TIMES;i++){
pthread_mutex_lock(&mutex1);
pthread_mutex_lock(&mutex2);
critical_section(1,i);
pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1);
}
pthread_mutex_destroy(&mutex1);
pthread_mutex_destroy(&mutex2);
return 0;
}
void * thread_worker(void * p)
{
int i = 0;
for(i = 0;i<LOOP_TIMES;i++){
pthread_mutex_lock(&mutex2);
pthread_mutex_lock(&mutex1);
critical_section(2,i);
pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex2);
}
}
void critical_section(int thread_num,int i)
{
printf("thread%d:%d\n",thread_num,i);
}
上一篇: TCP半关闭