Twosmi1e's Blog.

Linux网络编程基础

Word count: 6,698 / Reading time: 28 min
2018/11/20 Share

套接字编程

套接字是一种网络API,用来开发网络程序。套接字接口提供一种进程间通信的方法,使得在相同或不同的主机上的进程能以相同的规范进行双向信息传送。

套接字类型

常用协议:

  • INET ipv4
  • INET6 ipv6
    类型:
  • SOCK_STREAM :提供面向连接、可靠的数据传输服务。TCP协议支持。
  • SOCK_DGRAM :提供面向无连接的服务。UDP协议支持。
  • SOCK_RAW:原始套接字,常用于检测新的协议。

    套接字地址结构

    套接字地址结构定义在头文件<netinet/in.h>

    IPv4

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    typedef uint32_t in_addr_t;
    typedef uint16_t in_port_t;
    typedef unsigned short sa_family_t;

    struct in_addr
    {
    in_addr_t s_addr;
    };
    struct sockaddr_in
    {
    uint8_t sin_len;//套接字地址结构长度
    sa_family_t sin_family;//Internet地址族
    in_port_t sin_port;//端口号
    struct in_addr sin_addr;//结构体 成员存储IP地址
    char sin_zero[8];//暂未使用,置0
    };

IPv6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef uint16_t in_port_t;
typedef unsigned short sa_family_t

struct in6_addr
{
uint8_t s6_addr[16];
};
struct sockaddr_in6
{
uint8_t sin6_len;//套接字地址结构长度
sa_family_t sin6_family;//Internet地址族
in_port_t sin6_port;//端口号
uint32_t sin6_flowinfo; //低24位流量标号,下4位优先级,再下4位保留
struct in6_addr sin6_addr;//结构体 成员存储IP地址
};

地址结构比较

通用套接字地址结构

定义在头文件<sys/socket.h>

1
2
3
4
5
struct  sockaddr{
uint8_t sa_len;
sa_family_t sa_family;
char sa_data[14];
};

基本函数

字节排序函数

1
2
3
4
5
6
7
8
#include <netinet/in.h>
uint16_t htons(uint16_t hostshort)
uint32_t htonl(uint32_t hostlong)
//均返回:网络字节序值

uint16_t ntohs(uint16_t netshort)
uint32_t ntohl(uint32_t netlong)
//均返回:主机字节序值

h:主机 n:网络 s:短整数 l:长整数

字节操纵函数

1
2
3
4
5
/*BSD*/
#include <string.h>
void bzero(void *dest, size_t nbytes);
void bcopy(const void *src, void *dest, size_t nbytes);
int bcmp(const void *src, void *dest, size_t nbytes); /*返回0则相同,非0不相同*/

bzero置0,bcopy拷贝,bcmp比较。

1
2
3
4
/*ANSI C*/
void *memset(void *dest, int c, size_t len);
void *memcpy(void *dest, const void *src, size_t nbytes);
int memcmp(const void *ptr1, const void *ptr2, size_t nbytes)

IP地址转换函数

1
2
3
4
5
6
#include <arpa/inet.h>
将点分十进制数串转换成32位网络字节序二进制值。
int inet_aton(const char *cp, struct in_addr *inp);
返回:1-串有效,0-串有错
in_addr_t inet_addr(const char *cp);
返回:若成功,返回32位二进制的网络字节序地址,若有错,则返回一个常值

inet_aton函数将cp所指的字符串(点分十进制数串,如192.168.0.1)转换成32位的网络字节序二进制,并通过指针inp来存储。这个函数需要对字符串所指的地址进行有效性验证。但如果cp为空,函数仍然成功,但不存储任何结果。
inet_addr进行相同的转换,但不进行有效性验证,也就是说,所有232种可能的二进制值对inet_addr函数都是有效的。
inet_ntoa函数将32位的网络字节序二进制IPv4地址转换成相应的点分十进制数串。但由于返回值所指向的串留在静态内存中,这意味着函数是不可重入的。

