乐趣区

关于c++:TCP-IP网络编程学习笔记

根本数据结构和函数

示意 IPv4 地址的构造体:

struct sockaddr_in
{
    sa_family_t sin_family; // 示意地址族
    uint16_t sin_port;     // 示意 16 位的端口号(包含 TCP 和 UDP)struct in_addr sin_addr; //32 位的 ip 地址
    char sin_zer[8] ;     // 不应用
    
};

对于 in_addr:

struct in_addr{In_addr_t s_addr;//32 位 IP 地址}

对于下面定义的成员变量类型,大部分都是在 POSIX 外面定义好了的,属于跨平台可移植的根本数据类型的别名。

对于 sockaddr_in 成员变量的剖析:

  • sin_family: 对于每种协定实用于不同的地址族。典型的有:

    • AF_INET : IPv4 协定应用的地址族
    • AF_INET6 : IPv6 协定应用的地址族
    • AF_LOCAL : 本地通信
  • sin_port: 以网络字节序列保留端口号
  • sin_zero: 字节填充符,无非凡的含意。

对于 socket 过程须要绑定 socket 地址。对应的函数是 bind

具体用法:

bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr));

这里须要留神的是 struct sockaddr* 这个数据结构。在原先老版本的 unix 网络通信中并没有 sockaddr_in 这个数据结构,只有 sockaddr

struct sockaddr 
{
    sa_family_t sin_family;
    char sa_data[14];    // 地址信息
}    

留神这里的 sa_data 数组蕴含了 sockaddr_in 数据结构中前面的所有信息,包含端口号、IP 地址等等,这样的结果是编程起来十分不不便。所以前面把这个数据结构转换为了 sockaddr_in,但同时也保障了字节序列和这个数据结构统一。因而这里即使应用了强制类型转换也不会影响数据的含意。

网络字节序列

因为不同的机器 CPU 采纳的存储策略也不同。有些采纳大端法,有些采纳小端法。为了放弃在不同机器之间进行网络通信数据格式的对立,网络序列对立采纳大端法。所以在小端法的机器上发送数据时,首先要转换为大端法。对应的零碎 API:


unsigned short htons(unsigned short);//htons:host to network , short number
unsigned long htonl(unsigned long);//host to net , long number 
unsigned short ntohs(unsigned short);//net to host, short number
…………

网络地址初始化

对于 IP 地址的示意,因为咱们大部分时候是以点分十进制表示法来了解的。然而计算机保留的是二进制序列,所以在初始化真正的 IP 地址时,咱们须要可能在点分十进制和二进制之间相互转换的 API


#include <arpa/inet.h>

in_addr_t inet_addr(const char* string);
// 胜利时返回 32 位的大端法整数值,失败返回 INADDR_NONE

int inet_aton(const char *string, struct in_addr * addr);
// 与上述的 API 不同点在于,将返回的 IP 地址间接保留到指针外部
// 失败了返回 0,胜利了返回 1

char* inet_ntoa(struct in_addr addr);
// 相同的性能

联合上述的所有内容,个别状况下 unix 的网络链接初始化的代码如下:


struct sockaddr_in addr;// 新建一个网络地址信息对象

char * serv_ip  = "211.217.168.13";    // 输出已知的点分十进制 IP 地址
char * serv_port = "9190" ; // 端口号
memest(&addr, 0, sizeof(addr));// 初始化
addr.sin_family = AF_INET;//IPv4 的协定地址族
addr.sin_addr.s_addr = inet_addr(serv_ip); // 初始化字符 IP
addr.sin_port = htons(atoi(serv_port)); // 初始化字符 port。也能够是数字

对于服务端的 IP 地址,能够应用一个随机初始化变量:INADDR_ANY。个别服务器无限思考这种模式。

最初把初始化信息调配给套接字:

#include <sys/socket.h>

int bind(int sockfd, struct sockaddr* myaddr,socklen_t addrlen);

实现基于 TCP 的服务端

TCP 服务端默认的函数调用程序

socket(); // 创立套接字
|
bind();// 分配套接字地址
|
listen();// 监听客户端的申请
|
accept();// 容许连贯
|
read()/write();// 数据交换
|
close();// 敞开连贯 

期待连贯申请状态

在服务期端的体现为 listen 函数

