目錄
創建——shmget() ——SHare Memory GET
概述
每個進程擁有獨立進程空間的優缺點
優點:
- 對編程人員來說,系統更容易捕獲隨意的內存讀取和寫入操作
- 對用戶來說,操作系統將變得更加健壯,因爲一個應用程序無法破壞另一個進程或操作系統的運行(防止被攻擊)
缺點:
- 多任務實現開銷較大
- 編寫能夠與其他進程進行通信,或者能夠對其他進程進行操作的應用程序將要困難得多
進程間通信的原理
儘管進程空間是各自獨立的,相互之間沒有任何可以共享的空間,但是至少還有一樣東西是所有進程所共享的,那就是OS。
因此進程間通信的原理就是,OS作爲所有進程共享的第三方,會提供相關的機制,以實現進程間數據的轉發,達到數據共享的目的。
爲什麼進程間要通信
- 數據傳輸:一個進程需要將它的數據發送給另一個進程
- 資源共享:多個進程之間共享同樣的資源
- 通知事件:一個進程需要向另一個或一組進程發送消息,通知它們發生了某種事情
- 進程控制:有些進程希望完全控制另一個進程的執行(如Debug進程),此時控制進程希望能夠攔截另一個進程的所有操作,並能夠及時知道它的狀態改變。
管道通信
什麼是管道
通過內核產生一個管道文件,通過對這個管道文件的讀和寫來做通信。
管道包括無名管道和有名管道兩種。
特點
- 無名管道只能用於有血緣關係(如父子進程)的進程之間的通信,有名管道可用於任意兩個進程間的通信。
- 傳輸的是無格式的數據/字符流,也就是說讀多個數據其中的一個數據,是比較難的。
- 只能做小數據的傳輸(比如傳輸一個文件描述符,簡單的字符串,指令)(所以最後引出了消息隊列)
無名管道——pipe()
- 管道只允許具有血緣關係的進程間通信,如父子進程間的通信
- 管道只允許單向通信(如何實現雙向通信?再建一個管道。。收發端正好反一反,對一方來講,兩個管道一個用來讀一個用來寫)
- 讀管道時,如果沒有數據的話,讀操作會休眠(阻塞),寫數據時,緩衝區寫滿會休眠(阻塞)(當管道滿了再往裏面寫也會阻塞)
#include <unistd.h>
int pipe(int filedes[2]);
/*
當一個管道建立時,它會創建兩個文件描述符:
filedis[0]用於讀管道(把東西從裏面讀出來),filedis[1]用於寫管道(把東西寫到裏面)。filed就相當於文件描述符了
*/
必須在系統調用fork()前調用pipe(),否則子進程將不會繼承文件描述符。
管道讀寫用read,write函數,把它當做文件讀寫
管道里的數據讀出來了就沒有了
讀寫管道數據時記得把另一個管道口關掉
例.
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
pid_t pid;
int pipefd[2];
char write_buf[50] = {0};
char read_buf[50] = {0}; //別忘記賦初值!不然read_buf讀數據輸出時會有奇怪的字符
int ret;
ret = pipe(pipefd);
if(ret < 0)
{
perror("pipe");
return -1;
}
pid = fork();
if(pid < 0)
{
perror("fork:");
return -1;
}
if(0 == pid) //子進程讀
{
close(pipefd[1]);
read(pipefd[0],read_buf,sizeof(read_buf)); //子進程一直接收不到就會一直堵塞,讓父進程先執行
printf("child pid %d read_buf is %s\n",getpid(),read_buf);
close(pipefd[0]);
}
else if(pid > 0) //父進程寫
{
close(pipefd[0]); //因爲它是寫,先關閉讀的管道
printf("parent pid is %d, please input write_buf:",getpid());
scanf("%s",write_buf);
write(pipefd[1],write_buf,strlen(write_buf));
wait(NULL); //不需要讀取子進程返回值,寫NULL
close(pipefd[1]);
}
return 0;
}
命名管道——mkfifo()
無名管道只能由父子進程使用;但是通過命名管道,不相關的進程也能交換數據。一個有名管道同樣不法實現雙向通信。
FIFO不同於管道之處在於它提供一個路徑名與之關聯,以FIFO的文件形式存在於文件系統中。這樣,即使與FIFO的創建進程不存在親緣關係的進程,只要可以訪問該路徑,就能夠彼此通過FIFO相互通信(能夠訪問該路徑的進程以及FIFO的創建進程之間)。
使用步驟
- 進程調用mkfifo創建有名管道
- open打開有名管道
- read/write讀寫管道進行通信
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
/*
參數:
pathname: FIFO文件名(該文件內不會真的有值寫入)
mode:屬性(同文件操作),一般設置爲0664(110110100),必須包含讀寫權限
O_RDONLY,O_WRONLY
返回值;
成功返回0,失敗則返回-1,並且errno被設置
*/
- “有名管道”這種特殊文件,只能使用mkfifo函數來創建
- 爲了保證管道一定被創建,最好是兩個進程都包含創建管道的代碼,誰先運行就誰先創建,後運行的發現管道已
經創建好了,那就直接open打開使用。(一般的文件訪問函數(close、read、write等)都可用於FIFO。) - 不能以O_RDWR模式打開命名管道FIFO文件,否則其行爲是未定義的,管道是單向的,不能同時讀寫;
當打開FIFO時,非阻塞標識(O_NONBLOCK)將對以後的讀寫產生影響:
1、沒有使用O_NONBLOCK:訪問要求無法滿足時進程將阻塞。如果試圖讀取空的FIFO,將導致進程阻塞。
2、使用O_NONBLOCK:訪問要求無法滿足時不阻塞,立刻出錯返回。errno是ENXIO。
寫:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
int main()
{
int ret;
int fd;
char buf[100] = {0};
//一開始我放的路徑是/mnt/hgfs/share/2019/0420/namedfifo但會報錯可能沒有權限,所以管道文件放在home目錄下比較好
if(ret = mkfifo("/home/namedfifo",0666) < 0) //0666表示可讀可寫
{
if(!(errno == EEXIST && ret == -1)) //這個操作可以使即使fifo文件已創建了
//也能繼續後面的操作
{ //不用規定哪個先執行哪個後執行
perror("mkfifo");
return -1;
}
}
fd = open("/home/namedfifo",O_WRONLY);
if(fd < 0)
{
perror("fd");
return -1;
}
while(1)
{
printf("write buf:\n");
scanf("%s",buf);
write(fd,buf,strlen(buf));
memset(buf,0,sizeof(buf));
}
return 0;
}
讀:
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
int main()
{
int fd;
char buf[100] = {0};
int ret;
if(ret = mkfifo("/home/namedfifo",0666) < 0) //0666表示可讀可寫
{
if(!(errno == EEXIST && ret == -1)) //這個操作可以使即使fifo文件已創建了
//也能繼續後面的操作
{ //不用規定哪個先執行哪個後執行
perror("mkfifo");
return -1;
}
}
fd = open("/home/namedfifo",O_RDONLY);
if(fd < 0)
{
perror("open");
return -1;
}
while(1)
{
read(fd,buf,sizeof(buf));
printf("get buf %s\n",buf);
memset(buf,0,sizeof(buf));
}
return 0;
}
popen()+pclose()
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
/*
函數功能:
popen函數允許一個程序將另一個程序作爲新進程來啓動,並可以
傳遞數據給它或者通過它接收數據。
pclose函數只在popen啓動的進程結束後才返回。如果調用pclose時
它仍在運行,pclose將等待該進程的結束。
參數:
command字符串:要運行的程序名和相應的參數。
type:必須是"r"或"w"。
如果type是"r",被調程序的輸出就可以被調用程序使用,調用程序
利用popen函數返回的FILE *文件流指針,可以讀取被調程序的輸出;
如果type是"w",調用程序就可以向被調程序發送數據,而被調程序可
以在自己的標準輸入上讀取這些數據。
*/
例.
#include <stdio.h>
int main()
{
//讀
#if 0
char buf[100] = {0};
FILE* fp = popen("ls /home","r");
if(fp == NULL)
{
perror("popen");
return -1;
}
fread(buf,sizeof(char),sizeof(buf)/sizeof(char),fp);
printf("buf is %s\n",buf);
#endif
//寫
char buf[100] = "abcdefg";
FILE* fp = popen("cat > 1","w");
if(fp == NULL)
{
perror("popen");
return -1;
}
fwrite(buf,sizeof(char),sizeof(buf)/sizeof(char),fp);
return 0;
}
結果
關閉管道
關閉管道只需要將兩個文件描述符關閉即可,可以使用普通的close函數逐個關閉。
消息隊列(之後用的比較多)
消息隊列的本質就是由內核創建的用於存放消息的鏈表。(由於是存放消息的,所以我們就把這個鏈表稱爲消息隊列)
特點
- 傳送有格式的消息流(相較於管道,因爲讀消息的函數可以設置id,即讀指定的數據)
- 多進程網狀交叉通信時,消息隊列是上上之選
- 能實現大規模數據的通信
分類
- System V的消息隊列: 對此消息隊列的讀則可以返回任意指定優先級的消息,以下說的均是此隊列
- Posix消息隊列:對此消息隊列的讀總是返回最高優先級的最早消息
消息隊列的組成
struct msgbuf //該結構體可以隨便取名,但成員一點要包含以下兩個
{
long mtype; /* 放消息編號,必須> 0 ,識別消息用*/
char mtext[msgsz]; /* 消息內容(消息正文) */
};
使用步驟
- 先創建一個消息隊列(使用msgget函數創建新的消息隊列、或者獲取已存在的某個消息隊列,並返回唯一標識消息隊列的
標識符(msqID),後續收發消息就是使用這個標識符來實現的。) - 發送,接收(發送接受的都是結構體),發送前把發送的結構體先創建了,接收時除了消息隊列標識符,還要傳遞你要接收消息的編號
- 用完關閉,但它不會自己刪除(持續性),一直在內核中存在,除非重啓纔會自動刪除。使用msgctl函數刪除
生成鍵值——ftok()
消息隊列描述符(即ID)的生成與鍵值key有關,key不一樣id就不一樣。爲了防止鍵值key的重複,可使用該函數,通過指定文件名與一個char型字符,生成對應的唯一鍵值。
(但通常爲了方便,直接自己寫一個整數,然後強制類型轉換成key_t類型即可)
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(char *pathname, int proj);
/*
函數功能:
通過指定路徑名和一個整形數,就可以計算並返回一個唯一對應的key值,只要路徑名和整形數不變,所對應的key值就唯一不變的。
參數:
pathname:任意已創建文件的文件名(該文件必須是存在而且可以訪問的,可以提前創建一下)
proj:由於ftok只會使用整形數(proj_id)的低8位,因此我們往往會指定爲一個ASCII碼值,因爲ASCII碼值剛好是8位的整形數。(比如賦一個'a')
*/
打開/創建——msgget()
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg)
/*
參數:
key: 鍵值,用於爲消息隊列生成(計算出)唯一的消息隊列ID。
我們可以指定三種形式的key值:
第一種:指定爲IPC_PRIVATE宏,指定這個宏後,每次調用msgget時都會創建一個新的消息隊列。如果你每次使用的必須是新消息隊列的話,就可以指定這個,不過這個用的很少。因爲一般來說,只要有一個消息隊列可以用來通信就可以了,並不需要每次都創建一個全新的消息隊列。
第二種:可以自己指定一個整形數,但是容易重複指定本來我想創建一個新的消息隊列,結果我所指定的這個整形數,之前就已經被用於創建某個消息隊列了,當我的指定重複時,msgget就不會創建新消息隊列,而是使用的是別人之前就創建好的消息隊列。所以我們也不會使用這種方式來指定key值。
第三種:使用ftok函數來生成key
msgflg:標誌位,取值:0644 消息隊列讀取權限,與下面或一下
IPC_CREAT:創建新的消息隊列,如果不存在則創建,存在就直接使用
IPC_EXCL:與IPC_CREAT一同使用,表示如果要創建的消息隊列已經存在,則返回錯誤。
IPC_NOWAIT:讀寫消息隊列要求無法得到滿足時,不阻塞。
在以下兩種情況下,將創建一個新的消息隊列:如果沒有與鍵值key相對應的消息隊列,並且
msgflg中包含了IPC_CREAT標誌位。
返回值:
與鍵值key相對應的消息隊列的描述符。失敗返回-1
*/
發送消息——msgsnd()
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
struct msgbuf
{
long mtype; // 消息類型 > 0
char mtext[1]; // 消息數據的首地址
}
int msgsnd(int msqid, struct msgbuf * msgp, int msgsz, int msgflg)
/*
函數功能:
向消息隊列中發送一條消息
參數:
msqid:消息隊列描述符
msgp:消息隊列指針,指向存放消息的結構
msgsz:消息數據長度
msgflg:
0:阻塞發送消息也就是說,如果沒有發送成功的話,該函數會一直阻塞等,直到發送成功爲止。
IPC_NOWAIT:非阻塞方式發送消息,不管發送成功與否,函數都將返回也就是說,發送不成功的的話,函數不會阻塞。
返回值:
成功返回0,失敗返回-1
*/
接收消息——msgrcv()
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgrcv(int msqid, struct msgbuf* msgp, int msgsz, long msgtp, int msgflg)
/*
函數功能:
從msqid代表的消息隊列中讀取一個msgtyp類型的消息,並把消息存儲在
msgp指向的msgbuf結構中。在成功的讀取了一條消息以後,隊列中的這條
消息將被刪除。
*/
刪除/隊列控制
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
struct msgid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
/*
參數:
command:將要採取的動作,它可以取3個值,
IPC_STAT:把msgid_ds結構中的數據設置爲消息隊列的當前關聯值,
即用消息隊列的當前關聯值覆蓋msgid_ds的值。
IPC_SET:如果進程有足夠的權限,就把消息列隊的當前關聯值設置爲
msgid_ds結構中給出的值
IPC_RMID:刪除消息隊列
buf:指向msgid_ds結構的指針,它指向消息隊列模式和訪問權限的結構。
返回值:
成功時返回0,失敗時返回-1.
*/
例.
先運行接收的文件,因爲接收函數會阻塞,直到接收到數據。調用順序反的話會直接結束?
發送
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf
{
long mytype; //同一個msgid可以有不同的mytype值,這樣接受的時候可能大家都是同一個消息隊列,但能接受不同的消息
char mbuf[50];
};
int main()
{
int msgid;
struct msgbuf buf;
//創建消息隊列
//int msgget(key_t key, int msgflg)
msgid = msgget(0x1111, IPC_CREAT);
if(msgid < 0)
{
perror("msgget");
return -1;
}
memset(&buf,0,sizeof(buf));
buf.mytype = 88; //這個數字可以隨便寫,以後最好用宏定義,方便時間久了查看文件理解
memcpy(buf.mbuf,"abcdefg",strlen("abcdefg"));
//發送消息
//int msgsnd(int msqid, struct msgbuf * msgp, int msgsz, int msgflg)
msgsnd(msgid,&buf,50,0);
return 0;
}
接收
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf
{
long mytype;
char mbuf[50]; //同一個msgid可以有不同的mytype值,這樣接受的時候可能大家都是同一個消息隊列,但能接受不同的消息
};
int main()
{
int msgid;
struct msgbuf buf;
//打開消息隊列
//int msgget(key_t key, int msgflg)
msgid = msgget(0x1111, IPC_CREAT);
if(msgid < 0)
{
perror("msgget");
return -1;
}
memset(&buf,0,sizeof(buf));
//接收消息
//int msgrcv(int msqid, struct msgbuf* msgp, int msgsz, long msgtp, int msgflg)
msgrcv(msgid,&buf,50,88,0);
printf("recv buf is %s\n",buf.mbuf);
return 0;
}
共享內存(用的最多)
讓同一塊物理內存被映射到進程A、B各自的進程地址空間。進程A可以即時看到進程B對共享內存中數據的更新。
不允許進程直接對物理內存做操作,所以對物理內存要做映射。
特點:
- 直接使用地址來讀寫緩存時,效率會更高,適用於大數據量的通信,通訊速度(讀寫速度)是最快的
- 減少了進入內核空間的次數(管道和消息隊列都要從用戶空間進入內核空間,再返回,開銷很大)
- 在消息上要做協議商定,傳輸的是無格式的數據(什麼數據類型都能放,但正因如此不知道傳的是什麼,要告訴對方傳的是什麼)
- 當沒有數據可讀的時候(或數據沒有刷新時),它一直在讀(不會阻塞),一直佔用CPU
- 上一步解決方法是使用strlen判斷是否爲空,空就不讀,讀完就設置爲空,但只針對字符串有效,結構體等無用
共享內存實現分幾個步驟:
- (從內存上開闢一段空間)創建共享內存,使用shmget函數,將物理地址轉化爲虛擬地址
- (做映射)映射共享內存,將這段創建的共享內存映射到具體的進程空間去,使用shmat函數。
- 最後用delete函數取消映射,把開闢的空間釋放掉
使用步驟
- 進程調用shmget函數創建新的或獲取已有共享內存
- 進程調用shmat函數,將物理內存映射到自己的進程空間
- shmdt函數,取消映射
- 調用shmctl函數釋放開闢的那片物理內存空間
創建——shmget() ——SHare Memory GET
物理內存轉化爲虛擬地址
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
/*
函數功能:創建新的,或者獲取已有的共享內存
參數:
key:1、0/IPC_PRIVATE:當key的取值爲IPC_PRIVATE,則函數shmget()
將創建一塊新的共享內存;如果key取值爲0,而參數shmflg中又設置
IPC_PRIVATE這個標誌,則同樣會創建一塊新的共享內存。
2、大於0的32位整數:視參數shmflg來確定操作。
size:1、大於0的整數:新建的共享內存大小,以字節爲單位0:
2、只獲取共享內存時指定爲0
shmflg:模式標誌參數,使用時需要與IPC對象存取權限(如0600)進行|運算
來確定共享內存的存取權限
1、0:取共享內存標識符,若不存在則函數會報錯
2、IPC_CREAT:當shmflg&IPC_CREAT爲真時,如果內核中不存在鍵值
key相等的共享內存,則新建一個共享內存;如果存在這樣的共享內存,
返回此共享內存的標識符
3、IPC_CREAT|IPC_EXCL:如果內核中不存在鍵值 與key相等的共享內存,
則新建一個共享內存;如果存在這樣的共享內存則報錯
返回值:如果成功,返回共享內存標識符;如果失敗,返回-1。
*/
映射——shmat()
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
/*
函數功能:將shmid所指向的共享內存空間映射到進程空間(虛擬內存地址),並返回映射後的起始地址(虛擬地址),有了這個地址後,就可以通過對這個地址對共享內存進程讀寫操作
參數:
shm_id:由shmget函數返回的共享內存標識。
shm_addr:指定共享內存連接到當前進程中的地址位置,通常爲空NULL,
表示讓系統來選擇共享內存的地址。也可以自己寫但不推薦。
shm_flg:指定映射條件。
通常爲0:以可讀可寫的方式映射共享內存。可以讀,也可以寫共享內存
SHM_RDONLY:以只讀方式映射共享內存,只可以讀共享內存,不能寫
返回值:如果成功,則返回共享內存映射到進程中的地址;如果失敗,則返回-1。
*/
解除映射—— shmdt()
當一個進程不再需要共享內存時,需要把它從進程地址空間中脫離
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
/*
函數功能:
該函數用於將共享內存從當前進程中分離。注意,將共享內存分離
並不是刪除它,只是使該共享內存對當前進程不再可用。
參數:
shmaddr:shmat函數返回的地址指針,
返回值:
調用成功時返回0,失敗時返回-1.
*/
刪除/共享內存控制——shmctl()
只有當所有映射取消後才能刪除共享內存
#include <sys/ipc.h>
#include <sys/shm.h>
struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
/*
參數:
shm_id:shmget函數返回的共享內存標識符。
command:要採取的操作,它可以取下面的三個值 :
IPC_STAT:把shmid_ds結構中的數據設置爲共享內存的當前關聯值,
即用共享內存的當前關聯值覆蓋shmid_ds的值。
IPC_SET:如果進程有足夠的權限,就把共享內存的當前關聯值設置爲shmid_ds結構中給出的值
IPC_RMID:刪除共享內存段(一般用這個)
buf:一個結構指針,它指向共享內存模式和訪問權限的結構。(一般寫NULL/0)
*/
例子
寫
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <string.h>
int main()
{
int shmid;
char *ptr; //指向共享空間的指針
shmid = shmget((key_t)0x1234,1024,IPC_CREAT | 0666);
if(shmid < 0)
{
perror("shmid");
return -1;
}
ptr = (char *)shmat(shmid,NULL,0);
if((void *) -1 == ptr)
{
perror("shmat");
return -1;
}
memcpy(ptr,"abdefg",sizeof("abcdefg"));
}
讀
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
int main()
{
int shmid;
int ret;
char *ptr; //指向共享空間的指針
shmid = shmget((key_t)0x1234,0,0666); //因爲共享空間已經創建了,所以申請內存寫0,第三個參數只要寫訪問方式就行了
if(shmid < 0)
{
perror("shmid");
return -1;
}
ptr = (char *)shmat(shmid,NULL,0);
if((void *) -1 == ptr)
{
perror("shmat");
return -1;
}
printf("ptr is %s",ptr);
#if 1
ret = shmdt(ptr); //解除映射
if(ret < 0)
{
perror("shmdt"a);
return -1;
}
printf(
"after shmdt ptr is %s",ptr);
#endif
#if 0
shmctl(shmid,IPC_RMID,NULL); //刪除共享內存
#endif
}
信號量(可配合共享內存改進它,用的也很多)
什麼是信號量
爲了防止因多個進程/線程同時訪問一個共享資源而出現相互干擾的情況,使用信號量對資源進行保護,使在任一時刻只能有一個進程訪問共享資源。也就是說信號量能保證多進程訪問同一共享資源時實現進程互斥與同步。
信號量能實現原子操作,原子操作:不可被打斷,不可被終止,只有本次操作結束之後,才能執行其他操作
- 二進制信號量:信號量只能取0和1
- 多值信號量:信號量的最大值 > 1,能讓多個進程同時訪問同一資源,但這樣就不能保證互斥了
每次創建是創建一個信號量集,只要互斥的話只用一個信號量,要同步的話要多個信號量
比如:有三個進程A,B,C。想先讓A,再B,最後C。有三個信號量numA,numB,numC,分別告訴ABC它們能不能訪問資源了。先把numA置爲1,其他倆個都是0。A讀完,把numB置爲1,B讀完把numC置爲1,C讀完再把numA置爲1。
使用步驟
- 進程調用semget函數創建新的信號量集合,或者獲取已有的信號量集合。
- 調用semctl函數給集合中的每個信號量設置初始值
- 調用semop函數,對集合中的信號量進行pv操作(加鎖解鎖):P操作(加鎖):對信號量的值進行-1,如果信號量的值爲0,p操作就會阻塞;V操作(解鎖):對信號量的值進行+1,V操作不存在阻塞的問題
- 調用semctl刪除信號量集合
如果想簡單控制哪個進程先加鎖,或者解鎖後想讓其他進程獲得鎖而不是還是自己,可以使用sleep
工作原理
信號量其實是OS在內核創建的一個共享變量,進程在進行操作之前,會先檢查這個變量的值,這變量的值就是一個標記,通過這個標記就可以知道可不可以操作,以實現互斥。
信號量只能進行兩種操作:等待和發送信號,即P(sv)和V(sv)
P(sv)加鎖:如果sv的值大於零,就給它減1;如果它的值爲零,就掛起該進程的執行
V(sv)解鎖:如果有其他進程因等待sv而被掛起,就讓它恢復運行,如果沒有進程因等待sv而掛起,就給它加1.
舉個例子,就是兩個進程共享信號量sv,一旦其中一個進程執行了P(sv)操作,它將得到信號量,並可以進入臨界區,使sv減1。而第二個進程將被阻止進入臨界區,因爲當它試圖執行P(sv)時,sv爲0,它會被掛起以等待第一個進程離開臨界區域並執行V(sv)釋放信號量,這時第二個進程就可以恢復執行。
創建——semget()
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
/*
功能:
根據key值創建新的、或者獲取已有的信號量集合,並返回其標識符。
參數:
key:整數值(唯一非零),不相關的進程可以通過它訪問一個信號量,
num_sems:指定需要的信號量數目,它的值幾乎總是1。
sem_flags:設置訪問權限,一般都設置爲(可讀可寫)0664 | IPC_CREAT。
設置了IPC_CREAT標誌後,即使給出的鍵是一個已有信號量的鍵,也不會產生錯誤。
IPC_CREAT | IPC_EXCL則可以創建一個新的,唯一的信號量,如果信號量已存在,
返回一個錯誤。
返回值:
函數成功返回一個相應信號標識符(非零),失敗返回-1.
*/
操作——semop()
它的作用是改變信號量的值,一般定義兩個struct sembuf,一個叫p,一個叫v
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
此結構體已經定義過了
struct sembuf{
short sem_num;//除非使用一組信號量,否則它爲0,即編號
short sem_op;//信號量在一次操作中需要改變的數據,通常是兩個數,一個是-1,即P(等待)操作,
//一個是+1,即V(發送信號)操作。
short sem_flg;//通常爲SEM_UNDO,防止死鎖,還是以二值信號量爲例,當進程在v操作之前就結束時,
信號量的值就會一直保持爲0,那麼其它進程將永遠無法p操作成功
,會使得進程永遠休眠下去,這造成就是死鎖。但是設置了SEM_UNDO
後,如果進程在結束時沒有V操作的話,OS會自動幫忙V操作,
防止死鎖。
IPC_NOWAIT:一般情況下,當信號量的值爲0時進行p操作的話,semop的p操作會阻塞。
如果你不想阻塞的話,可以指定這個選項,NOWAIT就是不阻塞的意思。
不過除非某些特殊情況,否則我們不需要設置爲非阻塞。
};
int semop(int semid, struct sembuf *sops, unsigned nsops);
/*
參數:
sem_id:由semget返回的信號量標識符。
nsops:信號操作結構的數量,恆大於或等於1
*/
控制——semctl()
該函數用來直接控制信號量信息(初始化?)
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
//如果有第四個參數,它通常是一個union semum結構,定義如下:
//這個聯合體要自己定義,一般還是用semun這個名字,一般不定義,在第四個參數直接寫int也可以
union semun{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
int semctl(int semid, int semnum, int cmd, ...);
/*
參數:
第一個參數是信號量集IPC標識符。
第二個參數是操作信號在信號集中的編號,第一個信號的編號是0
command:通常是下面兩個值中的其中一個
SETVAL:用來把信號量初始化爲一個已知的值。p 這個值通過
union semun中的val成員設置,其作用是在信號量
第一次使用前對它進行設置。
IPC_RMID:用於刪除一個已經無需繼續使用的信號量標識符。
*/
例子
先運行文件1,再運行文件2,文件2不要再初始化信號量了(沒有函數semctl)
文件1
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/sem.h>
int main()
{
int i = 0;
char string[100];
int semid;
int ret;
struct sembuf p,v;
semid = semget((key_t)0x123,1,IPC_CREAT);
if(semid < 0)
{
perror("semget");
return -1;
}
ret = semctl(semid,0,SETVAL,1);
if(ret < 0)
{
perror("semctl");
return -1;
}
p.sem_num = 0;
p.sem_op = -1;
p.sem_flg = SEM_UNDO;
v.sem_num = 0;
v.sem_op = 1;
v.sem_flg = SEM_UNDO;
while(1)
{
semop(semid,&p,1);
//sprintf(string,"echo sem1 = %d >> /1",i++);
//system(string);
system("echo sem1 >> /1");
sleep(5);
semop(semid,&v,1);
}
return 0;
}
文件2
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/sem.h>
int main()
{
int i = 0;
char string[100];
int semid;
int ret;
struct sembuf p,v;
semid = semget((key_t)0x123,1,IPC_CREAT);
if(semid < 0)
{
perror("semget");
return -1;
}
p.sem_num = 0;
p.sem_op = -1;
p.sem_flg = SEM_UNDO;
v.sem_num = 0;
v.sem_op = 1;
v.sem_flg = SEM_UNDO;
while(1)
{
semop(semid,&p,1);
//sprintf(string,"echo sem2 = %d >> /1",i++);
//system(string);
system("echo sem2 >> /1");
semop(semid,&v,1);
}
return 0;
}
信號
不做數據傳輸,只做控制。信號是一種向進程發送通知,告訴其某件事情發生了的一種簡單通信機制
與信號量完全無關!
信號的產生
- 底層硬件發送信號
- 內核發送信號
- 另一個進程發送信號
linux下使用kill -l查看所有信號(前面的序號即代表他們的數字)
常用信號:
信號宏名 信號編號 說明 系統默認處理方式
SIGABRT 6 終止進程,調abort函數是產生 終止,產生core文件
SIGALRM 14 超時,調用alarm函數時產生 終止
SIGBUS 7 硬件故障 終止,產生core文件
SIGCHLD 17 子進程狀態改變 忽略
SIGINT 2 終止進程(ctrl+c) 終止
SIGIO 29 異步通知信號 終止
SIGKILL 9 無條件終止一個進程,不可以被捕獲或忽略 終止
SIGPIPE 13 寫沒有讀權限的管道文件時 終止
SIGPOLL 8 輪詢事件,涉及POLL機制 終止
SIGQUIT 3 終止進程(ctrl+\) crtl+z? 終止,產生core文件
SIGSEGV 11 無效存儲訪問(指針錯誤) 終止,產生core文件
SIGTERM 15 終止,kill PID時,默認發送的就是這個信號 終止
SIGUSR1 10 用戶自定義信號1 終止
SIGUSR2 12 用戶自定義信號2 終止
kill()
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
/*
功能:
向進程id爲pid的進程發送sig信號
參數:
pid > 0:將信號傳給進程識別碼爲pid的進程
pid = 0:將信號傳給目前進程相同進程的所有進程
pid = -1:將信號像廣播般送給系統內所有的進程
pid < 0:將信號傳給進程組識別碼爲pid絕對值的所有進程
返回值:
執行成功返回0,失敗返回-1
*/
raise()
#include <signal.h>
int raise(int sig);
/*
功能:
給自己的進程發送sig信號
返回值:
*/
alarm
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
/*
功能:
隔seconds秒給自己發送alarm信號
返回值:
*/
pause():執行這個函數後進程掛起,處於等待態,不消耗CPU資源(像不消耗系統資源的while(1)語句),但一旦信號被用戶處理了,它就不休眠了,繼續往下執行接下來的程序。
abort()
#include <stdlib.h>
void abort();
/*
功能:
終止程序
返回值:
*/
信號處理的方式
- 忽略(比如接收到crtl+c如同沒接收到一樣)
- 執行用戶需要執行的動作(比如接收到crtl+c打印一個hello world)
- 默認處理(比如接收到crtl+c就結束程序)
signal()
在使用共享內存時,經常用while語句,到最後共享內存都釋放不了。可以用signal捕獲crtl+c信號然後把共享內存釋放了。
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
/*
功能:
設置某個信號的處理方式,處理方式可以被設置爲忽略,捕獲,默認。
參數:
signum:信號編號。
handler:信號處理方式。
(a)忽略:SIG_IGN
(b)默認:SIG_DFL
(c)捕獲:填寫類型爲void (*)(int)的捕獲函數的地址,當信號發生時,
會自動調用捕獲函數來進行相應的處理。捕獲函數的int參數,用於接收信號編號。
這就表明用一個函數可以處理所有信號的相應事件。
返回值:
*/
例.
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
void handle(int sig)
{
if(sig == SIGALRM)
{
printf("hello world!\n");
}
}
int main()
{
signal(SIGALRM, handle); //alarm信號觸發時,執行handle函數
signal(SIGINT, SIG_IGN); //crtl+c信號觸發時,忽略
alarm(10);
pause();
printf("exit...\n");
}
signal()
signal()函數超詳細介紹(包含一些常用signal的中文)
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
/*
函數功能:
設置某一信號的對應動作。當一個信號的信號處理函數執行時,如果進程
又接收到了該信號,該信號會自動被儲存而不會中斷信號處理函數的執行,直到信
號處理函數執行完畢再重新調用相應的處理函數。但是如果在信號處理函數執行時
進程收到了其它類型的信號,該函數的執行就會被中斷。
參數:
signum:指明瞭所要處理的信號類型,它可以取除了SIGKILL和SIGSTOP外的任何一種信號。
handler:描述了與信號關聯的動作,它可以取以下三種值:
1)SIG_IGN:忽略該信號
2)SIG_DFL:恢復對信號的系統默認處理
3)sighandler_t類型的函數指針
返回:
返回先前的信號處理函數指針,如果有錯誤則返回SIG_ERR(-1)。
*/
第二個參數的類型三的函數詳解:
此函數必須在signal()被調用前申明,handler中爲這個函數的名字。當接收到一個類型爲sig的信號時,就執行handler 所指定的函數。(int)signum是傳遞給它的唯一參數。執行了signal()調用後,進程只要接收到類型爲sig的信號,不管其正在執行程序的哪一部分,就立即執行func()函數。當func()函數執行結束後,控制權返回進程被中斷的那一點繼續執行。
man 7 signal 查看信號列表:
SIGINT信號代表由InterruptKey產生,通常是CTRL +C 或者是DELETE 。
#include <stdio.h>
#include <signal.h>
void func(int argc)
{
printf("argc = %d\n",argc);
}
void func1(int argc)
{
printf("alrm ############ = %d\n",argc);
}
int main()
{
signal(SIGTERM,func); //SIGTERM是終止指令,用“kill -s SIGTERM 進程ID”實現
//signal(SIGALRM,func1); //SIGALRM是alarm函數會觸發的信號,此函數調用1s後就會執行func1函數
//alarm(1);
while(1);
return 0;
}
alarm()
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
/*
函數功能:
alarm()函數的主要功能是設置信號傳送鬧鐘,即用來設置信號SIGALRM
在經過參數seconds秒數後發送給目前的進程。如果未設置信號SIGALARM的處理
函數,那麼alarm()默認處理終止進程。如果在seconds秒內再次調用了alarm函
數設置了新的鬧鐘,則後面定時器的設置將覆蓋前面的設置,即之前設置的秒數被
新的鬧鐘時間取代;當參數seconds爲0時,之前設置的定時器鬧鐘將被取消,並
將剩下的時間返回。
*/
sigaction()
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
/*
參數:
signum:要操作的信號。
act:要設置的對信號的新處理方式,指向sigaction結構的指針。
oldact:原來對信號的處理方式。
返回值:
0 表示成功,-1 表示有錯誤發生。
*/
struct sigaction
{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
結構體詳解:
- sa_handler:函數指針,其含義與 signal 函數中的信號處理函數類似。參數即爲信號值,所以信號不能傳遞除信號值之外的任何信息。
- sa_sigaction:另一個信號處理函數,它有三個參數,可以獲得關於信號的更詳細的信息。第一個參數爲信號值;第二個參數是一個指向struct siginfo結構的指針,此結構中包含信號攜帶的數據值;第三個參數沒有使用。siginfo_t結構體
當 sa_flags 成員的值包含了 SA_SIGINFO 標誌時,系統將使用 sa_sigaction 函數作爲信號處理函數,否則使用 sa_handler 作爲信號處理
函數。(在某些系統中,成員 sa_handler 與 sa_sigaction 被放在聯合體中,因此使用時不要同時設置。)
-
sa_mask:指定在信號處理程序執行過程中,哪些信號應當被阻塞。默認當前信號本身被阻塞。
- sa_flags:用於指定信號處理的行爲,它可以是一下值的“按位或”組合。
◆ SA_RESTART:使被信號打斷的系統調用自動重新發起。
◆ SA_NOCLDSTOP:使父進程在它的子進程暫停或繼續運行時不會收到 SIGCHLD 信號。
◆ SA_NOCLDWAIT:使父進程在它的子進程退出時不會收到 SIGCHLD 信號,這時子進程如果退出也不會成爲殭屍進程。
◆ SA_NODEFER:使對信號的屏蔽無效,即在信號處理函數執行期間仍能發出這個信號。
◆ SA_RESETHAND:信號處理之後重新設置爲默認的處理方式。
- re_restorer :是一個已經廢棄的數據域,不要使用。
例.
#include <stdio.h>
#include <signal.h>
void func1(int argc)
{
printf("alrm ############ = %d\n",argc);
}
void func2(int argc,siginfo_t *info,void *v)
{
printf("sig = %d\n",info->si_int);
}
int main()
{
struct sigaction act;
act.sa_flags = SA_SIGINFO;
act.sa_sigaction = func2;
signal(SIGALRM,func1); //SIGALRM是alarm函數會觸發的信號,此函數調用1s後就會執行func1函數
sigaction(SIGTERM,&act,NULL);
alarm(5);
while(1);
return 0;
}