上述三个地址转换函数都只能处理IPv4协议,而不能处理IPv6地址。

inet_pton的实现(只支持ipv4)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int inet_pton(int family, const char *strptr, void *addrptr)
{
if (family == AF_INET) {
struct in_addr in_val;
if (inet_aton(strptr, &in_val)) {
memcpy(addrptr, &in_val, sizeof(struct in_addr));
return (1);
}
return(0);
}
else {
errno = EAFNOSUPPORT; /*以不被支持的地址族做为family的参数*/
return (-1);
}
}

TCP套接字编程

Alt text

服务器端

  1. 创建套接字
  2. 绑定套接字
  3. 设置套接字为监听模式,进入被动接受连接状态
  4. 接受请求,建立连接
  5. 读写数据
  6. 终止连接

    客户端

  7. 创建套接字
  8. 与服务器建立连接
  9. 读写数据
  10. 终止连接

TCP服务器模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main(void)
{
int sockfd,connect_sock;
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1) {

perror(“create socket failed.”);
exit(-1);
}
/* bind sockfd to some address */
/* listen */
……
loop {

if((connect_sock=accept(sockfd,NULL,NULL))==-1) {
perror(“Accept error.”);
exit(-1);
}
/* read and process request */
close(connect_sock);
}
close(sockfd);
}

TCP客户端模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* include some header files */
int main(void)
{
int sockfd;
if((sockfd=socket(AF_INET,SOCK_STREAM,0))=-1)
{
perror(“Create socket failed.”);
exit(-1);
}
/* connect to server */
……
/* send requst and receive response */
……
close(sockfd);
}

基本套接字函数

socket

1
2
3
#include <sys/socket.h>
int socket(int family, int type, int protocol)
返回:非负套接字(sockfd)-成功;-1-出错

family:协议族;type:套接字类型; protocol:如调用者不想指定,一般为0 ,表示缺省,原始套接字除外。
Alt text
Alt text
protocal: 指明socket请求所使用的协议,IPPROTO_TCP表示TCP协议,IPPROTO_UDP表示UDP协议

创建socket代码

1
2
3
4
5
6
7
8
9
 #include <sys/socket.h>
……
int sockfd;
//crteate socket
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) = = -1)
{
//handle exception
……
}

bind

1
2
3
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_len len)
返回:0-成功;-1-出错并置errno

该函数指明套接字将使用本地的哪一个协议的参数进行数据传送(IP地址和端口号)注意:协议地址addr是通用地址。
Len是该地址结构(第二个参数)的长度。
Alt text

setsockopt

1
2
3
#include <sys/socktet.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
//返回:0-OK;-1-出错。

Level是选项所在的层及协议,有如下值:

  • SOL_SOCKET (通用套接字 )
  • IPPROTO_TCP(传输层,TCP协议)
  • IPPROTO_IP(网际层,IP协议)
    optname是操作选项名
    optval是一个指向变量的指针

    listen

    1
    2
    3
    #include <sys/socket.h>
    int listen(int sockfd, int backlog)
    返回:0-成功;-1-出错并置errno值;

仅被服务器调用。函数listen将未连接的套接字转化成被动套接字,指示内核应接受指向此套接字的连接请求;函数的第二个参数规定了内核为此套接字排队的最大连接个数;
Alt text
Alt text

connect

1
2
3
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 返回:0-成功;-1-出错;

函数connect激发TCP的三次握手过程。
|错误代码|原因|
|:–:|:—:|
|ETIMEDOUT|客户没有收到SYN分节响应|
|ECONNREFUSED|SYN响应为RST|
|EHOSTUNREACH或ENETUNREACH|ICMP错误,地址不可达|

accept

1
2
3
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
返回:非负描述字(connfd)-OK;-1-出错;

由TCP服务器调用,从已完成连接队列头返回下一个已完成连接。如果为空则进程进入睡眠状态。

close