#include <sys/socket.h>
int listen(int sock    //the socket in server
           , int backlog);//the size of waiting queue

只有当服务器端处于监听状态能力承受客户端的 connect 申请,否则会报错。

受理客户端的连贯申请

之后要对处于监听队列的客户端申请作出受理。须要应用 accept 函数

#include <sys/socket.h>

int accept(int sock,// the socket in server
           struct sockaddr * client_addr,    // 客户端的 socket 地址详细信息
           socketlen_t * addrlen);    // 下面的变量的长度
    

hello_server 服务端实例

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
void error_handling(char *message);


int main(int argc,char *argv[]){
    int serv_sock;
    int clnt_sock;
    struct sockaddr_in serv_addr;
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size;
    char msg[]  = "Hello World!";

    if(argc != 2){printf("Usage: %s <port>\n",argv[0]);
        exit(1);
    }
    serv_sock = socket(PF_INET,SOCK_STREAM,0);//create a TCP socket

    if(serv_sock == -1){error_handling("socket() error");
    }

    memset(&serv_addr,0,sizeof(serv_addr));

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    serv_addr.sin_port = htons(atoi(argv[1]));

    if(bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) == -1){error_handling("bind() error");
    }

    if(listen(serv_sock,5) == -1){error_handling("listen() error");
    }

    clnt_addr_size = sizeof(clnt_addr);
    clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_addr, &clnt_addr_size);
    if(clnt_sock == -1){error_handling("accept() error");

    }

    write(clnt_sock,msg,sizeof(msg));

    close(clnt_sock);
    close(serv_sock);
    return 0;
}

void error_handing(char* message){fputs(message, stderr);
    fputc('\n',stderr);
    exit(-1);
}

实现基于 TCP 的客户端

客户端默认的函数调用程序

socket();// 创立套接字
|
connect();// 申请连贯
|
read()/write();// 替换数据
|
close();// 敞开连贯

申请连贯


#include<sys/socket.h>

int connect(int sock,// 客户端的套接字文件描述符
            struct sockaddr* servaddr,// 指标服务器的地址信息变量地址
            socklen_t addrlen);

客户端调用这个申请的时候,只有当服务器端承受了连贯或者产生异常中断的时候才会失去返回后果。

hello_client 客户端实例

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
void error_handling(char *message);

int main(int argc,char **argv){
    int sock;
    struct sockaddr_in serv_addr;
    char msg[30];
    int str_len;

    if(argc != 3){printf("Usage : %s <IP> <port>\n",argv[0]);
        exit(1);
    }
    
    sock = socket(PF_INET,SOCK_STREAM,0);

    if(sock == -1){error_handling("socket() error");

    }

    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));

    if(connect(sock,(struct sockaddr*) &serv_addr,sizeof(serv_addr)) == -1){error_handling("connect() error");
    }

    str_len = read(sock,msg,sizeof(msg)-1);
    if(str_len == -1){error_handling("read() error");
    }

    printf("Message from server : %s\n",msg);
    close(sock);
    return 0;

}

void error_handling(char* message){fputs(message,stderr);
    fputc('\n',stderr);
    exit(1);
}

TCP 套接字的缓冲

server 端的输入端就是 client 端的输出端。同时,每个端口都配有缓冲区,单方进行通行的时候,首先把数据放到缓冲区上,而后再从缓冲区进行 IO:read 就是把对应的字符流退出到缓冲区,write 就是把对应的字符流从缓冲中取出

基于 UDP 的服务端和客户端

UDP 与 TCP 的最次要区别:

TCP 存在流控制,而 UDP 则不存在流控制

同时在编程的时候,理论不存在服务端和客户端的概念,只有发送端和和接收端。因为 TCP 连贯中,服务端有很多的 socket 过程,每一个 socket 对应一个客户端 socket。而 UDP 连贯中不存在这样的货色,哪怕是服务端也只有一个 UDP 套接字。所以实际上在 UDP 连贯中,客户端和服务端是处于对等的状态

罕用 API

#include <sys/socket.h>

ssize_t sendto(int sock,    // 用于传输数据的套接字文件描述符
               void *buff,    // 带传输的数据地址
               size_t nbytes,    // 须要传输的字节大小
              
               int flags,    // 选项参数
               struct sockaddr* to,    // 指标地址的详细信息
               socklen_t addrlen);  // 传递给参数 to 的地址值构造体变量长度

