1. 程式人生 > >TCP/IP網路程式設計基礎閱讀筆記

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