1
2
3
#include <unistd.h>
int close(int sockfd);
返回:0-OK;-1-出错;

关闭套接字。
如果套接字描述符访问计数在调用close后大于0(在多个进程共享同一个套接字的情况下),则不会引发TCP终止序列(即不会发送FIN分节);

shutdown

1
2
3
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
返回:0-OK;-1-出错,并置相应的errno的值;

立即发送FIN分节。

read

1
2
3
#include <unistd.h>
int read(int fd, char *buf, int len);
返回:大于0-读写字节大小;-1-出错;

接收缓冲区数据,返回接收到的字节数。
tcp协议收到FIN数据,返回0;
tcp协议收到RST数据,返回-1,同时errno为ECONNRESET;
进程阻塞过程中接收到信号,返回-1,同时errno为EINTR。

write

1
2
3
#include <unistd.h>
int write(int fd, char *buf, int len);
返回:大于0-读写字节大小;-1-出错;

向缓冲区发送数据,返回发送的字节数。
tcp协议接收到RST数据,返回-1,同时errno为ECONNRESET; ;
进程阻塞过程中接收到信号,返回-1,同时errno为EINTR。

数据传输函数

send

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send (int fd, const void *msg, size_t len, int flags);
//返回:非0-发送成功的数据长度;-1-出错;

flags 是传输控制标志,其值定义如下:

  • 0:常规操作,如同write()函数
  • MSG_OOB:发送带外数据(TCP紧急数据)。
  • MSG_DONTROUTE:忽略底层协议的路由设置,只能将数据发送给与发送机处在同一个网络中的机器上。

    recv

    1
    2
    3
    4
    #include <sys/types.h>
    #include <sys/socket.h>
    ssize_t recv(int fd, void *buf ,size_t len, int flags);
    返回:大于0表示成功接收的数据长度;0: 对方已关闭,-1:出错。

flags是传输控制标志,其值定义如下:

  • 0:常规操作,如同read()函数;
  • MSG_PEEK:只查看数据而不读出数据,后续读操作仍然能读出所查看的该数据;
  • MSG_OOB:忽略常规数据,而只读带外数据;
  • MSG_WAITALL:recv函数只有在将接收缓冲区填满后才返回。

    域名解析函数——gethostbyname

    1
    2
    3
    #include <netdb.h>
    struct hostent *gethostbyname(const char *hostname);
    返回:非空指针-成功;空指针-出错,同时设置h_error

可解析IPv4和IPv6地址,既可接收域名也可接收点分十进制。
hostname为点分十进制时,函数不执行网络查询直接拷贝到结果。
Alt text

读取文件函数

1
char *fgets(char *buf, int num, FILE *fp);

fp指向的文件读取一个长度为num-1的字符串,存入起始地址为buf的空间。返回地址buf,若遇文件结束或出错,返回NULL。
其中,fp使用stdin就是从标准输入读取数据

UDP套接字编程

Alt text

服务器端

  1. 建立UDP套接字;
  2. 绑定套接字到特定地址;
  3. 等待并接收客户端信息;
  4. 处理客户端请求;
  5. 发送信息回客户端;
  6. 关闭套接字;

    客户端

  7. 建立UDP套接字;
  8. 发送信息给服务器;
  9. 接收来自服务器的信息;
  10. 关闭套接字

    服务器端模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #include <sys/types.h>
    #include <sys/socket.h>
    #inlcude <netinet/in.h>
    int main(void)
    {
    int socketfd;
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
    perror(“Create socket failed.”);
    exit(1);
    }
    /* Bind socket to address */
    ……
    loop {
    /* receive and process data from client */
    ……
    /* send resuts to client */
    }
    close(sockfd); }
    ![Alt text](./1541906670794.png)

客户端模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <sys/types.h>
#include <sys/socket.h>
#inlcude <netinet/in.h>
int main(void)
{
int sockfd;
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
perror(“Create socket failed.”);
exit(1);
}
/* send data to the server */
……
/* receive data from the server */
……
close(sockfd);
}
![Alt text](./1541906705051.png)