ssize_t recvfrom(int sock,    // 用于传输数据的套接字文件描述符
               void *buff,    // 带承受数据的地址
               size_t nbytes,    // 须要承受的字节大小
              
               int flags,    // 选项参数
               struct sockaddr* from,    // 源地址的详细信息
               socklen_t addrlen);  // 传递给参数 from 的地址值构造体变量长度 

IP 地址和端口调配

咱们都晓得,

断开连接

以上所有代码的断开连接咱们都是应用了 close。然而咱们之前也晓得,一个 socket 同时领有输出流和输入流,如果间接 close,就会把输入输出流同时关掉。思考这样的一个场景,服务器要发送大量的数据给客户端,而客户端给服务端响应的内容只有几个字节。因为客户端收到数据后还有进行一系列的操作,如果只为了等那么点信息而始终放弃 socket 关上显然是不划算的。所以当咱们把数据发送完之后,能够抉择敞开服务端的输入流,共事放弃输出流关上,这样就节俭了肯定的资源,又可能放弃和客户端的通信。

API

int shutdown(int sock, // 套接字的文件描述符
             int howto)// 如何敞开,能够全关,能够只关输出 / 输入流 

域名零碎编程

利用域名获取 IP 地址

次要用到的 API:

#include<netdb.h>
struct hostent* gethostbyname(const char* hostname);

struct hostent {
    char * h_name;     // 官网域名
    char ** h_aliases;    // 通过多个域名可能拜访同一 IP,这里保留对应的别名
    int h_addrtype; //IPv4 or IPv6 or else
    int h_length;   //IP 地址长度
    char** h_addr_list; // 对应的 IP 地址列表
    
}

前面的 IP 地址列表的变量类型是 char**,但实际上指向的是 in_addr 构造体变量的地址。所以在取得这个成员属性的时候还要做适当的转换。

inet_ntoa(*(struct in_addr*)web_info->h_addr_list[i])

这样就可能失去字符串模式的主机模式 IP 地址

利用 IP 地址获取域名

次要 API:

struct hostent* gethostbyaddr(const char* addr,  // 含有 IP 信息的 in_addr 构造体指针
                              socklen_t len,    //IPv4 的时候为 4,IPv6 为 16
                              int family);        // 传递地址族信息 

过程相干 API

僵尸过程

失常状况下,当父过程生成子过程的时候,期待接管子过程调用的返回后果。

如果父过程收到了这个后果,那么就能够被动销毁子过程;而如果没有,那么就必须让子过程始终不停的在运行,直到父过程本人的工作全副完结。

但即便子过程调用 return 或者 exit 函数传递返回值,这些返回值并不会被父过程接管,它们首先被操作系统接管。之后当父过程调用某些特定的函数后才可能从操作系统中获取这些信息进而销毁子过程

销毁僵尸过程的形式

利用 wait 函数

#include<sys/wait.h>
pid_t wait(int * statloc);// 将子过程的返回值放入到 statloc 指针当中。/*
然而返回的内容中还有其余的货色,须要进行宏拆散
*/

WIFEXITED(status): 子过程失常终止时返回 true
WEXITSTATUS(status): 返回子过程的返回值 

利用 waitpid 函数

pid_t waitpid(pid_t pid,     // 期待被终止的子过程 ID,如果为 -1,则能够是任意子过程
              int* statloc, // 与 wait 函数含意统一
              int options); // 若传递 WNOHANG,即便没有终止的子过程也不会进入阻塞状态,而是返回 0 并退出

胜利时返回终止子过程的 ID 或者 0,失败返回 -1

demo:

#include"common.h"
int main(int argc, char **argv){
    int status;
    pid_t pid = fork();
    if(pid == 0){sleep(15);
        exit(24);
    }
    else{while (!waitpid(pid,&status,WNOHANG))
        {sleep(3);
            printf("the parent process sleep for 3 seconds\n");
        }
        if(WIFEXITED(status)){printf("the child process finished\n,return value is: %d\n",WEXITSTATUS(status));
        }
        
    }
    return 0;
}

