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循环服务器
  1. 调用socket()函数创建UDP套接字
  2. 调用bind()函数将套接字绑定到本地可用端点地址
  3. while(1)死循环
  4. 循环体内
    1. 调用recvfrom()函数读取客户请求
    2. 处理数据
    3. 调用sendto()函数返回数据给客户
TCP循环服务器
  1. 调用socket()函数创建TCP套接字
  2. 调用bind()函数将套接字绑定到本地可用端点地址
  3. 调用listen()函数将套接字设为被动模式
  4. while(1)死循环
    1. 调用accept()函数接收客户请求并建立一个处理该连接的临时套接字
    2. 调用recv/send()函数进行数据相互传递
    3. 交互完毕,关闭临时套接字(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);

僵尸进程的避免
  1. 父进程调用wait()或waitpid()等函数等待子进程结束,但是这会使父进程挂起(进入阻塞或等待状态)
  2. 如果父进程很忙不能被挂起,可以调用signal()函数为SIGCHLD信号安装handler来避免。当子进程结束后,内核发送SIGCHLD信号给其父进程,父进程收到信号后则可在handler中调用wait()函数来进行回收。
  3. 如果父进程不关心子进程何时结束,则可调用signal(SIGCHLD,SIG_IGN)函数通知内核,让子进程在结束时由内核自动回收,且内核不会给父进程发送SIGCHLD信号。
  4. 当父进程结束后,子进程就成了孤儿进程。从而会被过继给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)

不固定数量的进程并发模型

  1. 主进程创建套接字msock并绑定到熟知端口
  2. 主进程调用accept()函数等待客户连接的到达
  3. 当有客户连接时,主进程建立与客户端的通信连接,同时accept()函数返回新的套接字ssock
  4. 主进程创建新的从进程来处理ssock
  5. 主进程调用close()函数将ssock的引用计数减一
  6. 主进程返回步骤2
  7. 从进程关闭msock(close()函数,减少引用计数,shutdown()函数才是直接关闭套接字的)
  8. 从进程调用recv/send函数进行客户端的数据处理
  9. 从进程处理完毕,关闭ssock,结束进程。

固定进程数的并发模型

  1. 父进程
    1. 主进程创建主套接字msock,并绑定到熟知端口
    2. 主进程创建给定的数量的从进程
    3. 主进程调用wait()函数等待从进程结束,一旦有从进程退出,则调用fork()创建新的进程,以保持数量不变
  2. 从进程
    1. 从进程调用accept()等待客户连接到达
    2. 当有客户连接时,从进程建立与客户端的通信连接,同时accept()函数返回新的套接字ssock
    3. 从进程调用recv/send函数处理
    4. 处理完毕,从进程关闭套接字

互斥锁

互斥锁就是排他锁,一个时间只有一个线程能拥有该锁

互斥锁有三种:快速互斥锁,递归互斥锁,检错互斥锁。
快速互斥锁:调用的线程会阻塞直到解锁为止。
递归互斥锁:能成功的返回并增加调用线程在互斥锁上的加锁次数
检错互斥锁:调用的线程不会被阻塞,它会立刻返回一个错误信息

操作步骤

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)

条件变量

条件变量是一种同步机制,允许线程挂起,直到共享数据上某些条件得到满足。条件变量是利用线程间的全局变量进行同步的一种机制。一个线程等待条件变量的条件成立,另一个线程使条件成立并发出条件成立信号。
条件变量一般要和互斥锁一起使用,基本操作步骤:

  1. 声明pthread_cond_t条件变量,使用pthread_cond_init()函数初始化
  2. 声明pthread_mutex_t变量,调用pthread_mutex_init()函数初始化
  3. 调用pthread_cond_signal()函数发出信号,如果此时有线程在等待该信号,则线程被唤醒,否则忽略该信号。如果想让所有等待该信号的线程都唤醒,则调用pthread_cond_broadcast()函数
  4. 调用pthread_cond_wait()/pthread_cond_timedwait()等待信号。如果没有信号就阻塞。在调用之前必须先获得互斥量,如果线程阻塞则释放互斥量
  5. 调用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;
}

死锁

原因
  1. 竞争资源
  2. 进程推进顺序不当
产生死锁的必要条件
  1. 互斥条件:存在独占资源。
  2. 请求和保持条件:占有资源后提出新的资源请求,没有得到满足但是又不释放自己已经获得的资源。
  3. 不剥夺条件:进程已经获得的资源在未使用完之前不能被剥夺。
  4. 环路等待条件:指进程的资源需求形成了环形。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);
}

猜你喜欢

转载自blog.csdn.net/github_33719169/article/details/84839127