UDP数据传输函数

sendto

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *msg, size_t len, int flags, const struct sockaddr *to, int tolen);
返回:大于0-成功发送数据长度;-1-出错;

UDP是无连接协议,必须使用sendto函数。指明目的地址。
flags是传输控制标志,其值定义如下:
0:常规操作,如同write()函数;
MSG_OOB:发送带外数据;
MSG_DONTROUTE:忽略底层路由协议,直接发送。

recvfrom

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *from, int *fromlen);
返回:大于0-成功接收数据长度;-1-出错;

指明源地址。flags是传输控制标志,其值定义如下:
0:常规操作,如同read()函数;
MSG_PEEK:只察看数据而不读出数据;
MSG_OOB:忽略常规数据,而只读取带外数据;

注意复习实验内容和作业内容

多线程多进程并发服务器

Linux系统支持并发三种方式:进程,线程, I/O多路复用

按连接类型分为面向连接和无连接的服务器
按处理方式分为迭代和并发服务器
Alt text

进程

基本概念

进程是一个动态实体,是独立的任务,拥有独立的地址空间、执行堆栈、文件描述符。进程是相互独立的,互不影响。可以通过IPC机制相互通信。

进程操作

创建新进程
1
2
3
4
5
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
//返回:父进程中返回子进程的进程ID, 子进程返回0,返回-1-出错
pid_t vfork(void);//父进程将被暂时阻塞
终止进程

进程的终止存在两个可能:

  • 父进程先于子进程终止(init进程领养)
  • 子进程先于主进程终止
1
2
3
#include<stdlib.h>
void exit(int status)
//关闭所有子进程打开的描述符,向父进程发送SIGCHLD信号,并返回状态
获取子进程终止信息
1
2
3
4
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *stat_loc);
//返回:终止子进程的ID-成功;-1-出错;stat_loc存储子进程的终止状态(一个整数);

如果没有终止的子进程,但是有一个或多个正在执行的子进程,则该函数将堵塞,直到有一个子进程终止或者wait被信号中断时,wait返回。
当调用该系统调用时,如果有一个子进程已经终止,则该系统调用立即返回,并释放子进程所有资源。
使用wait可能会留下僵尸进程

1
2
pid_t waitpid(pid_t pid, int *stat_loc, int options);
//返回:终止子进程的ID-成功;-1-出错;stat_loc存储子进程的终止状态;

pid

  • -1:要求知道任何一个子进程的返回状态(等待第一个终止的子进程);
  • >0:要求知道进程号为pid的子进程的状态;
  • <-1:要求知道进程号为pid的绝对值的子进程的终止状态

options:最常用的选项是WNOHANG,它通知内核在没有已终止进程时不要堵塞。

调用wait或waitpid函数时,正常情况下,可能会有以下几种情况:

  • 阻塞(如果其所有子进程都还在运行);
  • 获得子进程的终止状态并立即返回(如果一个子进程已终止,正等待父进程存取其终止状态);
  • 出错立即返回(如果它没有任何子进程)

    多进程并发服务器模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    int main(void)
    {
    int listenfd, connfd;
    pid_t pid;
    int BACKLOG = 5;
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
    perror(“Create socket failed.”);
    exit(1);
    }
    bind(listenfd, …);
    listen(listenfd, BACKLOG);
    while(1) {
    if ((connfd = accept(sockfd, NULL, NULL)) == -1) {
    perror(“Accept error.”);
    exit(1);
    }
    if((pid = fork() ) > 0){
    /*parent process */
    close(connfd);
    …….
    continue;
    }
    else if (pid == 0){
    /*child process */
    close(lisetenfd);
    …….
    exit(0);
    }
    else{
    printf(“fork error\n”);
    exit(1);
    }
    }
    }

父子进程对套接字处理