waitpid 相比于下面的 wait 函数,益处在于:

  1. 可能针对某一个特定的子过程进行销毁操作,而不是像 wait 函数一样依据工夫的先后顺序。灵活性更强
  2. 在 options 参数外面能够设置为非阻塞模式,在某些场景下会更加适宜

signal 信号处理

信号处理是在特定的事件产生之后,操作系统向过程发送音讯,为了响应这个音讯,执行与音讯相干的过程就被称为“解决”或者“信号处理”

signal 函数

#include<signal.h>

void (*signal (int signo, // 信号的类型
               void (*function)(int))) // 解决信号的函数指针(能够了解为 handler)(int);// 因为返回的是函数指针,所以须要返回给参数为 int 的某个函数

第一个参数罕用的值:

  • SIGALRM: 曾经通过调用 alarm 函数注册的工夫,也就是说 alarm 正式失效,收回 alarm 信号
  • SIGINT:通过 Ctrl+ C 按钮终止
  • SIGCHILD:子过程终止

alarm 函数:

#include <unistd.h>

unsigned int alarm(unsigned int seconds);
    返回 0 或者以秒为单位的间隔 SIGALRM 信号产生所剩工夫
       

demo:

#include"common.h"
void timeout(int sig){if(sig == SIGALRM){puts("time out");
    }
    alarm(2);
}

void keycontrol(int sig){if(sig == SIGINT){puts("Ctrl + C pressed");
    }
}

int main(int argc,char **argv){
    int i;
    signal(SIGINT,&keycontrol);
    signal(SIGALRM,&timeout);
    alarm(2);
    for(i =0 ;i<1000;i++){puts("wait....");
        sleep(109);
    }
    return 0;
}

sigaction 函数

之前所用的 signal 函数目前应用的比拟少了,大部分时候咱们都会用 sigaction 这个函数来进行信号处理。sigaction 函数的劣势在于它在不同的 UNIX 操作系统中基本上统一。

API:

#include<signal.h>
int sigaction(int signo,    // 信号的信息
              const struct sigaction * act,    // 对应于第一个参数的信号处理动作
              struct sigaction* oldact);    // 获取之前注册的信号处理指针,若不须要置为 0

struct sigaction{void (*sa_handler)(int);    // 保留信号处理函数的指针值
    sigset_t sa_mask;    // 应用 sigemptyset(addr) 进行初始化
    int sa_flags;        //0
}

利用信号处理函数敞开僵尸过程

原理: 利用子过程应用零碎调用 exit 或者 return 时,在操作系统中产生子过程完结信号。这个时候只须要咱们写一个处理器,专门用于解决这种僵尸过程,而后把这个处理器和该信号绑定即可。这样,每当产生一个子过程完结信号的时候,就会主动调用 read_childproc 函数。

#include"common.h"

//handle the zombie process
void read_childproc(int pid){
    int status;
    //because we randomly kill zombie process,"id" is to get the kiled pid
    pid_t id = waitpid(-1,&status,WNOHANG);

    if(WIFEXITED(status)){printf("Removed proc id: %d\n",id);
        printf("Child send: %d\n",WEXITSTATUS(status));
    }
}

int main(int argc,char **argv){
    pid_t pid;
    struct sigaction act;
    act.sa_handler = read_childproc;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    //whenever OS produce a child process end signal, out program will call the handler function
    sigaction(SIGCHLD, &act,0);
    pid = fork();

    if(pid == 0){puts("Hi! I'm chlild process\n");
        sleep(10);
        return 12;
    }
    else{printf("Child proc id: %d \n",pid);
        pid = fork();

        if(pid == 0){puts("Hi! I'm child process");
            sleep(10);
            return 24;
        }
        else{
            int i ;
            printf("Chlid proc id: %d\n",pid);
            for(int i = 0;i<5;i++){puts("wait..");
                sleep(5);
            }
        }
    }
    return 0;
}

多过程服务器

demo:

