关于操作系统中进程、线程、死锁、同步、进程间通信(IPC)的超详细详解整理
???????作者主頁:https://www.zhihu.com/people/san-hao-bai-du-ren-79
一、什么是進程?什么是線程?
1.1 進程定義
1.2 線程定義
1.3 進程與線程的區別
1.4 進程狀態轉換圖
1.5? 線程比進程具有哪些優勢?
1.6 什么時候用多進程?什么時候用多線程?
1.7 協程是什么?
二、進程間的通信方式
2.1 管道通信
2.1.1 特點
2.1.2 函數原型
2.1.3 實例
2.1.4 代碼實現
2.2 FIFO(有名管道)
2.2.1 特點
2.2.2 函數原型
2.2.3 實例
2.3 消息隊列??
2.3.1 特點
2.3.2 函數原型
2.3.3 實例
2.4 信號量
2.4.1 特點
2.4.2 函數原型
2.4.3 實例
2.5 共享內存
2.5.1 特點
2.5.2 函數原型
2.5.3 實例
2.6 套接字通信
2.6.1 命名socket
2.6.2 綁定
2.6.3 監聽
2.6.4 連接服務器
2.6.5 客戶端與服務端通信
2.6.6 斷開連接
三、死鎖是什么?必要條件?如何解決?
3.1 死鎖定義
3.2 死鎖發生的必要條件
3.3 解決死鎖問題
四、線程的同步方式
作者主頁:https://www.zhihu.com/people/san-hao-bai-du-ren-79
期待關注。
一、什么是進程?什么是線程?
1.1 進程定義
進程是指在系統中正在運行的一個應用程序,程序一旦運行就是進程。
進程是操作系統資源分配的最小單位,且每個進程擁有獨立的地址空間;
進程與進程之間資源分隔,不能共享,一個進程無法直接訪問另一個進程的變量和數據結構,如果希望一個進程去訪問另一個進程的資源,需要使用進程間的通信;
(1)進程是程序的一次執行,該程序可以與其他程序并發執行;
(2)進程有運行、阻塞、就緒三個基本狀態;
(3)進程調度算法:先來先服務調度算法、短作業優先調度算法、非搶占式優先級調度算法、搶占式優先級調度算法、高響應比優先調度算法、時間片輪轉法調度算法;
1.2 線程定義
線程是進程的一個實體,是進程的一條執行路徑;比進程更小的獨立運行的基本單位,線程也被稱為輕量級進程,一個程序至少有一個進程,一個進程至少有一個線程;
線程是操作系統資源調配的最小單位,是程序執行的最小單位。
1.3 進程與線程的區別
(1)一個程序至少有一個進程,一個進程至少有一個線程;
本文作者:https://www.zhihu.com/people/san-hao-bai-du-ren-79
(由于文章總是被三無號到處復制發布,選擇這種方式插入原文鏈接影響閱讀實在抱歉!)
本文原文鏈接:https://blog.csdn.net/qq_41687938/article/details/118179581
(2)同一進程的線程共享本進程的地址空間,而進程之間則是獨立的地址空間。同一進程內的線程共享本進程的資源,但是進程之間的資源是獨立的。進程在執行過程中擁有獨立的內存單元,而多個線程共享內存,從而極大地提高了程序的運行效率;
(3)一個進程崩潰后,在保護模式下不會對其他進程產生影響,但是一個線程崩潰整個進程崩潰,所以多進程比多線程健壯;
(4)進程切換,消耗的資源大。所以涉及到頻繁的切換,使用線程要好于進程;
(5)兩者均可并發執行,但線程的劃分尺度小于進程,使得多線程程序的并發性高;
(6)每個獨立的進程有一個程序的入口、程序出口。但是線程不能獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。
(7)多線程的意義在于一個應用程序中,有多個執行部分可以同時執行。但操作系統并沒有將多個線程看做多個獨立的應用,來實現進程的調度和管理以及資源分配;
(8)一個進程中的所有線程共享該進程的地址空間,但它們有各自獨立的(/私有的)棧(stack),Windows線程的缺省堆棧大小為1M。堆(heap)的分配與棧有所不同,一般是一個進程有一個C運行時堆,這個堆為本進程中所有線程共享,windows進程還有所謂進程默認堆,用戶也可以創建自己的堆。?
線程私有:線程棧,寄存器,程序寄存器
共享:堆,地址空間,全局變量,靜態變量
進程私有:地址空間,堆,全局變量,棧,寄存器
共享:代碼段,公共數據,進程目錄,進程ID
1.4 進程狀態轉換圖
(1)新狀態:進程已經創建
(2)就緒態:進程做好了準備,準備執行,等待分配處理機
(3)執行態:該進程正在執行;
(4)阻塞態:等待某事件發生才能執行,如等待I/O完成;
(5)終止狀態
1.5? 線程比進程具有哪些優勢?
(1)線程在程序中是獨立的,并發的執行流,但是,進程中的線程之間的隔離程度要小;
(2)線程比進程更具有更高的性能,這是由于同一個進程中的線程都有共性:多個線程將共享同一個進程虛擬空間;
(3)當操作系統創建一個進程時,必須為進程分配獨立的內存空間,并分配大量相關資源;
1.6 什么時候用多進程?什么時候用多線程?
(1)需要頻繁創建銷毀的優先用線程;
(2)需要進行大量計算的優先使用線程;
(3)強相關的處理用線程,弱相關的處理用進程;
(4)可能要擴展到多機分布的用進程,多核分布的用線程;
1.7 協程是什么?
(1)是一種比線程更加輕量級的存在。正如一個進程可以擁有多個線程一樣,一個線程可以擁有多個協程;協程不是被操作系統內核管理,而完全是由程序所控制。
(2)協程的開銷遠遠小于線程;
(3)協程擁有自己寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切換回來的時候,恢復先前保存的寄存器上下文和棧。
(4)每個協程表示一個執行單元,有自己的本地數據,與其他協程共享全局數據和其他資源。
(5)跨平臺、跨體系架構、無需線程上下文切換的開銷、方便切換控制流,簡化編程模型;
(6)協程又稱為微線程,協程的完成主要靠yeild關鍵字,協程執行過程中,在子程序內部可中斷,然后轉而執行別的子程序,在適當的時候再返回來接著執行;
(7)協程極高的執行效率,和多線程相比,線程數量越多,協程的性能優勢就越明顯;
(8)不需要多線程的鎖機制;
二、進程間的通信方式
IPC這部分的參考鏈接:https://www.cnblogs.com/CheeseZH/p/5264465.html
進程間通信(IPC,InterProcess Communication)是指在不同進程之間傳播或交換信息。
IPC的方式通常有管道(包括無名管道和命名管道)、消息隊列、信號量、共享存儲、Socket、Streams等。其中 Socket和Streams支持不同主機上的兩個進程IPC。
以Linux中的C語言編程為例。
2.1 管道通信
管道,通常指無名管道,是 UNIX 系統IPC最古老的形式。
2.1.1 特點
(1)它是半雙工的(即數據只能在一個方向上流動),具有固定的讀端和寫端。
(2)它只能用于具有親緣關系的進程之間的通信(也是父子進程或者兄弟進程之間)。
(3)它可以看成是一種特殊的文件,對于它的讀寫也可以使用普通的read、write 等函數。但是它不是普通的文件,并不屬于其他任何文件系統,并且只存在于內存中。
(4)Int pipe(int fd[2]);當一個管道建立時,會創建兩個文件描述符,要關閉管道只需將這兩個文件描述符關閉即可。
2.1.2 函數原型
#include <unistd.h> int pipe(int fd[2]); // 返回值:若成功返回0,失敗返回-1當一個管道建立時,它會創建兩個文件描述符:fd[0]為讀而打開,fd[1]為寫而打開。如下圖:
要關閉管道只需將這兩個文件描述符關閉即可。
2.1.3 實例
單個進程中的管道幾乎沒有任何用處。所以,通常調用 pipe 的進程接著調用 fork,這樣就創建了父進程與子進程之間的 IPC 通道。如下圖所示:
若要數據流從父進程流向子進程,則關閉父進程的讀端(fd[0])與子進程的寫端(fd[1]);反之,則可以使數據流從子進程流向父進程。
2.1.4 代碼實現
#include<stdio.h> #include<unistd.h>int main() {int fd[2]; // 兩個文件描述符pid_t pid;char buff[20];if(pipe(fd) < 0) // 創建管道printf("Create Pipe Error!\n");if((pid = fork()) < 0) // 創建子進程printf("Fork Error!\n");else if(pid > 0) // 父進程{close(fd[0]); // 關閉讀端write(fd[1], "hello world\n", 12);}else{close(fd[1]); // 關閉寫端read(fd[0], buff, 20);printf("%s", buff);}return 0; }2.2 FIFO(有名管道)
FIFO,也稱為命名管道,它是一種文件類型。
2.2.1 特點
(1)FIFO可以在無關的進程之間交換數據,與無名管道不同。
(2)FIFO有路徑名與之相關聯,它以一種特殊設備文件形式存在于文件系統中。
(3)Int mkfifo(const char* pathname,mode_t mode);
本文作者:https://www.zhihu.com/people/san-hao-bai-du-ren-79
(由于文章總是被三無號到處復制發布,選擇這種方式插入原文鏈接影響閱讀實在抱歉!)
本文原文鏈接:https://blog.csdn.net/qq_41687938/article/details/118179581
2.2.2 函數原型
#include <sys/stat.h> // 返回值:成功返回0,出錯返回-1 int mkfifo(const char *pathname, mode_t mode);其中的 mode 參數與open函數中的 mode 相同。一旦創建了一個 FIFO,就可以用一般的文件I/O函數操作它。
當 open 一個FIFO時,是否設置非阻塞標志(O_NONBLOCK)的區別:
? ? ? (1)若沒有指定O_NONBLOCK(默認),只讀 open 要阻塞到某個其他進程為寫而打開此 FIFO。類似的,只寫 open 要阻塞到某個其他進程為讀而打開它。
? ? ? (2)若指定了O_NONBLOCK,則只讀 open 立即返回。而只寫 open 將出錯返回 -1 如果沒有進程已經為讀而打開該 FIFO,其errno置ENXIO。
2.2.3 實例
FIFO的通信方式類似于在進程中使用文件來傳輸數據,只不過FIFO類型文件同時具有管道的特性。在數據讀出時,FIFO管道中同時清除數據,并且“先進先出”。下面的例子演示了使用 FIFO 進行 IPC 的過程:
write_fifo.c
#include<stdio.h> #include<stdlib.h> // exit #include<fcntl.h> // O_WRONLY #include<sys/stat.h> #include<time.h> // timeint main() {int fd;int n, i;char buf[1024];time_t tp;printf("I am %d process.\n", getpid()); // 說明進程IDif((fd = open("fifo1", O_WRONLY)) < 0) // 以寫打開一個FIFO{perror("Open FIFO Failed");exit(1);}for(i=0; i<10; ++i){time(&tp); // 取系統當前時間n=sprintf(buf,"Process %d's time is %s",getpid(),ctime(&tp));printf("Send message: %s", buf); // 打印if(write(fd, buf, n+1) < 0) // 寫入到FIFO中{perror("Write FIFO Failed");close(fd);exit(1);}sleep(1); // 休眠1秒}close(fd); // 關閉FIFO文件return 0; }read_fifo.c
#include<stdio.h> #include<stdlib.h> #include<errno.h> #include<fcntl.h> #include<sys/stat.h>int main() {int fd;int len;char buf[1024];if(mkfifo("fifo1", 0666) < 0 && errno!=EEXIST) // 創建FIFO管道perror("Create FIFO Failed");if((fd = open("fifo1", O_RDONLY)) < 0) // 以讀打開FIFO{perror("Open FIFO Failed");exit(1);}while((len = read(fd, buf, 1024)) > 0) // 讀取FIFO管道printf("Read message: %s", buf);close(fd); // 關閉FIFO文件return 0; }在兩個終端里用 gcc 分別編譯運行上面兩個文件,可以看到輸出結果如下:
[cheesezh@localhost]$ ./write_fifo I am 5954 process. Send message: Process 5954's time is Mon Apr 20 12:37:28 2015 Send message: Process 5954's time is Mon Apr 20 12:37:29 2015 Send message: Process 5954's time is Mon Apr 20 12:37:30 2015 Send message: Process 5954's time is Mon Apr 20 12:37:31 2015 Send message: Process 5954's time is Mon Apr 20 12:37:32 2015 Send message: Process 5954's time is Mon Apr 20 12:37:33 2015 Send message: Process 5954's time is Mon Apr 20 12:37:34 2015 Send message: Process 5954's time is Mon Apr 20 12:37:35 2015 Send message: Process 5954's time is Mon Apr 20 12:37:36 2015 Send message: Process 5954's time is Mon Apr 20 12:37:37 2015 [cheesezh@localhost]$ ./read_fifo Read message: Process 5954's time is Mon Apr 20 12:37:28 2015 Read message: Process 5954's time is Mon Apr 20 12:37:29 2015 Read message: Process 5954's time is Mon Apr 20 12:37:30 2015 Read message: Process 5954's time is Mon Apr 20 12:37:31 2015 Read message: Process 5954's time is Mon Apr 20 12:37:32 2015 Read message: Process 5954's time is Mon Apr 20 12:37:33 2015 Read message: Process 5954's time is Mon Apr 20 12:37:34 2015 Read message: Process 5954's time is Mon Apr 20 12:37:35 2015 Read message: Process 5954's time is Mon Apr 20 12:37:36 2015 Read message: Process 5954's time is Mon Apr 20 12:37:37 2015上述例子可以擴展成 客戶進程—服務器進程 通信的實例,write_fifo的作用類似于客戶端,可以打開多個客戶端向一個服務器發送請求信息,read_fifo類似于服務器,它適時監控著FIFO的讀端,當有數據時,讀出并進行處理,但是有一個關鍵的問題是,每一個客戶端必須預先知道服務器提供的FIFO接口,下圖顯示了這種安排:
2.3 消息隊列??
消息隊列,是消息的鏈接表,存放在內核中。一個消息隊列由一個標識符(即隊列ID)來標識。
2.3.1 特點
(1)消息隊列是面向記錄的,其中的消息具有特定的格式以及特定的優先級。
(2)消息隊列獨立于發送與接收進程。進程終止時,消息隊列及其內容并不會被刪除。
(3)消息隊列可以實現消息的隨機查詢,消息不一定要以先進先出的次序讀取,也可以按消息的類型讀取。
2.3.2 函數原型
#include <sys/msg.h> // 創建或打開消息隊列:成功返回隊列ID,失敗返回-1 int msgget(key_t key, int flag); // 添加消息:成功返回0,失敗返回-1 int msgsnd(int msqid, const void *ptr, size_t size, int flag); // 讀取消息:成功返回消息數據的長度,失敗返回-1 int msgrcv(int msqid, void *ptr, size_t size, long type,int flag); // 控制消息隊列:成功返回0,失敗返回-1 int msgctl(int msqid, int cmd, struct msqid_ds *buf);在以下兩種情況下,msgget將創建一個新的消息隊列:
? ? ? ?(1)如果沒有與鍵值key相對應的消息隊列,并且flag中包含了IPC_CREAT標志位。
? ? ? ?(2)key參數為IPC_PRIVATE。
函數msgrcv在讀取消息隊列時,type參數有下面幾種情況:
? ? ? (1)type == 0,返回隊列中的第一個消息;
? ? ? (2)type > 0,返回隊列中消息類型為 type 的第一個消息;
? ? ? (3)type < 0,返回隊列中消息類型值小于或等于 type 絕對值的消息,如果有多個,則取類型值最小的消息。
可以看出,type值非 0 時用于以非先進先出次序讀消息。也可以把 type 看做優先級的權值。(其他的參數解釋,請自行Google之)
2.3.3 實例
下面寫了一個簡單的使用消息隊列進行IPC的例子,服務端程序一直在等待特定類型的消息,當收到該類型的消息以后,發送另一種特定類型的消息作為反饋,客戶端讀取該反饋并打印出來。
msg_server.c
msg_client.c
#include <stdio.h> #include <stdlib.h> #include <sys/msg.h>// 用于創建一個唯一的key #define MSG_FILE "/etc/passwd"// 消息結構 struct msg_form {long mtype;char mtext[256]; };int main() {int msqid;key_t key;struct msg_form msg;// 獲取key值if ((key = ftok(MSG_FILE, 'z')) < 0){perror("ftok error");exit(1);}// 打印key值printf("Message Queue - Client key is: %d.\n", key);// 打開消息隊列if ((msqid = msgget(key, IPC_CREAT|0777)) == -1){perror("msgget error");exit(1);}// 打印消息隊列ID及進程IDprintf("My msqid is: %d.\n", msqid);printf("My pid is: %d.\n", getpid());// 添加消息,類型為888msg.mtype = 888;sprintf(msg.mtext, "hello, I'm client %d", getpid());msgsnd(msqid, &msg, sizeof(msg.mtext), 0);// 讀取類型為777的消息msgrcv(msqid, &msg, 256, 999, 0);printf("Client: receive msg.mtext is: %s.\n", msg.mtext);printf("Client: receive msg.mtype is: %d.\n", msg.mtype);return 0; }2.4 信號量
信號量(semaphore)與已經介紹過的 IPC 結構不同,它是一個計數器。信號量用于實現進程間的互斥與同步,而不是用于存儲進程間通信數據。
2.4.1 特點
(1)信號量用于進程間同步,若要在進程間傳遞數據需要結合共享內存。
(2)信號量基于操作系統的 PV 操作,程序對信號量的操作都是原子操作。
(3)每次對信號量的 PV 操作不僅限于對信號量值加 1 或減 1,而且可以加減任意正整數。
(4)支持信號量組。
2.4.2 函數原型
最簡單的信號量是只能取 0 和 1 的變量,這也是信號量最常見的一種形式,叫做二值信號量(Binary Semaphore)。而可以取多個正整數的信號量被稱為通用信號量。
Linux 下的信號量函數都是在通用的信號量數組上進行操作,而不是在一個單一的二值信號量上進行操作。
當semget創建新的信號量集合時,必須指定集合中信號量的個數(即num_sems),通常為1; 如果是引用一個現有的集合,則將num_sems指定為 0 。
在semop函數中,sembuf結構的定義如下:
其中 sem_op 是一次操作中的信號量的改變量:
-
若sem_op > 0,表示進程釋放相應的資源數,將 sem_op 的值加到信號量的值上。如果有進程正在休眠等待此信號量,則換行它們。
-
若sem_op < 0,請求 sem_op 的絕對值的資源。
- 如果相應的資源數可以滿足請求,則將該信號量的值減去sem_op的絕對值,函數成功返回。
- 當相應的資源數不能滿足請求時,這個操作與sem_flg有關。
- sem_flg 指定IPC_NOWAIT,則semop函數出錯返回EAGAIN。
- sem_flg 沒有指定IPC_NOWAIT,則將該信號量的semncnt值加1,然后進程掛起直到下述情況發生:
- 當相應的資源數可以滿足請求,此信號量的semncnt值減1,該信號量的值減去sem_op的絕對值。成功返回;
- 此信號量被刪除,函數smeop出錯返回EIDRM;
- 進程捕捉到信號,并從信號處理函數返回,此情況下將此信號量的semncnt值減1,函數semop出錯返回EINTR
若sem_op == 0,進程阻塞直到信號量的相應值為0:
- 當信號量已經為0,函數立即返回。
- 如果信號量的值不為0,則依據sem_flg決定函數動作:
- sem_flg指定IPC_NOWAIT,則出錯返回EAGAIN。
- sem_flg沒有指定IPC_NOWAIT,則將該信號量的semncnt值加1,然后進程掛起直到下述情況發生:
- 信號量值為0,將信號量的semzcnt的值減1,函數semop成功返回;
- 此信號量被刪除,函數smeop出錯返回EIDRM;
- 進程捕捉到信號,并從信號處理函數返回,在此情況將此信號量的semncnt值減1,函數semop出錯返回EINTR
在semctl函數中的命令有多種,這里就說兩個常用的:
- SETVAL:用于初始化信號量為一個已知的值。所需要的值作為聯合semun的val成員來傳遞。在信號量第一次使用之前需要設置信號量。
- IPC_RMID:刪除一個信號量集合。如果不刪除信號量,它將繼續在系統中存在,即使程序已經退出,它可能在你下次運行此程序時引發問題,而且信號量是一種有限的資源。
2.4.3 實例
#include<stdio.h> #include<stdlib.h> #include<sys/sem.h>// 聯合體,用于semctl初始化 union semun {int val; /*for SETVAL*/struct semid_ds *buf;unsigned short *array; };// 初始化信號量 int init_sem(int sem_id, int value) {union semun tmp;tmp.val = value;if(semctl(sem_id, 0, SETVAL, tmp) == -1){perror("Init Semaphore Error");return -1;}return 0; }// P操作: // 若信號量值為1,獲取資源并將信號量值-1 // 若信號量值為0,進程掛起等待 int sem_p(int sem_id) {struct sembuf sbuf;sbuf.sem_num = 0; /*序號*/sbuf.sem_op = -1; /*P操作*/sbuf.sem_flg = SEM_UNDO;if(semop(sem_id, &sbuf, 1) == -1){perror("P operation Error");return -1;}return 0; }// V操作: // 釋放資源并將信號量值+1 // 如果有進程正在掛起等待,則喚醒它們 int sem_v(int sem_id) {struct sembuf sbuf;sbuf.sem_num = 0; /*序號*/sbuf.sem_op = 1; /*V操作*/sbuf.sem_flg = SEM_UNDO;if(semop(sem_id, &sbuf, 1) == -1){perror("V operation Error");return -1;}return 0; }// 刪除信號量集 int del_sem(int sem_id) {union semun tmp;if(semctl(sem_id, 0, IPC_RMID, tmp) == -1){perror("Delete Semaphore Error");return -1;}return 0; }int main() {int sem_id; // 信號量集IDkey_t key;pid_t pid;// 獲取key值if((key = ftok(".", 'z')) < 0){perror("ftok error");exit(1);}// 創建信號量集,其中只有一個信號量if((sem_id = semget(key, 1, IPC_CREAT|0666)) == -1){perror("semget error");exit(1);}// 初始化:初值設為0資源被占用init_sem(sem_id, 0);if((pid = fork()) == -1)perror("Fork Error");else if(pid == 0) /*子進程*/{sleep(2);printf("Process child: pid=%d\n", getpid());sem_v(sem_id); /*釋放資源*/}else /*父進程*/{sem_p(sem_id); /*等待資源*/printf("Process father: pid=%d\n", getpid());sem_v(sem_id); /*釋放資源*/del_sem(sem_id); /*刪除信號量集*/}return 0; }上面的例子如果不加信號量,則父進程會先執行完畢。這里加了信號量讓父進程等待子進程執行完以后再執行。
2.5 共享內存
共享內存(Shared Memory),指兩個或多個進程共享一個給定的存儲區。
2.5.1 特點
(1)共享內存是最快的一種 IPC,因為進程是直接對內存進行存取。
(2)因為多個進程可以同時操作,所以需要進行同步。
(3)信號量+共享內存通常結合在一起使用,信號量用來同步對共享內存的訪問。
2.5.2 函數原型
#include <sys/shm.h> // 創建或獲取一個共享內存:成功返回共享內存ID,失敗返回-1 int shmget(key_t key, size_t size, int flag); // 連接共享內存到當前進程的地址空間:成功返回指向共享內存的指針,失敗返回-1 void *shmat(int shm_id, const void *addr, int flag); // 斷開與共享內存的連接:成功返回0,失敗返回-1 int shmdt(void *addr); // 控制共享內存的相關信息:成功返回0,失敗返回-1 int shmctl(int shm_id, int cmd, struct shmid_ds *buf);當用shmget函數創建一段共享內存時,必須指定其 size;而如果引用一個已存在的共享內存,則將 size 指定為0 。
當一段共享內存被創建以后,它并不能被任何進程訪問。必須使用shmat函數連接該共享內存到當前進程的地址空間,連接成功后把共享內存區對象映射到調用進程的地址空間,隨后可像本地空間一樣訪問。
shmdt函數是用來斷開shmat建立的連接的。注意,這并不是從系統中刪除該共享內存,只是當前進程不能再訪問該共享內存而已。
shmctl函數可以對共享內存執行多種操作,根據參數 cmd 執行相應的操作。常用的是IPC_RMID(從系統中刪除該共享內存)。
2.5.3 實例
下面這個例子,使用了【共享內存+信號量+消息隊列】的組合來實現服務器進程與客戶進程間的通信。
(1)共享內存用來傳遞數據;
(2)信號量用來同步;
(3)消息隊列用來 在客戶端修改了共享內存后 通知服務器讀取。
server.c
client.c
#include<stdio.h> #include<stdlib.h> #include<sys/shm.h> // shared memory #include<sys/sem.h> // semaphore #include<sys/msg.h> // message queue #include<string.h> // memcpy// 消息隊列結構 struct msg_form {long mtype;char mtext; };// 聯合體,用于semctl初始化 union semun {int val; /*for SETVAL*/struct semid_ds *buf;unsigned short *array; };// P操作: // 若信號量值為1,獲取資源并將信號量值-1 // 若信號量值為0,進程掛起等待 int sem_p(int sem_id) {struct sembuf sbuf;sbuf.sem_num = 0; /*序號*/sbuf.sem_op = -1; /*P操作*/sbuf.sem_flg = SEM_UNDO;if(semop(sem_id, &sbuf, 1) == -1){perror("P operation Error");return -1;}return 0; }// V操作: // 釋放資源并將信號量值+1 // 如果有進程正在掛起等待,則喚醒它們 int sem_v(int sem_id) {struct sembuf sbuf;sbuf.sem_num = 0; /*序號*/sbuf.sem_op = 1; /*V操作*/sbuf.sem_flg = SEM_UNDO;if(semop(sem_id, &sbuf, 1) == -1){perror("V operation Error");return -1;}return 0; }int main() {key_t key;int shmid, semid, msqid;char *shm;struct msg_form msg;int flag = 1; /*while循環條件*/// 獲取key值if((key = ftok(".", 'z')) < 0){perror("ftok error");exit(1);}// 獲取共享內存if((shmid = shmget(key, 1024, 0)) == -1){perror("shmget error");exit(1);}// 連接共享內存shm = (char*)shmat(shmid, 0, 0);if((int)shm == -1){perror("Attach Shared Memory Error");exit(1);}// 創建消息隊列if ((msqid = msgget(key, 0)) == -1){perror("msgget error");exit(1);}// 獲取信號量if((semid = semget(key, 0, 0)) == -1){perror("semget error");exit(1);}// 寫數據printf("***************************************\n");printf("* IPC *\n");printf("* Input r to send data to server. *\n");printf("* Input q to quit. *\n");printf("***************************************\n");while(flag){char c;printf("Please input command: ");scanf("%c", &c);switch(c){case 'r':printf("Data to send: ");sem_p(semid); /*訪問資源*/scanf("%s", shm);sem_v(semid); /*釋放資源*//*清空標準輸入緩沖區*/while((c=getchar())!='\n' && c!=EOF);msg.mtype = 888;msg.mtext = 'r'; /*發送消息通知服務器讀數據*/msgsnd(msqid, &msg, sizeof(msg.mtext), 0);break;case 'q':msg.mtype = 888;msg.mtext = 'q';msgsnd(msqid, &msg, sizeof(msg.mtext), 0);flag = 0;break;default:printf("Wrong input!\n");/*清空標準輸入緩沖區*/while((c=getchar())!='\n' && c!=EOF);}}// 斷開連接shmdt(shm);return 0; }注意:當scanf()輸入字符或字符串時,緩沖區中遺留下了\n,所以每次輸入操作后都需要清空標準輸入的緩沖區。但是由于 gcc 編譯器不支持fflush(stdin)(它只是標準C的擴展),所以我們使用了替代方案:
while((c=getchar())!='\n' && c!=EOF);2.6 套接字通信
套接字( socket ) : 套接口也是一種進程間通信機制,與其他通信機制不同的是,它可用于不同機器間的進程通信,一般說的進程間五大通信方式不包括這個。
通信過程如下:
2.6.1 命名socket
SOCK_STREAM 式本地套接字的通信雙方均需要具有本地地址,其中服務器端的本地地址需要明確指定,指定方法是使用 struct sockaddr_un 類型的變量。
2.6.2 綁定
SOCK_STREAM 式本地套接字的通信雙方均需要具有本地地址,其中服務器端的本地地址需要明確指定,指定方法是使用 struct sockaddr_un 類型的變量,將相應字段賦值,再將其綁定在創建的服務器套接字上,綁定要使用 bind 系統調用,其原形如下:
int bind(int socket, const struct sockaddr *address, size_t address_len);其中 socket表示服務器端的套接字描述符,address 表示需要綁定的本地地址,是一個 struct sockaddr_un 類型的變量,address_len 表示該本地地址的字節長度。
2.6.3 監聽
服務器端套接字創建完畢并賦予本地地址值(名稱,本例中為Server Socket)后,需要進行監聽,等待客戶端連接并處理請求,監聽使用 listen 系統調用,接受客戶端連接使用accept系統調用,它們的原形如下:
int listen(int socket, int backlog); int accept(int socket, struct sockaddr *address, size_t *address_len);其中 socket 表示服務器端的套接字描述符;backlog 表示排隊連接隊列的長度(若有多個客戶端同時連接,則需要進行排隊);address 表示當前連接客戶端的本地地址,該參數為輸出參數,是客戶端傳遞過來的關于自身的信息;address_len 表示當前連接客戶端本地地址的字節長度,這個參數既是輸入參數,又是輸出參數。
2.6.4 連接服務器
客戶端套接字創建完畢并賦予本地地址值后,需要連接到服務器端進行通信,讓服務器端為其提供處理服務。
對于SOCK_STREAM類型的流式套接字,需要客戶端與服務器之間進行連接方可使用。連接要使用 connect 系統調用,其原形為
其中socket為客戶端的套接字描述符,address表示當前客戶端的本地地址,是一個 struct sockaddr_un 類型的變量,address_len 表示本地地址的字節長度。實現連接的代碼如下:
connect(client_sockfd, (struct sockaddr*)&client_address, sizeof(client_address));2.6.5 客戶端與服務端通信
無論客戶端還是服務器,都要和對方進行數據上的交互,這種交互也正是我們進程通信的主題。一個進程扮演客戶端的角色,另外一個進程扮演服務器的角色,兩個進程之間相互發送接收數據,這就是基于本地套接字的進程通信。發送和接收數據要使用 write 和 read 系統調用,它們的原形為:
int read(int socket, char *buffer, size_t len); int write(int socket, char *buffer, size_t len);其中 socket 為套接字描述符;len 為需要發送或需要接收的數據長度;
對于 read 系統調用,buffer 是用來存放接收數據的緩沖區,即接收來的數據存入其中,是一個輸出參數;
對于 write 系統調用,buffer 用來存放需要發送出去的數據,即 buffer 內的數據被發送出去,是一個輸入參數;返回值為已經發送或接收的數據長度。
2.6.6 斷開連接
交互完成后,需要將連接斷開以節省資源,使用close系統調用,其原形為:
int close(int socket);三、死鎖是什么?必要條件?如何解決?
3.1 死鎖定義
所謂死鎖,是指多個進程循環等待它方占有的資源而無限期地僵持下去的局面。很顯然,如果沒有外力的作用,那麼死鎖涉及到的各個進程都將永遠處于封鎖狀態。當兩個或兩個以上的進程同時對多個互斥資源提出使用要求時,有可能導致死鎖。
本文作者:https://www.zhihu.com/people/san-hao-bai-du-ren-79
(由于文章總是被三無號到處復制發布,選擇這種方式插入原文鏈接影響閱讀實在抱歉!)
本文原文鏈接:https://blog.csdn.net/qq_41687938/article/details/118179581
3.2 死鎖發生的必要條件
3.3 解決死鎖問題
死鎖的預防是保證系統不進入死鎖狀態的一種策略。它的基本思想是要求進程申請資源時遵循某種協議,從而打破產生死鎖的四個必要條件中的一個或幾個,保證系統不會進入死鎖狀態。
<1>打破互斥條件。即允許進程同時訪問某些資源。但是,有的資源是不允許被同時訪問的,像打印機等等,這是由資源本身的屬性所決定的。所以,這種辦法并無實用價值。
<2>打破不可搶占條件。即允許進程強行從占有者那里奪取某些資源。就是說,當一個進程已占有了某些資源,它又申請新的資源,但不能立即被滿足時,它必須釋放所占有的全部資源,以后再重新申請。它所釋放的資源可以分配給其它進程。這就相當于該進程占有的資源被隱蔽地強占了。這種預防死鎖的方法實現起來困難,會降低系統性能。????
<3>打破占有且申請條件。可以實行資源預先分配策略。即進程在運行前一次性地向系統申請它所需要的全部資源。如果某個進程所需的全部資源得不到滿足,則不分配任何資源,此進程暫不運行。只有當系統能夠滿足當前進程的全部資源需求時,才一次性地將所申請的資源全部分配給該進程。由于運行的進程已占有了它所需的全部資源,所以不會發生占有資源又申請資源的現象,因此不會發生死鎖。
<4>打破循環等待條件,實行資源有序分配策略。采用這種策略,即把資源事先分類編號,按號分配,使進程在申請,占用資源時不會形成環路。所有進程對資源的請求必須嚴格按資源序號遞增的順序提出。進程占用了小號資源,才能申請大號資源,就不會產生環路,從而預防了死鎖
死鎖避免:銀行家算法
四、線程的同步方式
總結
以上是生活随笔為你收集整理的关于操作系统中进程、线程、死锁、同步、进程间通信(IPC)的超详细详解整理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 程序员情人节送这些!
- 下一篇: java get cookies_Jav