产生新的子进程后,父进程要关闭连接套接字,子进程要关闭监听套接字
原因

  1. 节省系统资源。
  2. 避免父子进程同时对共享描述符进程操作造成错误。
  3. 为了正确关闭连接,需要父进程将不需要的已连接描述符关闭,子进程关闭不需要的监听描述符,使访问计数符在关闭时能置为0。

僵尸进程与孤儿进程

参考文章:https://www.cnblogs.com/Anker/p/3271773.html

僵尸进程

一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用waitwaitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。
防止:

  1. 子进程正常或异常终止时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。
  2. fork两次。原理是将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。
    孤儿进程
    一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

线程

基本概念

线程是进程内的独立执行实体和调度单元,又称为“轻量级”进程(lightwight process);创建线程比进程快10~100倍。
Alt text

线程操作

创建线程
1
2
3
#include <pthread.h>
int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*func)(void *), void *arg);
//返回:成功时为0;出错时为正的Exxx值

tid:线程的id标识,attr:属性如优先级,是否是守护线程。func:线程的执行函数。arg:线程的调用参数。
常见的返回错误值:

  • EAGAIN:超过了系统线程数目的限制。
  • ENOMEN:没有足够的内存产生新的线程。
  • EINVAL:无效的属性attr值。

    等待线程终止
    1
    2
    3
    #inlcude <pthread.h>
    int pthread_join(pthread_t tid, void **status);
    // 返回:成功时为0;出错时为正的Exxx值,不设置error

    waitpid函数类似,指定等待线程的ID。被等待线程必须是当前进程成员,且不是分离的线程和守护线程。

    分离线程
    1
    2
    3
    #include <pthread.h>
    int pthread_detach(pthread_t tid)
    返回:成功时为0;出错时为正Exxx值;

线程分为可联合的(joinable)和分离的(detached)。前者终止时,线程ID和终止状态将保留,直到另外一个线程调用pthread_join。后者终止时释放所有资源。
此函数将指定线程设置为分离的。

获取ID
1
2
3
4
#include<pthread.h>
pthread_t pthread_self(void);
//返回调用函数的线程ID
pthread_detach(pthread_self());//将自己设为分离线程
终止线程
1
2
3
#include <pthread.h>
void pthread_exit(void *status);
//无返回值;

指针status执行线程的退出状态。

终止线程的三种方法:

  1. 调用pthread_exit
  2. 启动线程的函数pthread_create的第三个参数,返回终止状态。
  3. 进程终止,线程随之终止。
    一次执行
    1
    2
    3
    #include <pthread.h>
    int pthread_once(pthread_once_t *once_control, void (*init_routine) (void))
    // 成功返回0,否则返回错误码

如果本函数中,once_control变量使用的初值为PTHREAD_ONCE_INIT,可保证init_routine()函数在本进程执行序列中仅执行一次。
LinuxThreads使用互斥锁条件变量保证由pthread_once()指定的函数执行且仅执行一次,而once_control则表征是否执行过。如果once_control的初值不是PTHREAD_ONCE_INIT(LinuxThreads定义为0),pthread_once()的行为就会不正常;

取消机制
1
2
int pthread_cancel(pthread_t tid);
//返回:成功为0,失败非0

线程通过向另外一个线程发送取消请求,接收到取消请求的线程根据其设置状态,作出:1)忽略该请求;2)立即终止自己;3) 延迟一段时间终止自己;

给新线程传递参数的方法

由于同一个进程内的所有线程共享内存和变量,因此在传递参数时需作特殊处理,下面参考如下几种方法:

  • 传递参数的普通方法
  • 通过指针传递参数
  • 通过分配arg的空间来传递参数
  • 还可以通过加锁等同步设施来实现传递参数