#include"common.h"
int main(int argc,char **argv){
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;

    pid_t pid;
    struct sigaction act;
    socklen_t adr_sz;
    int str_len, state;
    char buf[BUF_SIZE];
    if(argc != 2){
        printf("Usage : %s <port> \n",argv[0]
        );
        exit(1);
    }
    act.sa_handler = read_childproc;
    serv_sock = socket(PF_INET,SOCK_STREAM,0);
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    state = sigaction(SIGCHLD,&act,0);
    memset(&serv_adr,0,sizeof(serv_adr));

    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));

    if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1){error_handling("bind() error");
    }

    if(listen(serv_sock,5) == -1){error_handling("listen() error");
    }

    while (1)
    {adr_sz = sizeof(clnt_adr);
        clnt_sock = accept(serv_sock,(struct sockaddr*) &clnt_adr, &adr_sz);
        if(clnt_sock == -1){continue;}
        puts("new client connected....");
        pid = fork();
        if(pid == 0){close(serv_sock);
            while ((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0)
            {write(clnt_sock,buf,str_len);
            }
            close(clnt_sock);
            puts("connection lost..");
            return 0;
        }
        else
        {close(clnt_sock);
        }
    }
    close(serv_sock);
    return 0;
}

须要留神的是,子过程拷贝父过程的内存,然而文件描述符存在于操作系统中的一张表中,复制的是援用,实际上二者指向的是同一个套接字对应的硬件资源。而套接字必须要等所有指向它的文件描述符全副敞开才会被销毁,因而在子过程中必须每次都开释服务器的套接字文件描述符

利用过程对客户端 I / O 拆散

之前客户端程序的重复 read/write 实际上节约了很多的工夫,read 操作必须要等到服务器端将对应的数据写入到 socket 中才可能实现。而实际上咱们能够把 IO 离开,在 write 的同时就让 read 操作进入到忙期待状态,这样当处于低网速状态的时候可能十分显著地晋升效率

#include"common.h"

