Linux的進程操作方式主要有產生進程、終止進程,並且進程之間存在數據和控制的交互,即進程間通信和同步。
進程的產生有多種方式,其基本過程是一致的。
(1)首先復制其父進程的環境配置。
(2)在內核中建立進程結構。
(3)將結構插入到進程列表,便於維護。
(4)分配資源給此進程。
(5)復制父進程的內存映射信息。
(6)管理文件描述符和鏈接點。
(7)通知父進程。
有5種方式使進程終止:
從main返回。
調用exit。
調用_exit。
調用abort。
由一個信號終止。
進程在終止的時候,系統會釋放進程所擁有的資源,例如內存、文件符、內核結構等。
進程之間的通信有多種方式,其中管道、共享內存和消息隊列是最常用的方式。
管道:利用Linux內核在兩個進程之間建立管道,在管道的一端只寫,另一端只讀。利用讀寫的方式在進程間傳遞數據。
共享內存:將內存中的一段內存地址,在多個進程之間共享,多個進程利用獲得的共享內存的地址來直接對地址進行操作。
消息隊列:在內核中建立一個鏈表,發送方按照一定的標識將數據發送到內核中,內核將其放入量表中,等待對方接收方請求,接收方發送請求,內核按照請求的標識,從內核中的鏈表中將其取下,傳遞給接收方。這個是一種異步操作,等到請求的時候,才發送數據。
多個進程之間需要協作完成任務時,經常發生任務之間的依賴現象,從而出現了進程的同步問題。Linux下進程的同步方式主要有消息隊列、信號量等。信號量是一個共享的表示數量的值。用於多個進程之間操作或者共享資源的保護,它是進程之間同步的最主要方式。
線程和進程是另一對有意義的概念,主要區別和聯系如下:
進程是操作系統進行資源分配的基本單位,進程擁有完整的虛擬空間。進行系統資源分配的時候,除了CPU資源之外,不會給線程分配獨立的資源,線程所需要的資源需要共享。
線程是進程的一部分
多個線程之間像內存、變量等資源可以通過簡單的辦法共享,多進程則不同,進程間的共享方式有限。
進程有進程控制表PCB,系統通過PCB對進程進行調度;線程有線程控制表TCB。
進程號:每個進程初始化的時候,都會分配一個ID號,用次來標識進程,進程號是唯一的。
linux可以通過getpid()和getppid()來獲得當前進程和其父進程。
#include
#include
#include
int main()
{
pid_t pid,ppid;
/* 獲得當前進程和其父進程的ID號 */
pid = getpid();
ppid = getppid();
printf("當前進程的ID號為:%d\n",pid);
printf("當前進程的的父進程號ID號為:%d\n",ppid);
return 0;
}
進程復制:fork()函數以父進程為藍本復制一個進程,其ID號和父進程ID號不同。
#include
#include
#include
#include
int main(void)
{
pid_t pid;
/* 分叉進程 */
pid = fork();
/* 判斷是否執行成功 */
if(-1 == pid){
printf("進程創建失敗!\n");
return -1;
} else if(pid == 0){
/* 子進程中執行此段代碼 */
printf("子進程,fork返回值:%d, ID:%d, 父進程ID:%d\n",pid, getpid(), getppid());
} else{
/* 父進程中執行此段代碼 */
printf("父進程,fork返回值:%d, ID:%d, 父進程ID:%d\n",pid, getpid(), getppid());
}
return 0;
}
system()函數調用shell的外部命令在當前進程中開始另一個進程。
在使用fork()函數和system()函數的時候,系統中都會建立一個新的進程,執行調用者的操作,而原來的進程還會存在,直到用戶顯式地退出;
#include
#include
#include
int main()
{
int ret;
printf("系統分配的進程號是:%d\n",getpid());
ret = system("ping www.baidu.com -c 2");
printf("返回值為:%d\n",ret);
return 0;
}
而exec()族的函數與之前的fork()和system()函數不同,exec()族函數會用新進程代替原有的進程,系統會從新的進程運行,新進程的PID值會與原來進程的PID值相同。
#include
#include
int main(void)
{
char *args[]={"/bin/ls",NULL};
printf("系統分配的進程號是:%d\n",getpid());
if(execve("/bin/ls",args,NULL)<0)
printf("創建進程出錯!\n");
return 0;
}
注意:在Linux系統中,所有的進程都是有父子或者堂兄關系的,除了初始進程init,沒有哪個進程與其他進程完全獨立。
管道是一種把兩個進程之間的標准輸入和標准輸出連接起來的機制。管道是一種歷史悠久的進程間通信的辦法,自UNIX操作系統誕生,管道就存在了。
由於管道僅僅是將某個進程的輸出和另一個進程的輸入相連接的單向通信的辦法,因此稱其為“半雙工”。在shell中管道用“|”表示。
#include
#include
#include
#include
#include
int main(void)
{
int result = -1; /*創建管道結果*/
int fd[2]; /*文件描述符,字符個數*/
pid_t pid; /*PID值*/
/* 文件描述符1用於寫,文件描述符0用於讀 */
int *write_fd = &fd[1]; /*寫文件描述符*/
int *read_fd = &fd[0]; /*讀文件描述符*/
result = pipe(fd); /*建立管道*/
if( -1 == result) /*建立管道失敗*/
{
printf("建立管道失敗\n"); /*打印信息*/
return -1; /*返回錯誤結果*/
}
pid = fork(); /*分叉程序*/
if( -1 == pid) /*fork失敗*/
{
printf("fork 進程失敗\n"); /*打印信息*/
return -1; /*返回錯誤結果*/
}
if( 0 == pid) /*子進程*/
{
close(*read_fd); /*關閉讀端*/
}
else /*父進程*/
{
close(*write_fd); /*關閉寫端*/
}
return 0;
}
管道讀寫操作:
#include
#include
#include
#include
#include
int main(void)
{
int result = -1; /*創建管道結果*/
int fd[2],nbytes; /*文件描述符,字符個數*/
pid_t pid; /*PID值*/
char string[] = "你好,管道";
char readbuffer[80];
/* 文件描述符1用於寫,文件描述符0用於讀 */
int *write_fd = &fd[1]; /*寫文件描述符*/
int *read_fd = &fd[0]; /*讀文件描述符*/
result = pipe(fd); /*建立管道*/
if( -1 == result) /*建立管道失敗*/
{
printf("建立管道失敗\n"); /*打印信息*/
return -1; /*返回錯誤結果*/
}
pid = fork(); /*分叉程序*/
if( -1 == pid) /*fork失敗*/
{
printf("fork 進程失敗\n"); /*打印信息*/
return -1; /*返回錯誤結果*/
}
if( 0 == pid) /*子進程*/
{
close(*read_fd); /*關閉讀端*/
result = write(*write_fd,string,strlen(string)); /*向管道端寫入字符*/
return 0;
}
else /*父進程*/
{
close(*write_fd); /*關閉寫端*/
nbytes = read(*read_fd, readbuffer,sizeof(readbuffer)); /*從管道讀取數值*/
printf("接收到%d個數據,內容為:”%s“\n",nbytes,readbuffer); /*打印結果*/
}
return 0;
}
管道阻塞和管道操作的原子性
當管道的寫端沒有關閉時,如果寫請求的字節數目大於阈值PIPE_BUF,寫操作的返回值是管道中目前的數據字節數,如果請求的字節數目不大於PIPE_BUF,則返回管道中現有數據字節數(此時,管道中數據量小於請求的數據量);或者返回請求的字節數(此時,管道中數據量不小於請求的數據量)。
注意:管道寫入數據時,Linux將不保證寫入的原子性,管道緩沖區一有空閒區域,寫進程就會試圖向管道寫入數據。如果讀進程不讀取管道緩沖區中的數據,那麼寫進程將會一直阻塞。
#include
#include
#include
#include
#include
#define K 1024
#define WRITELEN (128*K)
int main(void)
{
int result = -1; /*創建管道結果*/
int fd[2],nbytes; /*文件描述符,字符個數*/
pid_t pid; /*PID值*/
char string[WRITELEN] = "你好,管道";
char readbuffer[10*K]; /*讀緩沖區*/
/* 文件描述符1用於寫,文件描述符0用於讀 */
int *write_fd = &fd[1];
int *read_fd = &fd[0];
result = pipe(fd); /*建立管道*/
if( -1 == result) /*建立管道失敗*/
{
printf("建立管道失敗\n"); /*打印信息*/
return -1; /*返回錯誤結果*/
}
pid = fork(); /*分叉程序*/
if( -1 == pid) /*fork失敗*/
{
printf("fork 進程失敗\n"); /*打印信息*/
return -1; /*返回錯誤結果*/
}
if( 0 == pid) /*子進程*/
{
int write_size = WRITELEN; /*寫入的長度*/
result = 0; /*結果*/
close(*read_fd); /*關閉讀端*/
while( write_size >= 0) /*如果沒有將數據寫入繼續操作*/
{
result = write(*write_fd,string,write_size); /*寫入管道數據*/
if(result >0) /*寫入成功*/
{
write_size -= result; /*寫入的長度*/
printf("寫入%d個數據,剩余%d個數據\n",result,write_size);
}
else /*寫入失敗*/
{
sleep(10); /*等待10s,讀端將數據讀出*/
}
}
return 0;
}
else /*父進程*/
{
close(*write_fd); /*關閉寫端*/
while(1) /*一直讀取數據*/
{
nbytes = read(*read_fd, readbuffer,sizeof(readbuffer)); /*讀取數據*/
if(nbytes <= 0) /*讀取失敗*/
{
printf("沒有數據寫入了\n");/*打印信息*/
break; /*退出循環*/
}
printf("接收到%d個數據,內容為:”%s“\n",nbytes,readbuffer);
}
}
return 0;
}
命名管道的工作方式與普通的管道非常相似,但也有一些明顯的區別。
在文件系統中命名管道是以設備特殊文件的形式存在的。
不同的進程可以通過命名管道共享數據。
在FIFO中,必須使用一個open()函數來顯式地建立連接到管道的通道。一般來說FIFO總是處於阻塞狀態。如果不希望在進行命名管道操作的時候發生阻塞,可以在open()調用中使用O_NONBLOCK標志,以關閉默認的阻塞動作。
消息隊列是內核地址空間中的內部鏈表,通過Linux內核在各個進程之間傳遞內容。消息順序地發送到消息隊列中,每個消息隊列可以用IPC標識符唯一地進行標識。內核中的消息隊列是通過IPC的標識符來區別的,不同的消息隊列之間是相對獨立的。每個消息隊列中的消息,又構成一個獨立的鏈表。
1.消息緩沖區結構
常用的結構是msgbuf結構。程序員可以以這個結構為模板定義自己的消息結構。
struct msgbuf {
long mtype;
char mtext[1];
};
2.結構msgid_ds
struct msqid_ds {
struct ipc_perm msg_perm;
time_t msg_stime; /發送到隊列的最後一個消息的時間戳/
time_t msg_rtime; /從隊列中獲取的最後一個消息的時間戳/
time_t msg_ctime; /對隊列進行最後一次變動的時間戳/
unsigned long __msg_cbytes; /在隊列上所駐留的字節總數/
msgqnum_t msg_qnum; /當前處於隊列中的消息數目/
msglen_t msg_qbytes; /隊列中能容納的字節的最大數目/
pid_t msg_lspid; /發送最後一個消息進程的PID /
pid_t msg_lrpid; /接收最後一個消息進程的PID /
};
3.結構ipc_perm
內核把IPC對象的許可權限信息存放在ipc_perm類型的結構中。
struct ipc_perm {
key_t key; /函數msgget()使用的鍵值/
uid_t uid; /用戶的UID/
gid_t gid; /用戶的GID/
uid_t cuid; /建立者的UID/
gid_t cgid; /建立者的GID/
unsigned short mode; /權限 /
unsigned short seq; /序列號/
};
4.內核中的消息隊列關系
作為IPC的消息隊列,其消息的傳遞是通過Linux內核來進行的。
5.鍵值構建ftok()函數
ftok()函數將路徑名和項目的表示符轉變為一個系統V的IPC鍵值。
6.獲得消息msgget()函數
創建一個新的消息隊列,或者訪問一個現有的隊列,可以使用函數msgget()
7.發送消息msgsnd()函數
一旦獲得了隊列標識符,用戶就可以開始在該消息隊列上執行相關操作了。為了向隊列傳遞消息,用戶可以使用msgsnd()函數
8.接收消息msgrcv()函數
當獲得隊列標識符後,用戶就可以開始在該消息隊列上執行消息隊列的接收操作。msgrcv()函數用於接收隊列標識符中的消息
9.消息控制msgctl()函數
為了在一個消息隊列上執行控制操作,用戶可以使用msgctl()函數。
看下面一個例子:
在建立消息隊列後,打印其屬性,並在每次發送和接收後均查看其屬性,最後對消息隊列進行修改。
#include
#include
#include
#include
#include
#include
#include
#include
void msg_show_attr(int msg_id, struct msqid_ds msg_info) /*打印消息屬性的函數*/
{
int ret = -1;
sleep(1);
ret = msgctl(msg_id, IPC_STAT, &msg_info); /*獲取消息*/
if( -1 == ret)
{
printf("獲得消息信息失敗\n"); /*獲取消息失敗,返回*/
return ;
}
printf("\n"); /*以下打印消息的信息*/
printf("現在隊列中的字節數:%ld\n",msg_info.msg_cbytes); /*消息隊列中的字節數*/
printf("隊列中消息數:%d\n",(int)msg_info.msg_qnum); /*消息隊列中的消息數*/
printf("隊列中最大字節數:%d\n",(int)msg_info.msg_qbytes); /*消息隊列中的最大字節數*/
printf("最後發送消息的進程pid:%d\n",msg_info.msg_lspid); /*最後發送消息的進程*/
printf("最後接收消息的進程pid:%d\n",msg_info.msg_lrpid); /*最後接收消息的進程*/
printf("最後發送消息的時間:%s",ctime(&(msg_info.msg_stime))); /*最後發送消息的時間*/
printf("最後接收消息的時間:%s",ctime(&(msg_info.msg_rtime))); /*最後接收消息的時間*/
printf("最後變化時間:%s",ctime(&(msg_info.msg_ctime))); /*消息的最後變化時間*/
printf("消息UID是:%d\n",msg_info.msg_perm.uid); /*消息的UID*/
printf("消息GID是:%d\n",msg_info.msg_perm.gid); /*消息的GID*/
}
int main(void)
{
int ret = -1;
int msg_flags, msg_id;
key_t key;
struct msgmbuf{ /*消息的緩沖區結構*/
int mtype;
char mtext[10];
};
struct msqid_ds msg_info;
struct msgmbuf msg_mbuf;
int msg_sflags,msg_rflags;
char *msgpath = "/ipc/msg/"; /*消息key產生所用的路徑*/
key = ftok(msgpath,'b'); /*產生key*/
if(key != -1) /*產生key成功*/
{
printf("成功建立KEY\n");
}
else /*產生key失敗*/
{
printf("建立KEY失敗\n");
}
msg_flags = IPC_CREAT|IPC_EXCL; /*消息的類型*/
msg_id = msgget(key, msg_flags|0x0666); /*建立消息*/
if( -1 == msg_id)
{
printf("消息建立失敗\n");
return 0;
}
msg_show_attr(msg_id, msg_info); /*顯示消息的屬性*/
msg_sflags = IPC_NOWAIT;
msg_mbuf.mtype = 10;
memcpy(msg_mbuf.mtext,"測試消息",sizeof("測試消息")); /*復制字符串*/
ret = msgsnd(msg_id, &msg_mbuf, sizeof("測試消息"), msg_sflags); /*發送消息*/
if( -1 == ret)
{
printf("發送消息失敗\n");
}
msg_show_attr(msg_id, msg_info); /*顯示消息屬性*/
msg_rflags = IPC_NOWAIT|MSG_NOERROR;
ret = msgrcv(msg_id, &msg_mbuf, 10,10,msg_rflags); /*接收消息*/
if( -1 == ret)
{
printf("接收消息失敗\n");
}
else
{
printf("接收消息成功,長度:%d\n",ret);
}
msg_show_attr(msg_id, msg_info); /*顯示消息屬性*/
msg_info.msg_perm.uid = 8;
msg_info.msg_perm.gid = 8;
msg_info.msg_qbytes = 12345;
ret = msgctl(msg_id, IPC_SET, &msg_info); /*設置消息屬性*/
if( -1 == ret)
{
printf("設置消息屬性失敗\n");
return 0;
}
msg_show_attr(msg_id, msg_info); /*顯示消息屬性*/
ret = msgctl(msg_id, IPC_RMID,NULL); /*刪除消息隊列*/
if(-1 == ret)
{
printf("刪除消息失敗\n");
return 0;
}
return 0;
}
信號量是一種計數器,用來控制對多個進程共享的資源所進行的訪問。它們常常被用做一個鎖機制,在某個進程正在對特定資源進行操作時,信號量可以防止另一個進程去訪問它。
信號量數據結構是信號量程序設計中經常使用的數據結構
union semun { /信號量操作的聯合結構/
int val; /整型變量/
struct semid_ds buf; /*semid_ds結構指針/
unsigned short array; /數組類型*/
struct seminfo __buf; /信號量內部結構*/
};
新建信號量函數semget();
信號量操作函數semop();
控制信號量參數semctl();
一個信號量的例子,代碼先建立一個信號量,然後再對這個信號量進行P、V操作,並將信號量的值打印出來,最後銷毀信號量。
:
#include
#include
#include
typedef int sem_t;
union semun { /*信號量操作的聯合結構*/
int val; /*整型變量*/
struct semid_ds *buf; /*semid_ds結構指針*/
unsigned short *array; /*數組類型*/
} arg; /*定義一個全局變量*/
sem_t CreateSem(key_t key, int value) /*建立信號量,魔數key和信號量的初始值 value*/
{
union semun sem; /*信號量結構變量*/
sem_t semid; /*信號量ID*/
sem.val = value; /*設置初始值*/
semid = semget(key,0,IPC_CREAT|0666); /*獲得信號量的ID*/
if (-1 == semid) /*獲得信號量ID失敗*/
{
printf("create semaphore error\n");/*打印信息*/
return -1; /*返回錯誤*/
}
semctl(semid,0,SETVAL,sem); /*發送命令,建立value個初始值的信號量*/
return semid; /*返回建立的信號量*/
}
int Sem_P(sem_t semid) /*增加信號量*/
{
struct sembuf sops={0,+1,IPC_NOWAIT}; /*建立信號量結構值*/
return (semop(semid,&sops,1)); /*發送命令*/
}
int Sem_V(sem_t semid) /*減小信號量值*/
{
struct sembuf sops={0,-1,IPC_NOWAIT}; /*建立信號量結構值*/
return (semop(semid,&sops,1)); /*發送信號量操作方法*/
}
void SetvalueSem(sem_t semid, int value) /*設置信號量的值*/
{
union semun sem; /*信號量操作的結構*/
sem.val = value; /*值初始化*/
semctl(semid,0,SETVAL,sem); /*設置信號量的值*/
}
int GetvalueSem(sem_t semid) /*獲得信號量的值*/
{
union semun sem; /*信號量操作的結構*/
return semctl(semid,0,GETVAL,sem); /*獲得信號量的值*/
}
void DestroySem(sem_t semid) /*銷毀信號量*/
{
union semun sem; /*信號量操作的結構*/
sem.val = 0; /*信號量值的初始化*/
semctl(semid,0,IPC_RMID,sem); /*設置信號量*/
}
int main(void)
{
key_t key; /*信號量的鍵值*/
int semid; /*信號量的ID*/
char i;
int value = 0;
key = ftok("/ipc/sem",'a'); /*建立信號量的鍵值*/
semid = CreateSem(key,100); /*建立信號量*/
for (i = 0;i <= 3;i++){ /*對信號量進行3次增減操作*/
Sem_P(semid); /*增加信號量*/
Sem_V(semid); /*減小信號量*/
}
value = GetvalueSem(semid); /*獲得信號量的值*/
printf("信號量值為:%d\n",value); /*打印結果*/
DestroySem(semid); /*銷毀信號量*/
return 0;
}
共享內存是在多個進程之間共享內存區域的一種進程間的通信方式,它是在多個進程之間對內存段進行映射的方式實現內存共享的。
1.創建共享內存函數shmget()
函數shmget()用於創建一個新的共享內存段,或者訪問一個現有的共享內存段,它與消息隊列以及信號量集合對應的函數十分相似
2.獲得共享內存地址函數shmat()
函數shmat()用來獲取共享內存的地址,獲取共享內存成功後,可以像使用通用內存一樣對其進行讀寫操作。
3.刪除共享內存函數shmdt()
函數shmdt()用於刪除一段共享內存。
4.共享內存控制函數shmctl()
共享內存的控制函數shmctl()的使用類似ioctl()的方式對共享內存進行操作:向共享內存的句柄發送命令來完成某種功能。
5.一個共享內存的例子
在父進程和子進程之間利用共享內存進行通信,父進程向共享內存中寫入數據,子進程讀出數據。兩個進程之間的控制采用了信號量的方法,父進程寫入數據成功後,信號量加1,子進程在訪問信號量之前先等待信號。
#include
#include
#include
#include
static char msg[]="你好,共享內存\n";
int main(void)
{
key_t key;
int semid,shmid;
char i,*shms,*shmc;
struct semid_ds buf;
int value = 0;
char buffer[80];
pid_t p;
key = ftok("/ipc/sem",'a'); /*生成鍵值*/
shmid = shmget(key,1024,IPC_CREAT|0604); /*獲得共享內存,大小為1024個 字節*/
semid = CreateSem(key,0); /*建立信號量*/
p = fork(); /*分叉程序*/
if(p > 0) /*父進程*/
{
shms = (char *)shmat(shmid,0,0); /*掛接共享內存*/
memcpy(shms, msg, strlen(msg)+1); /*復制內容*/
sleep(10); /*等待10s,另一個進程將數據讀 出*/
Sem_P(semid); /*獲得共享內存的信號量*/
shmdt(shms); /*摘除共享內存*/
DestroySem(semid); /*銷毀信號量*/
}
else if(p == 0) /*子進程*/
{
shmc = (char *)shmat(shmid,0,0); /*掛接共享內存*/
Sem_V(semid); /*減小信號量*/
printf("共享內存的值為:%s\n",shmc); /*打印信息*/
shmdt(shmc); /*摘除共享內存*/
}
return 0;
}
信號用於在一個或多個進程之間傳遞異步信號。信號可以由各種異步事件產生,例如鍵盤中斷等。Shell也可以使用信號將作業控制命令傳遞給它的子進程。
1.信號截取函數signal()
signal()函數用於截取系統的信號,對此信號掛接用戶自己的處理函數。
2.向進程發送信號函數kill()和raise()
在掛接信號處理函數後,可以等待系統信號的到來。同時,用戶可以自己構建信號,發送到目標進程中。此類函數有kill()和raise()函數
#include
#include
typedef void (*sighandler_t)(int);
static void sig_handle(int signo) /*信號處理函數*/
{
if( SIGSTOP== signo) /*為SIGSTOP信號*/
{
printf("接收到信號SIGSTOP\n"); /*打印信息*/
}
else if(SIGKILL==signo) /*為SIGKILL信號*/
{
printf("接收到信號SIGKILL\n"); /*打印信息*/
}
else /*其他信號*/
{
printf("接收到信號:%d\n",signo); /*打印信息*/
}
return;
}
int main(void)
{
sighandler_t ret;
ret = signal(SIGSTOP, sig_handle); /*掛接SIGSTOP信號處理函數*/
if(SIG_ERR == ret) /*掛接失敗*/
{
printf("為SIGSTOP掛接信號處理函數失敗\n");
return -1; /*返回*/
}
ret = signal(SIGKILL, sig_handle); /*掛接SIGKILL處理函數*/
if(SIG_ERR == ret) /*掛接失敗*/
{
printf("為SIGKILL掛接信號處理函數失敗\n");
return -1; /*返回*/
}
for(;;); /*等待程序退出*/
}
linux下的進程之間的通信主要方式就這麼多,這裡從書上看來的,認為很好的學習材料,並自己總結下,方便以後自己遺忘後,再學習。