多线程并发服务器模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void *start_routine( void *arg);
int main(void) {
int listenfd, connfd;
pthread_t tid;
type arg;
/* Create TCP socket */
……
/* Bind socket to address */
……
/* Listen */
……
while(1) {
/* Accept connection */
if ((pthread_create(&tid, NULL, start_routine, (void *)&arg))
/* handle exception */
……
}
……
}

互斥锁

先看一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <pthread.h>
#include <stdio.h>
int myglobal=0;
void *thread_function(void *arg)
{
int i,j;
for ( i=0; i<5; i++)
{
j=myglobal;
j=j+1;
printf(".");
fflush(stdout);
sleep(1);
myglobal=j;
}
return NULL;
}
int main(void)
{
pthread_t mythread;
int i;
if ( pthread_create( &mythread, NULL, thread_function, NULL) )
{
printf("error creating thread.");
exit(1);
}
for ( i=0; i<5; i++)
{
myglobal=myglobal+1;
printf("o");
fflush(stdout);
sleep(1);
}
if ( pthread_join ( mythread, NULL ) )
{
printf("error joining thread.");
exit(1);
}
printf("\nmyglobal equals %d\n",myglobal);
exit(0);
}

运行结果:
Alt text
主线程和新线程都将myglobal加1,一共累加10次。结果应是10,为什么是5?
答案:因为没有解决同步问题,最后退出的线程值确定全局变量的值。

互斥锁用来保护线程代码中共享数据的完整性。

  • 操作系统将保证同时只有一个线程能成功完成对一个互斥锁的加锁操作。
  • 如果一个线程已经对某一互斥锁进行了加锁,其他线程只有等待该线程完成对这一互斥锁解锁后,才能完成加锁操作。

函数

1
2
pthread_mutex_lock(pthread_mutex_t  *mptr) 
//返回:成功0,否则返回错误码

mptr:指向互斥锁的指针。
该函数接受一个指向互斥锁的指针作为参数并将其锁定。如果互斥锁已经被锁定,调用者将进入睡眠状态。函数返回时,将唤醒调用者。
如果互斥锁是静态分配的,就将mptr初始化为常值PTHREAD_MUTEX_INITIALIZER

1
2
pthread_mutex_unlock(pthread_mutex_t  *mptr)
//返回:成功0.否则返回错误码

用于互斥锁解锁操作。
改进后代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int myglobal=0;
pthread_mutex_t mymutex=PTHREAD_MUTEX_INITIALIZER;
void *thread_function(void *arg) {
int i,j;
for ( i=0; i<5; i++) {
pthread_mutex_lock(&mymutex);
j=myglobal;
j=j+1;
printf(".");
fflush(stdout);
sleep(1);
myglobal=j;
pthread_mutex_unlock(&mymutex);
}
return NULL;
}
int main(void) {
pthread_t mythread;
int i;
if ( pthread_create( &mythread, NULL, thread_function, NULL) ) {
printf("error creating thread.");
abort();
}
for ( i=0; i<5; i++) {
pthread_mutex_lock(&mymutex);
myglobal=myglobal+1;
pthread_mutex_unlock(&mymutex);
printf("o");
fflush(stdout);
sleep(1);
}
if ( pthread_join ( mythread, NULL ) ) {
printf("error joining thread.");
abort();
}
printf("\nmyglobal equals %d\n",myglobal);
exit(0);
}

Alt text

线程安全性编程

在多线程环境中,应避免使用静态变量。在linux环境中,用线程专用数据TSD取代静态变量。它类似于全局数据,只不过它是线程私有的,是以线程为界限的。

TSD是定义线程私有全局数据的唯一方法
每个TSD由进程内唯一的关键字(key)来标志,用来存取线程私有的数据。

函数

分配函数

1
2
3
#include<pthread.h>
int pthread_key_create(pthread_key_t *key, void (* destrctor)(void *value));
//返回:正常执行返回0,否则返回错误码

该函数在进程内部分配一个标志TSD的关键字,关键字是进程内部唯一的,所有线程在创建时关键字值是NULL。每个进程只能调用一次。
key指向创建的关键字;destructor是一个可选的析构函数,用于每个线程终止时调用该析构函数。

绑定函数

1
2
3
#include <pthread.h>
int pthread_setspecific(pthread_key_t key, const void *value);
//返回值:正常执行后返回0;否则返回正的错误码

为TSD关键字绑定一个与本线程相关的值。

获取值

1
2
void * pthread_getspecific(pthread_key_t key);
// 返回值:正常执行后返回与调用线程相关的关键字所绑定的值,否则返回NULL。

获得与调用线程相关的关键字所绑定的值。

删除函数

1
2
int pthread_key_delete(pthread_key_t key);
//返回值:成功为0,否则为非0;

该函数删除进程内的TSD表示的关键字。该函数既不检查TSD是否有绑定值,也不调用该关键字的析构函数。

I/O复用

五个模型

阻塞I/O

Alt text

非阻塞I/O

Alt text
循环调用recvfrom,称为轮询,极为浪费CPU资源。当请求不成功会有错误返回。

I/O复用

Alt text
调用selectpoll,并在该函数上阻塞。select的好处在于可以等待多个描述字准备好。

信号驱动I/O

Alt text
等待数据报到达时,可以不阻塞。

异步I/O

Alt text
让内核启动操作,并在整个操作完成后通知。
异步i/o让与信号驱动i/o的区别是:后者是由内核通知我们何时可以启动一个i/o操作,而前者是由内核通知我们i/o操作何时完成。

比较

Alt text

同步与异步

  • 同步i/o操作引起请求进程阻塞,直到i/o操作完成;
  • 异步i/o操作不引起请求进程阻塞;
    只有最后一个异步I/O模型是异步I/O

    select函数

    函数体

    1
    2
    3
    4
    #include <sys/select.h>
    #include <sys/time.h>
    int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
    //返回:准备好描述字的总数量,0-超时,-1-出错,大于0-总的位数

指示内核等待多个事件中的任意一个发生,并仅在一个或多个时间发生或经过指定时间时才唤醒进程。
timeval结构体可以提供秒数和毫秒数成员。

1
2
3
4
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
}