void read_routine(int sock, char* buf);
void write_routine(int sock, char* buf);
int main(int argc ,char **argv){
    int sock;
    struct sockaddr_in serv_addr;
    char message[BUF_SIZE];
    int str_len,recv_len,// 用于统计以后的承受数据长度
        recv_cnt;// 统计每一次接收数据的长度

    if(argc != 3){printf("Usage : %s <IP> <port>\n",argv[0]);
        exit(1);
    }
    
    sock = socket(PF_INET,SOCK_STREAM,0);

    if(sock == -1){error_handling("socket() error");

    }

    memset(&serv_addr,0,sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    serv_addr.sin_port = htons(atoi(argv[2]));

    if(connect(sock,(struct sockaddr*) &serv_addr,sizeof(serv_addr)) == -1){error_handling("connect() error");
    }
    puts("Connected.....");

    pid_t pid = fork();
    if(pid == 0){write_routine(sock,message);
    }
    else{read_routine(sock,message);
    }
    
    close(sock);
    return 0;
}

void read_routine(int sock,char *buf){while(1){int str_len = read(sock,buf,BUF_SIZE);
        if(str_len == 0) return;
        buf[str_len] = 0;
        printf("Message from server: %s\n",buf);
    }
}

void write_routine(int sock,char *buf){while(1){fgets(buf,BUF_SIZE,stdin);
        if(!strcmp(buf,"q\n") || !strcmp(buf,"Q\n")){shutdown(sock, SHUT_WR);
            return;
        }
        write(sock,buf,strlen(buf));
    }
}

I/ O 复用

背景

后面讲到的多过程服务端模型对于每一个来自客户端的申请,都会创立一个新的过程用来解决服务端的 IO 操作。而事实上,能够把不同套接字的 IO 操作集成到一个过程上来对立实现,这就是 IO 复用

select 函数

#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);// 超时设置
/*
fd_set 的构造 
 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | ....
fd0 fd1  fd2 fd3 fd4.......
设置为 1 示意对于该文件描符进行监听
*/



多线程

线程创立和执行


#include <pthread.h>
int pthread_create(
    pthread_t * restrict thread,// 保留线程的 ID
    const pthread_attr_t * restrict attr,// 默认为 NULL,高阶参数
    void * (* start_routine)(void *),// 对应的线程事件,传递函数指针
    void * restrict arg// 传递参数的变量地址
    
)
    
   // 胜利则返回 0,失败则返回其余的值 
int pthread_join(
    pthread_t thread, // 线程的编号
    void ** status // 线程对应的状态,也就是事件返回的指针变量的值
)

线程同步

案例:

#include "common.h"

long long num = 0;

void* inc(void* arg){for(int i = 0;i<5000000;i++){num++;}
    return NULL;
}

void * dec(void *arg){for(int i = 0;i<5000000;i++){num--;}
    return NULL;
}
int main(int argc,char ** argv){
    pthread_t t_id1,t_id2;
    int thread_para1,thread_para2;
    void *ret_ptr1,*ret_ptr2;
    if(pthread_create(&t_id1,NULL,inc,(void*)&thread_para1) != 0){fprintf(stderr,"error!");
        exit(1);
    }

    if(pthread_create(&t_id2,NULL,dec,(void*)&thread_para2) != 0){fprintf(stderr,"error2");
        exit(1);
    }

    if(pthread_join(t_id1,&ret_ptr1) != 0){fprintf(stderr,"error!");
        exit(1);
    }

    if(pthread_join(t_id2,ret_ptr2) != 0){exit(1);
    }

    printf("the value of num is : %lld\n",num);
}   

以上案例是为了证实线程取数据的异样。最初的运行后果(预期为 0):

<img class=”lazy” referrerpolicy=”no-referrer” data-src=”C:\Users\lelouch\AppData\Roaming\Typora\typora-user-images\image-20211027130704529.png” alt=”image-20211027130704529″ style=”zoom:100%;” />

能够看到,每一次运行后果都不雷同。起因是 num 在运行的时候是放在寄存器上,每一次对 num++ 或者 – 的时候,在 cpu 上复制 num 的值,进行加或者减,而后再笼罩回对应的寄存器上。如果恰好两个 ++ 操作同时进行,那么 num 的值同时被更新了两次,在数值上只被 + 了一次。所以须要线程同步,也就是说,不能同时有两个线程对 num 进行操作。

互斥量 API(Mutex):

互斥量初始化:

int pthread_mutex_init(pthread_mutex_t * mutex,const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t * mutex);

锁操作:

int pthread_mutex_lock(pthread_mutex_t * mutex);
int pthread_mutex_unlock(pthread_mutex_t * mutex);

信号量 API(Semaphore):

// 信号量的销毁以及创立
#include<semaphore.h>

int sem_init(sem_t * sem, // 信号量的根底变量
            
             int pshared, // 传递 0 示意只可能被一个线程享受
             unsigned int value);// 创立信号量的值
int sem_destroy(sem_t * sem);// 销毁信号量 

信号量的 P、V 操作

int sem_post(sem_t * sem);// 对信号量的值 +1
int sem_wait(sem_t * sem);// 对信号量的值 -1

sem_wait 就相当于用掉一个临界资源。比方输出缓冲区的大小为 1,那么在一开始没有输出的时候,输出缓冲的信号量为 1。当获取输出值之后,输出缓冲被用掉,那么就调用一次 sem_wait,把信号量的值置为 0,示意不可能有其余的线程再来拜访以后的输出缓冲区;直到某个线程取走了这个输出数字,输出缓冲再次空进去,那么就用 sem_post,信号量 +1。对于输入缓冲也是一个情理。

案例:线程 A 从终端输出,线程 B 获得这个输出,并把输出的值加到 num 中。

#include "common.h"

static sem_t sem_in,sem_out;
static int num,tmp;

void *produce(void *arg){for(int i = 0;i<5;i++){sem_wait(&sem_in);
        scanf("%d",&tmp);
        sem_post(&sem_out);
    }  
}

void *consume(void *arg){for(int i =  0;i<5;i++){sem_wait(&sem_out);
        num += tmp;
        tmp = 0;
        sem_post(&sem_in);
    }
}

int main(int argc,char ** argv){
    pthread_t t_in,t_out;
    int pthread_para1 ,pthread_para2 = 0;
    void **ret_adr;
    //at first the input buffer size is 1,so we need to 
    //set the sem_in value is 1
    sem_init(&sem_in,0,1);
    sem_init(&sem_out,0,0);
    
    pthread_create(&t_in,NULL,produce,(void*)&pthread_para1);

    pthread_create(&t_out,NULL,consume,(void*) &pthread_para2);

    pthread_join(t_in,ret_adr);
    pthread_join(t_out,ret_adr);


    sem_destroy(&sem_in);
    sem_destroy(&sem_out);

    printf("%d\n",num);
    
}
退出移动版