该函数有三种执行结果:

  1. 永远等待下去:仅在有一个或以上描述字准备好i/o才返回,为此,我们将timeout设置为空指针。
  2. 等待固定时间:在有一个描述字准备好时返回,但不超过由timeout参数指定的秒数和微秒数。
  3. 根本不等待,检查描述字后立即返回,这称为轮询。这种情况下,timeout必须指向结构timeval,且定时器的值必须为0。

    参数说明

    参数readset, writeset, execeptset指定让内核测试读,写,异常条件的描述字。
    1
    2
    3
    4
    void FD_ZERO(fd_set  *fdset);			 /* clear all bits in fdset 所有位设0*/
    void FD_SET(int fd, fd_set *fdset); /* turn on the bit for fd in fdset fd位设1*/
    void FD_CLR(int fd, fd_set *fdset); /* turn off the bit for fd in fdset fd位设0*/
    int FD_ISSET(int fd, fd_set *fdset); /* is the bit for fd on in fdset 检测fd位是否为1*/

分配一个fd_set数据类型的描述字集,利用上面四个宏操作。
参数maxfdp1指定被测试的描述字的个数,它是被测试的最大描述字加1

套接字可读条件

  • 套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区限度的当前值,对于TCP/UDP默认值为1;
  • 套接字是一个监听套接字且已完成的连接数为非0。
  • 如果对方tcp发送一个FIN(对方进程终止),套接字就变为可读且read返回0;
  • 有一个套接字错误待处理。

    套接字可写条件

  • 套接字发送缓冲区的可用空间大于等于套接字发送缓冲区的限度的当前值;
  • 套接字的写这一半关闭,对套接字的写将产生SIGPIPE信号;
  • 有一个套接字错误待处理

    套接字异常条件

  • 套接口带外数据的到达;
  • 控制状态信息的存在;

实现I/O多路复用步骤

  1. 清空描述符集合;
  2. 建立需要监视的描述符与描述符集合的联系;
  3. 调用select()函数;
  4. 检查所有需要监视的描述符,利用FD_ISSET宏判断是否已准备好;
  5. 对已准备好的描述符进行I/O操作。
CATALOG
  1. 1. 套接字编程
    1. 1.1. 套接字类型
    2. 1.2. 套接字地址结构
      1. 1.2.1. IPv4
      2. 1.2.2. IPv6
    3. 1.3. 通用套接字地址结构
    4. 1.4. 基本函数
      1. 1.4.1. 字节排序函数
      2. 1.4.2. 字节操纵函数
      3. 1.4.3. IP地址转换函数
      4. 1.4.4. inet_pton的实现(只支持ipv4)
    5. 1.5. TCP套接字编程
      1. 1.5.1. 服务器端
      2. 1.5.2. 客户端
      3. 1.5.3. TCP服务器模板
      4. 1.5.4. TCP客户端模板
    6. 1.6. 基本套接字函数
      1. 1.6.1. socket
      2. 1.6.2. bind
      3. 1.6.3. setsockopt
      4. 1.6.4. listen
      5. 1.6.5. connect
      6. 1.6.6. accept
      7. 1.6.7. close
      8. 1.6.8. shutdown
      9. 1.6.9. read
      10. 1.6.10. write
    7. 1.7. 数据传输函数
      1. 1.7.1. send
      2. 1.7.2. recv
    8. 1.8. 域名解析函数——gethostbyname
    9. 1.9. 读取文件函数
    10. 1.10. UDP套接字编程
      1. 1.10.1. 服务器端
      2. 1.10.2. 客户端
      3. 1.10.3. 服务器端模板
      4. 1.10.4. 客户端模板
    11. 1.11. UDP数据传输函数
      1. 1.11.1. sendto
      2. 1.11.2. recvfrom
    12. 1.12. 注意复习实验内容和作业内容
  2. 2. 多线程多进程并发服务器
    1. 2.1. 进程
      1. 2.1.1. 基本概念
      2. 2.1.2. 进程操作
        1. 2.1.2.1. 创建新进程
        2. 2.1.2.2. 终止进程
        3. 2.1.2.3. 获取子进程终止信息
      3. 2.1.3. 多进程并发服务器模板
      4. 2.1.4. 父子进程对套接字处理
      5. 2.1.5. 僵尸进程与孤儿进程
        1. 2.1.5.1. 僵尸进程
        2. 2.1.5.2. 孤儿进程
    2. 2.2. 线程
      1. 2.2.1. 基本概念
      2. 2.2.2. 线程操作
        1. 2.2.2.1. 创建线程
        2. 2.2.2.2. 等待线程终止
        3. 2.2.2.3. 分离线程
        4. 2.2.2.4. 获取ID
        5. 2.2.2.5. 终止线程
        6. 2.2.2.6. 一次执行
        7. 2.2.2.7. 取消机制
      3. 2.2.3. 给新线程传递参数的方法
      4. 2.2.4. 多线程并发服务器模板
    3. 2.3. 互斥锁
      1. 2.3.1. 函数
  3. 3. 线程安全性编程
    1. 3.1. 函数
      1. 3.1.1. 分配函数
      2. 3.1.2. 绑定函数
      3. 3.1.3. 获取值
      4. 3.1.4. 删除函数
  4. 4. I/O复用
    1. 4.1. 五个模型
      1. 4.1.1. 阻塞I/O
      2. 4.1.2. 非阻塞I/O
      3. 4.1.3. I/O复用
      4. 4.1.4. 信号驱动I/O
      5. 4.1.5. 异步I/O
      6. 4.1.6. 比较
    2. 4.2. 同步与异步
    3. 4.3. select函数
      1. 4.3.1. 函数体
      2. 4.3.2. 参数说明
      3. 4.3.3. 套接字可读条件
      4. 4.3.4. 套接字可写条件
      5. 4.3.5. 套接字异常条件
      6. 4.3.6. 实现I/O多路复用步骤