Unix環境高級編程

共享內存&文件映射

1、文件映射、內存映射(存儲映射I/O)

P391
【定義】
存儲映射I/O使一個磁盤文件與存儲空間中的一個緩存相映射。於是當從緩存中取數據,就相當於讀文件中的相應字節。與其類似,將數據存入緩存,則相應字節就自動地寫入文件。這樣,就可以在不使用read和write的情況下執行I/O。
【作用】
1. 直接用內存映射文件來訪問磁盤上的數據文件,無需再進行文件的I/0操作.
2. 用來在多個進程之間共享數據.
【API】
1、將給定的文件映射到一個存儲區域中

void * mmap(void addr, size_t len, int prot, int flag, int fd, off_t off);
//返回:若成功則爲存儲區的起始地址,若出錯則爲- 1

addr:參數用於指定映射存儲區的起始地址。通常將其設置爲0,這表示由系統選擇該映射區的起始地址。
len:是映射的字節數
prot:參數說明映射存儲區的保護要求 (可讀,可寫,可執行,不可訪問)
flag:參數影響映射存儲區的多種屬性
fd:指定要被映射文件的描述符
off:是要映射字節在文件中的起始位移量

保護要求:

PROT_READ|PROT_WRITE|PROT_EXEC|PROT_NONE

2、修改在內存映像上的保護模式

int mprotect(const void *addr, size_t len, int prot);

3、把映像的文件寫入磁盤

int msync(const void *addr, size_t len, int flags);

【signal】
SIGSEGV: 指示進程試圖存取它不能存取的存儲區。如果進程企圖存數據到用mmap指定爲只讀的映射存儲區,那麼也產生此信號。
SIGBUS: 存取一個已不存在的存取映射區時產生

【多進程】
在fork之後,子進程繼承存儲映射區(因爲子進程複製父進程地址空間,而存儲映射區是該地址空間中的一部分),但是由於同樣的理由,exec後的新程序則不繼承此存儲映射區。(關閉文件描述符也不影響存儲映射區,磁盤文件的頁高速緩存並不會因爲進程的撤銷而撤銷,如果有足夠的空閒內存,頁高速緩存中的頁將長期存在,使其它進程再使用該頁時不再訪問磁盤。)

【編程實例】
P394

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>     //close
#include <fcntl.h>      //open  O_RDWR
#include <sys/mman.h>

#define FILEPATH "/tmp/mmapped.bin"
#define NUMINTS (1000)
#define FILESIZE (NUMINTS * sizeof(int))

int main(int argc, char *argv[])
{
    int i;
    int fd;
    int result;
    int *map; /* mmapped array of int's */

    /* Open a file for writing.
     * - Creating the file if it doesn't exist.
     * - Truncating it to 0 size if it already exists. (not really needed)
     *
     * Note: "O_WRONLY" mode is not sufficient when mmaping.
     */
    fd = open(FILEPATH, O_RDWR | O_CREAT | O_TRUNC, (mode_t)0600);
    if (fd == -1) {
        perror("Error opening file for writing");
        exit(EXIT_FAILURE);
    }

    /* Stretch the file size to the size of the (mmapped) array of ints
     */
    result = lseek(fd, FILESIZE-1, SEEK_SET);
    if (result == -1) {
        close(fd);
        perror("Error calling lseek() to 'stretch' the file");
        exit(EXIT_FAILURE);
    }

    /* Something needs to be written at the end of the file to
     * have the file actually have the new size.
     * Just writing an empty string at the current file position will do.
     *
     * Note:
     * - The current position in the file is at the end of the stretched 
     * file due to the call to lseek().
     * - An empty string is actually a single '\0' character, so a zero-byte
     * will be written at the last byte of the file.
     */
    //需要在文件末尾寫入一個結束符,表示該文件有創建時的大小,不然後面的mmap會產生bus error 閱讀P394
    result = write(fd, "", 1);    
    if (result != 1) {
        close(fd);
        perror("Error writing last byte of the file");
        exit(EXIT_FAILURE);
    }

    /* Now the file is ready to be mmapped.
     */
    map = mmap(0, FILESIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (map == MAP_FAILED) {
        close(fd);
        perror("Error mmapping the file");
        exit(EXIT_FAILURE);
    }

    /* Now write int's to the file as if it were memory (an array of ints).
     */
    for (i = 1; i <=NUMINTS; ++i) {
        map[i] = 2 * i; 
    }

     /* Read the file int-by-int from the mmap
     */
    for (i = 1; i <=NUMINTS; ++i) {
        printf("%d: %d\n", i, map[i]);
    }

    /* Don't forget to free the mmapped memory
     */
    if (munmap(map, FILESIZE) == -1) {
        perror("Error un-mmapping the file");
    /* Decide here whether to close(fd) and exit() or not. Depends... */
    }

    /* Un-mmaping doesn't close the file, so we still need to do that.
     */
    close(fd);
    return 0;
}

【內存映射用於共享內存的IPC】
系統調用mmap()用於共享內存的兩種方式:
(1)使用普通文件提供的內存映射:適用於任何進程之間;此時,需要打開或創建一個文件,然後再以MAP_SHARED調用mmap();
典型調用代碼如下:

fd = open(name, flag, mode); 
    if(fd<0) ...
ptr = mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); 

通過mmap()實現共享內存的通信方式有許多特點和要注意的地方,我們將在範例中進行具體說明。
(2)使用特殊文件提供匿名內存映射:適用於具有親緣關係的進程之間;
由於父子進程特殊的親緣關係,在父進程中先調用mmap(),然後調用fork()。那麼在調用fork()之後,子進程繼承父進程匿名映射後的地址空間,同樣也繼承mmap()返回的地址,這樣,父子進程就可以通過映射區域進行通信了。注意,這裏不是一般的繼承關係。一般來說,子進程單獨維護從父進程繼承下來的一些變量。而mmap()返回的地址,卻由父子進程共同維護。對於具有親緣關係的進程實現共享內存最好的方式應該是採用匿名內存映射的方式。此時,不必指定具體的文件,只要設置相應的標誌即可,參見範例2。
參考:內存映射

2、共享內存

【定義】
允許兩個或多個進程共享一給定的存儲區。因爲數據不需要在客戶機和服務器之間複製,所以這是最快的一種IPC。使用共享存儲的唯一竅門是多個進程之間對一給定存儲區的同步存取。若服務器將數據放入共享存儲區,則在服務器做完這一操作之前,客戶機不應當去取這些數據。通常,信號量被用來實現對共享存儲存取的同步。
【實現原理】
共享內存實現原理就是:將相同的物理頁加入不同進程的地址空間。
將某個物理頁加入進程的地址空間,要做的事情就是將物理頁的物理地址填寫進程頁目錄表內的表項和頁表內的表項。
顯然進程中要加入一塊物理頁,就必須對應一塊線性空間,於是就要先申請一塊線性空間,然後根據這塊線性空間填寫頁目錄表內的表項和頁表內的表項。(這裏可以說明爲什麼不同進程中的不同線性地址可以對應相同的物理地址)。
參考: Linux共享內存實例及文件映射編程及實現原理
【API】
1、獲得一個共享存儲標識符

int shmget(key_t key, size_t size, int flag);
//返回:成功返回共享存儲標識符ID,出錯返回-1

創建一個新的IPC結構(服務器創建)
key: IPC_PRIVATE
flag: 指定IPC_CREATE
size: 共享存儲段的長度,創建時段內內容爲0,所有的內存分配操作都是以頁爲單位的

訪問現存IPC結構(用戶端)
key: 等於創建該隊列時所指定的鍵,不能指定IPC_PRIVATE
flag: 不能指定IPC_CREATE
size: 指定爲0

flag:
IPC_CREAT 如果共享內存不存在,則創建一個共享內存,否則打開操作。
IPC_EXCL 只有在共享內存不存在的時候,新的共享內存才建立,否則就產生錯誤。
IPC_ EXEL標誌本身並沒有太大的意義,但是和IPC_CREAT標誌一起使用可以用來保證所得的對象是新建的,而不是打開已有的對象。

返回值
成功返回共享內存的標識符;不成功返回-1,errno儲存錯誤原因。
EINVAL 參數size小於SHMMIN或大於SHMMAX。
EEXIST 預建立key所致的共享內存,但已經存在。
EIDRM 參數key所致的共享內存已經刪除。
ENOSPC 超過了系統允許建立的共享內存的最大值(SHMALL )。
ENOENT 參數key所指的共享內存不存在,參數shmflg也未設IPC_CREAT位。
EACCES 沒有權限。
ENOMEM 核心內存不足。

2、連接到共享內存的地址空間
把共享內存區對象映射到調用進程的地址空間

void *shmat(int shmid, void *addr, int flag) ;

shmid:共享內存標識符
addr:指定共享內存出現在進程內存地址的什麼位置,直接指定爲NULL/0讓內核自己決定一個合適的地址位置
flag:SHM_RDONLY:爲只讀模式,其他爲讀寫模式

【多線程】
fork後子進程繼承已連接的共享內存地址。exec後該子進程與已連接的共享內存地址自動脫離(detach)。進程結束後,已連接的共享內存地址會自動脫離(detach)

3、shmctl(共享內存管理)

int shmctl(int shmid, int cmd, struct shmid_ds *buf)
//成功:0 出錯:-1,錯誤原因存於error中

msqid: 共享內存標識符
cmd:
IPC_ STAT:得到共享內存的狀態,把共享內存的shmid_ds結構複製到buf中
IPC_ SET: 改變共享內存的狀態,把buf所指的shmid_ ds結構中的uid、gid、mode複製到共享內存的shmid_ds結構內
IPC_RMID:刪除這片共享內存
buf: 共享內存管理結構體

【返回值】
EACCESS:參數cmd爲IPC_STAT,確無權限讀取該共享內存
EFAULT:參數buf指向無效的內存地址
EIDRM: 標識符爲msqid的共享內存已被刪除
EINVAL:無效的參數cmd或shmid
EPERM:參數cmd爲IPC_ SET或IPC_RMID,卻無足夠的權限執行

4、脫接共享存儲段,並不刪除

int shmdt(const void *shmaddr)
//成功:0 出錯:-1,

【編程實例】
父子進程通信範例

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <error.h>

#define SIZE 1024

int main()
{
    int shmid ;
    char *shmaddr ;
    struct shmid_ds buf ;
    int flag = 0 ;
    int pid ;

    shmid = shmget(IPC_PRIVATE, SIZE, IPC_CREAT|0600 ) ;
    if ( shmid < 0 ){
            perror("get shm  ipc_id error") ;
            return -1 ;
    }

    pid = fork() ;
    if ( pid == 0 )
    {
        shmaddr = (char *)shmat( shmid, NULL, 0 ) ;

        if ( (int)shmaddr == -1 ){
            perror("shmat addr error") ;
            return -1 ;
        }
        strcpy( shmaddr, "Hi, I am child process!\n") ;

        shmdt( shmaddr ) ;

        return  0;
    } else if ( pid > 0) {
        sleep(3 ) ;

        flag = shmctl( shmid, IPC_STAT, &buf) ;
        if ( flag == -1 ){
            perror("shmctl shm error") ;
            return -1 ;
        }
        printf("shm_segsz =%d bytes\n", buf.shm_segsz ) ;
        printf("parent pid=%d, shm_cpid = %d \n", getpid(), buf.shm_cpid ) ;
        printf("chlid pid=%d, shm_lpid = %d \n",pid , buf.shm_lpid ) ;

        shmaddr = (char *) shmat(shmid, NULL, 0 ) ;

        if ( (int)shmaddr == -1 ){
            perror("shmat addr error") ;
            return -1 ;
        }
        printf("%s", shmaddr) ;

        shmdt( shmaddr ) ;
        shmctl(shmid, IPC_RMID, NULL) ;
    }else{
        perror("fork error") ;
        shmctl(shmid, IPC_RMID, NULL) ;
    }

    return 0 ;
}

參考:共享內存函數及其範例

信號機制

【頭文件】
signal.h
【種類】
SIGSTOP:暫停進程的執行,讓出CPU
SIGKILL:終止進程
SIGCHLD:進程Terminate或Stop的時候,SIGCHLD會發送給它的父進程。
SIGALRM: 當alarm設置的計時器超時時產生。
【API】

1、signal(參數1,參數2);

//SIGINT信號代表由InterruptKey產生,通常是CTRL +C 或者是DELETE 。發送給所有ForeGround Group的進程。
signal(SIGINT ,SIG_ING );    //SIG_ING忽略信號 SIG_DFL默認動作 

int main()
{
    signal(SIGINT, SIG_IGN);   //ps -aux kill命令殺掉
    while(1)
    {

    }
}   

2、void ( * signal( int sig, void (* handler)( int ) ) )( int );
3、typedef void (* sigfunc)(int);
sigfunc signal(int sig, sigfunc handler);
//返回值:返回之前的信號處理程序的指針。
注意:void (* handler)( int )
【實例】

#include <stdio.h>
#include <signal.h>
#include <unistd.h>  //fork

typedef void (* sigfunc)(int);

void sigint_pro(int arg)
{
    if(arg == SIGINT)
    {
        printf("sig int get\n");
    }
    else if(arg == SIGCHLD)
    {
        printf("child die\n");
    }
    else
    {
        printf("no get\n");
    }
}

int main()
{
    sigfunc p = signal(SIGINT, sigint_pro);
    p = signal(SIGCHLD, sigint_pro);

    pid_t pid = fork();
    if(pid < 0)
    {
        perror("fork");
        return 1;
    }
    else if(pid == 0)
    {
        printf("child\n");
        _exit(0);
    }
    else
    {
        sleep(3);
    }    
}

4、 kill
int kill(pid_t pid, int signo); //將信號發送給進程(pid > 0)或進程組(pid == 0 or < 0)
//當signo=0時,用於檢查某個進程是否存在,不存在時kill返回-1
int raise(int signo); //將信號發送給自己

5、alarm pause
unsigned int alarm(unsigned int seconds(秒)); //

    signal(SIGALRM, sigint_pro);  //必須在alarm設置定時器前設置處理函數

    alarm(4);

    printf("before pause\n");
    pause();
    printf("after pause\n");
    alarm(1);

    sleep(1);
    signal(SIGALRM, sigint_pro);  //此時的鬧鐘已經過去了

6、sigaction
//檢查或修改與指定信號相關聯的處理動作 POSIX的信號接口
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);

//信號集
typedef struct {
unsigned long sig[_NSIG_WORDS];
}sigset_t

struct sigaction{
  void (*sa_handler)(int);   //信號捕捉函數
  sigset_t  sa_mask;     //信號集,在調用sa_handler之前,這一信號集要加進進程的信號屏蔽字中。僅當從sa_handler返回時再將進程的信號屏蔽字復位爲原先值。
  int sa_flag;
  void (*sa_sigaction)(int,siginfo_t *,void *);
};

sa_mask;
信號集,在調用sa_ handler之前,這一信號集要加進進程的信號屏蔽字中。僅當從sa_handler返回時再將進程的信 號屏蔽字復位爲原先值。
sa_flag是一個選項,主要理解兩個
SA_INTERRUPT 由此信號中斷的系統調用不會自動重啓
SA_RESTART 由此信號中斷的系統調用會自動重啓
SA_SIGINFO 提供附加信息,一個指向siginfo結構的指針以及一個指向進程上下文標識符的指針

7、sigsetjmp siglongjmp

#include <setjmp.h>  
int setjmp(jmp_buf  env);
void longjmp(jmp_buf  env, int value);
int sigsetjmp(sigjmp_buf env, int savemask); //若直接調用則返回0,若從siglongjmp調用返回則返回非0值。  
void siglongjmp(sigjmp_buf env, int val); 

對sigsetjmp的說明:
sigsetjmp()會保存目前堆棧環境,然後將目前的地址作一個記號,
而在程序其他地方調用siglongjmp()時便會直接跳到這個記號位置,然後還原堆棧,繼續程序的執行
參數env爲用來保存目前堆棧環境,一般聲明爲全局變量
參數savemask若爲非0則代表擱置的信號集合也會一塊保存
當sigsetjmp()返回0時代表已經做好記號上,若返回非0則代表由siglongjmp()跳轉回來。

先看一個setjmp longjmp的實例
下面這個程序是個死循環,setjmp->setjmp_fun->longjmp->setjmp…….

jmp_buf jmpBuffer;
int jmp_ret = 1;

void setjmp_fun()
{
    printf("in the setjmp fun\n");

    longjmp(jmpBuffer,jmp_ret);     //跳轉到setjmp處
}

int main()
{
    int ret = setjmp(jmpBuffer);    //期望longjmp跳轉的地方
    if(ret == jmp_ret)
        printf("return from setjmp_fun\n");
    else if(ret == 0)       //setjmp第一次從自身返回爲0,從longjmp返回爲jmp_ret
        printf("direct return from setjmp\n");

    setjmp_fun();

    return 1;
}

8、sigsuspend 重點P269 10-15 10-16 10-17

守護進程

【實例】

/* 一個daemon程序 */
#include<unistd.h>
#include<sys/types.h>
#include <sys/stat.h>
#define MAXFILE 65535
main()
{
    pid_t pid;
    int i, j = 0;
    pid = fork();
    if (pid < 0)
    {
        printf("error in fork\n");
        exit(1);
    }
    else if (pid > 0)        
        exit(0);   /* 父進程退出 */

     /* 調用setsid,創建新會話 
    *   成爲新會話的首進程,擺脫原會話的控制
    *   成爲新進程組組長,擺脫原進程組的控制
    *   沒有終端控制,擺脫原控制終端的控制
    */
    setsid();   
    /* 切換當前目錄 */
    chdir("/");
    /* 設置文件權限掩碼,不會屏蔽用戶的任何操作 */
    umask(0);
    /* 關閉所有可能打開的不需要的文件 */
    for (i = 0; i < MAXFILE; i++)
        close(i);
    /*
     *到現在爲止,進程已經成爲一個完全的daemon進程,
     *你可以在這裏添加任何你要daemon做的事情,如:
     */
    for (j = 0; j < 5; j++)
        sleep(10);
}

信號量

信號量函數由semget、semop、semctl三個函數組成
【API】
所需頭文件:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

1、semget得到一個信號量集標識符或創建一個信號量集對象

int semget(key_t key, int nsems, int semflg)
//成功:返回信號量集的標識符 出錯:-1,錯誤原因存於error中

key
0(IPC_PRIVATE):會建立新信號量集對象
大於0的32位整數:視參數semflg來確定操作,通常要求此值來源於ftok返回的IPC鍵值
nsems
創建信號量集中信號量的個數,該參數只在創建信號量集時有效
msgflg
0:取信號量集標識符,若不存在則函數會報錯
IPC_CREAT:當semflg&IPC_CREAT爲真時,如果內核中不存在鍵值key相等的信號量集,則新建一個信號量集;如果存在這樣的信號量集,返回此信號量集的標識符
IPC_CREAT|IPC_EXCL:如果內核中不存在鍵值與key相等的信號量集,則新建一個消息隊列;如果存在這樣的信號量集則報錯
error:
EACCESS:沒有權限
EEXIST:信號量集已經存在,無法創建
EIDRM:信號量集已經刪除
ENOENT:信號量集不存在,同時semflg沒有設置IPC_CREAT標誌
ENOMEM:沒有足夠的內存創建新的信號量集
ENOSPC:超出限制
如果用semget創建了一個新的信號量集對象時,則semid_ds結構成員變量的值設置如下:

sem_otime設置爲0。
sem_ctime設置爲當前時間。
msg_qbytes設成系統的限制值。
sem_nsems設置爲nsems參數的數值。
semflg的讀寫權限寫入sem_perm.mode中。
sem_perm結構的uid和cuid成員被設置成當前進程的有效用戶ID,gid和cuid成員被設置成當前進程的有效組ID。

2、semop(完成對信號量的P操作或V操作)

int semop(int semid, struct sembuf *sops, unsigned nsops)
成功:返回信號量集的標識符 出錯:-1,錯誤原因存於error中

nsops:進行操作信號量的個數,即sops結構變量的個數,需大於或等於1。最常見設置此值等於1,只完成對一個信號量的操作
sops:指向進行操作的信號量集結構體數組的首地址,此結構的具體說明如下:

struct sembuf {
    short semnum; /*信號量集合中的信號量編號,0代表第1個信號量*/
    short val;
    /*若val>0進行V操作,信號量值加val,表示進程釋放控制的資源 */
    /*若val<0進行P操作,信號量值減val,若(semval-val)<0(semval爲該信號量值),則調用進程阻塞,直到資源可用;若設置IPC_NOWAIT不會睡眠,進程直接返回EAGAIN錯誤*/
    /*若val==0時阻塞等待,信號量爲0,調用進程進入睡眠狀態,直到信號值爲0;若設置IPC_NOWAIT,進程不會睡眠,直接返回EAGAIN錯誤*/
    short flag;  
    /*0 設置信號量的默認操作*/
    /*IPC_NOWAIT設置信號量操作不等待*/
    /*SEM_UNDO 選項會讓內核記錄一個與調用進程相關的UNDO記錄,如果該進程崩潰,則根據這個進程的UNDO記錄自動恢復相應信號量的計數值*/
  };

error:
E2BIG:一次對信號量個數的操作超過了系統限制
EACCESS:權限不夠
EAGAIN:使用了IPC_NOWAIT,但操作不能繼續進行
EFAULT:sops指向的地址無效
EIDRM:信號量集已經刪除
EINTR:當睡眠時接收到其他信號
EINVAL:信號量集不存在,或者semid無效
ENOMEM:使用了SEM_UNDO,但無足夠的內存創建所需的數據結構
ERANGE:信號量值超出範圍

【P/V操作封裝】

//semnum爲信號量的序號
/***對信號量數組semnum編號的信號量做P操作***/
int P(int semid, int semnum)
{
    struct sembuf sops = {semnum,-1, SEM_UNDO};
    return (semop(semid,&sops,1));
}

/***對信號量數組semnum編號的信號量做V操作***/
int V(int semid, int semnum)
{
    struct sembuf sops = {semnum,+1, SEM_UNDO};
    return (semop(semid,&sops,1));
}

sops爲指向sembuf數組,定義所要進行的操作序列。下面是信號量操作舉例。

struct sembuf sem_get={0,-1,IPC_NOWAIT}; /*將信號量對象中序號爲0的信號量減1*/
struct sembuf sem_get={0,1,IPC_NOWAIT};  /*將信號量對象中序號爲0的信號量加1*/
struct sembuf sem_get={0,0,0};           /*進程被阻塞,直到對應的信號量值爲0*/

flag一般爲0,若flag包含IPC_ NOWAIT,則該操作爲非阻塞操作。若flag包含SEM_ UNDO,則當進程退出的時候會還原該進程的信號量操作,這個標誌在某些情況下是很有用的,比如某進程做了P操作得到資源,但還沒來得及做V操作時就異常退出了,此時,其他進程就只能都阻塞在P操作上,於是造成了死鎖。若採取SEM_UNDO標誌,就可以避免因爲進程異常退出而造成的死鎖

3、semctl (得到一個信號量集標識符或創建一個信號量集對象)

int semctl(int semid, int semnum, int cmd, union semun arg)

semid 信號量集標識符
semnum 信號量集數組上的下標,表示某一個信號量
cmd 參考 信號量函數(semget、semop、semctl)及其範例
arg

union semun {
   short val;             /*SETVAL用的值*/
   struct semid_ds* buf;  /*IPC_STAT、IPC_SET用的semid_ds結構*/
   unsigned short* array; /*SETALL、GETALL用的數組值*/
   struct seminfo *buf;   /*爲控制IPC_INFO提供的緩存*/
} arg;

struct shmid_ds{
    struct ipc_perm shm_perm;  /* 操作權限*/
    int shm_segsz;             /*段的大小(以字節爲單位)*/
    time_t shm_atime;          /*最後一個進程附加到該段的時間*/
    time_t shm_dtime;          /*最後一個進程離開該段的時間*/
    time_t shm_ctime;          /*最後一個進程修改該段的時間*/
    unsigned short shm_cpid;   /*創建該段進程的pid*/
    unsigned short shm_lpid;   /*在該段上操作的最後1個進程的pid*/
    short shm_nattch;          /*當前附加到該段的進程的個數*/
/*下面是私有的*/
    unsigned short shm_npages;  /*段的大小(以頁爲單位)*/
    unsigned long *shm_pages;   /*指向frames->SHMMAX的指針數組*/
    struct vm_area_struct *attaches; /*對共享段的描述*/
};

【實例】

int main(int argc, char **argv)
{
    int key ;
    int semid,ret;
    union semun arg;
    struct sembuf semop;
    int flag ;

    //系統建立IPC通訊(如消息隊列、共享內存時)必須指定一個ID值。該id值通過ftok函數得到
    key = ftok("/tmp", 0x66 ) ; 
    if ( key < 0 )
    {
        perror("ftok key error") ;
        return -1 ;
    }

    /***本程序創建了三個信號量,實際使用時只用了一個0號信號量***/ 
    semid = semget(key,3,IPC_CREAT|0600);
    if (semid == -1)
    {
        perror("create semget error");
        return ;
    }

    if ( argc == 1 )    
    {   
        arg.val = 1;    
        /***對0號信號量設置初始值***/ 
        ret =semctl(semid,0,SETVAL,arg);    
        if (ret < 0 )   
        {   
            perror("ctl sem error");    
            semctl(semid,0,IPC_RMID,arg);   
            return -1 ; 
        }   
    }

    /***取0號信號量的值***/    
    ret =semctl(semid,0,GETVAL,arg);    
    printf("after semctl setval  sem[0].val =[%d]\n",ret);

    system("date") ;

    printf("P operate begin\n") ;       
    flag = P(semid,0)  ;    
    if ( flag ) 
    {   
        perror("P operate error") ; 
        return -1 ; 
    }   
    printf("P operate end\n") ;

    ret =semctl(semid,0,GETVAL,arg);    
    printf("after P sem[0].val=[%d]\n",ret);

    system("date") ;

    if ( argc == 1 )    
    {   
        sleep(120) ;    
    }

    printf("V operate begin\n") ;   
    if (V(semid, 0) < 0)    
    {   
        perror("V operate error") ; 
        return -1 ; 
    }   
    printf("V operate end\n") ;

    ret =semctl(semid,0,GETVAL,arg);    
    printf("after V sem[0].val=%d\n",ret);

    system("date") ;

    if ( argc >1 )  
    {   
        semctl(semid,0,IPC_RMID,arg);   
    }

    return 0 ;
}

多路複用IO 多路I/O就緒通知方法

select/poll/epoll
【API】
epoll只有epoll_create,epoll_ctl,epoll_wait 3個系統調用。

1.創建epoll句柄

 int epoll_create(int size);
 //返回創建的句柄

創建一個epoll的句柄。自從linux2.6.8之後,size參數是被忽略的。需要注意的是,當創建好epoll句柄後,它就是會佔用一個fd值,在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的,所以在使用完epoll後,必須調用close()關閉創建的fd,否則可能導致fd被耗盡。

  1. 事件註冊函數
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

它不同於select()是在監聽事件時告訴內核要監聽什麼類型的事件,而是在這裏先註冊要監聽的事件類型。
epfd: epoll_create()的返回值。
op: 第二個參數表示動作,用三個宏來表示:
EPOLL_CTL_ADD:註冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd;
fd: 需要監聽的fd。
event: 監聽的事件,告訴內核需要監聽什麼事件,struct epoll_event結構如下:

    //感興趣的事件和被觸發的事件  
    struct epoll_event {  
        __uint32_t events; /* Epoll events */  
        epoll_data_t data; /* User data variable */  
    }; 
    typedef union epoll_data {  //聯合體
        void *ptr;  
        int fd;  
        __uint32_t u32;  
        __uint64_t u64;  
    } epoll_data_t;   

events可以是以下幾個宏的集合:
EPOLLIN : 表示對應的文件描述符可以讀(包括對端SOCKET正常關閉);
EPOLLOUT: 表示對應的文件描述符可以寫
EPOLLPRI: 表示對應的文件描述符有緊急的數據可讀(這裏應該表示有帶外數據到來);
EPOLLERR: 表示對應的文件描述符發生錯誤;
EPOLLHUP: 表示對應的文件描述符被掛斷;
EPOLLET: 將EPOLL設爲邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列裏

3、收集監控事件

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//收集在epoll監控的事件中已經發送的事件

epfd: epoll_create()的返回值。
events: 分配好的epoll_event結構體數組epoll將會把發生的事件賦值到events數組中(events不可以是空指針,內核只負責把數據複製到這個events數組中,不會去幫助我們在用戶態中分配內存)
maxevents:告之內核這個events有多大,這個 maxevents的值不能大於創建epoll_create()時的size
timeout: 是超時時間(毫秒,0會立即返回,-1永久阻塞)。如果函數調用成功,返回對應I/O上已準備好的文件描述符數目,如返回0表示已超時。

【實例】
epoll程序框架

for( ; ; )
{
     //查詢所有的網絡接口,看哪一個可以讀,哪一個可以寫
     //events是一個epoll_event*的指針,當epoll_wait這個函數操作成功之後,epoll_events裏面將儲存所有的讀寫事件
     //max_events=20是當前需要監聽的所有socket句柄數
     nfds = epoll_wait(epfd,events,20,500);
     for(i=0;i<nfds;++i)
     {
         if(events[i].data.fd==listenfd) //有新的連接
         {
             connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept這個連接
             ev.data.fd=connfd;
             ev.events=EPOLLIN|EPOLLET;
             epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //將新的fd添加到epoll的監聽隊列中
         }
         else if( events[i].events&EPOLLIN ) //接收到數據,讀socket
         {
             n = read(sockfd, line, MAXLINE)) < 0    //讀
             ev.data.ptr = md;     //md爲自定義類型,添加數據
             ev.events=EPOLLOUT|EPOLLET;
             epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改標識符,等待下一個循環時發送數據,異步處理的精髓
         }
         else if(events[i].events&EPOLLOUT) //有數據待發送,寫socket
         {
             struct myepoll_data* md = (myepoll_data*)events[i].data.ptr;    //取數據
             sockfd = md->fd;
             send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );        //發送數據
             ev.data.fd=sockfd;
             ev.events=EPOLLIN|EPOLLET;
             epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改標識符,等待下一個循環時接收數據
         }
         else
         {
             //其他的處理
         }
     }//for
 }//for

epoll_fd = epoll_create(0);  
if (epoll_fd == -1)  
{  
   perror ("epoll_create");  
   abort ();  
}  

event.data.fd = socket_fd;  
event.events = EPOLLIN | EPOLLET;   //讀入,邊緣觸發方式  

s = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fd, &event);  
if (s == -1)  
{  
  perror ("epoll_ctl");  
  abort ();  
}  

/* Buffer where events are returned */  
event_buffer = calloc (MAXEVENTS, sizeof(event));  
n = epoll_wait (epoll_fd, event_buffer, MAXEVENTS, -1);

epoll工作原理
epoll同樣只告知那些就緒的文件描述符,而且當我們調用epoll_wait()獲得就緒文件描述符時,返回的不是實際的描述符,而是一個代表就緒描述符數量的值,你只需要去epoll指定的一個數組中依次取得相應數量的文件描述符即可,這裏也使用了內存映射(mmap)技術,這樣便徹底省掉了這些文件描述符在系統調用時複製的開銷。

另一個本質的改進在於epoll採用基於事件的就緒通知方式。在select/poll中,進程只有在調用一定的方法後,內核纔對所有監視的文件描述符進行掃描,而epoll事先通過epoll_ctl()來註冊一個文件描述符,一旦基於某個文件描述符就緒時,內核會採用類似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便得到通知。

Epoll的2種工作方式-水平觸發(LT)和邊緣觸發(ET)
假如有這樣一個例子:
1. 我們已經把一個用來從管道中讀取數據的文件句柄(RFD)添加到epoll描述符
2. 這個時候從管道的另一端被寫入了2KB的數據
3. 調用epoll_wait(2),並且它會返回RFD,說明它已經準備好讀取操作
4. 然後我們讀取了1KB的數據
5. 調用epoll_wait(2)……

Edge Triggered 工作模式:
如果我們在第1步將RFD添加到epoll描述符的時候使用了EPOLLET標誌,那麼在第5步調用epoll_wait(2)之後將有可能會掛起,因爲剩餘的數據還存在於文件的輸入緩衝區內,而且數據發出端還在等待一個針對已經發出數據的反饋信息。只有在監視的文件句柄上發生了某個事件的時候 ET 工作模式纔會彙報事件。因此在第5步的時候,調用者可能會放棄等待仍在存在於文件輸入緩衝區內的剩餘數據。在上面的例子中,會有一個事件產生在RFD句柄上,因爲在第2步執行了一個寫操作,然後,事件將會在第3步被銷燬。因爲第4步的讀取操作沒有讀空文件輸入緩衝區內的數據,因此我們在第5步調用 epoll_wait(2)完成後,是否掛起是不確定的。epoll工作在ET模式的時候,必須使用非阻塞套接口,以避免由於一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務餓死。最好以下面的方式調用ET模式的epoll接口,在後面會介紹避免可能的缺陷。
i 基於非阻塞文件句柄
ii 只有當read(2)或者write(2)返回EAGAIN時才需要掛起,等待。但這並不是說每次read()時都需要循環讀,直到讀到產生一個EAGAIN才認爲此次事件處理完成,當read()返回的讀到的數據長度小於請求的數據長度時,就可以確定此時緩衝中已沒有數據了,也就可以認爲此事讀事件已處理完成。

Level Triggered 工作模式
相反的,以LT方式調用epoll接口的時候,它就相當於一個速度比較快的poll(2),並且無論後面的數據是否被使用,因此他們具有同樣的職能。因爲即使使用ET模式的epoll,在收到多個chunk的數據的時候仍然會產生多個事件。調用者可以設定EPOLLONESHOT標誌,在 epoll_wait(2)收到事件後epoll會與事件關聯的文件句柄從epoll描述符中禁止掉。因此當EPOLLONESHOT設定後,使用帶有 EPOLL_CTL_MOD標誌的epoll_ctl(2)處理文件句柄就成爲調用者必須作的事情。

LT(level triggered)是epoll缺省的工作方式,並且同時支持block和no-block socket.在這種做法中,內核告訴你一個文件描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不作任何操作,內核還是會繼續通知你 的,所以,這種模式編程出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表.

ET (edge-triggered)是高速工作方式,只支持no-block socket,它效率要比LT更高。ET與LT的區別在於,當一個新的事件到來時,ET模式下當然可以從epoll_wait調用中獲取到這個事件,可是如果這次沒有把這個事件對應的套接字緩衝區處理完,在這個套接字中沒有新的事件再次到來時,在ET模式下是無法再次從epoll_wait調用中獲取這個事件的。而LT模式正好相反,只要一個事件對應的套接字緩衝區還有數據,就總能從epoll_wait中獲取這個事件。
因此,LT模式下開發基於epoll的應用要簡單些,不太容易出錯。而在ET模式下事件發生時,如果沒有徹底地將緩衝區數據處理完,則會導致緩衝區中的用戶請求得不到響應。

參考:epoll詳解

線程屬性

APUE P314
1、分離線程
線程的分離狀態決定一個線程以什麼樣的方式來終止自己。
線程的默認屬性,一般是非分離狀態,這種情況下,原有的線程等待創建的線程結束。只有當pthread_join()函數返回時,創建的線程纔算終止,才能釋放自己佔用的系統資源。而分離線程沒有被其他的線程所等待,自己運行結束了,線程也就終止了,馬上釋放系統資源。

int pthread_detach(pthread_t thread);
//讓子線程處於分離狀態

2、棧屬性

#include<pthread.h>
int pthread_attr_getstacksize(const pthtead_attr_t *restrict attr,void **restrict stackaddr,size_t *restrict stacksize)
int pthread_attr_setstacksize(const pthread_attr_t  *attr,void *stackaddr,size_t *stacksize)

同步異步阻塞非阻塞IO

【概念】
1. 同步阻塞I/O: 用戶進程進行I/O操作,一直阻塞到I/O操作完成爲止。
2. 同步非阻塞I/O: 用戶程序可以通過設置文件描述符的屬性O_NONBLOCK,I/O操作可以立即返回,但是並不保證I/O操作成功。
3. 異步方式: 當你用異步方式調用一個function的時候,這個方法會馬上返回,事實上多數情況下這種function call只是向某個任務執行體提交一個任務而已。 而你的主thread可以繼續執行其他的事情, 不必等待(阻塞), 而當那個任務執行體執行完你提交的這個任務後,它會通過某種方法callback給你的thread, 告訴你,你的這個任務已經完成。

1.read/write:
對於read操作來說,它是同步的。也就是說只有當申請讀的內容真正存放到buffer中後(user mode的buffer),read函數才返回。在此期間,它會因爲等待IO完成而被阻塞。研究過源碼的朋友應該知道,這個阻塞發生在兩個地方:一是read操作剛剛發起,kernel會檢查其它進程的need_sched標誌,如果有其它進程需要調度,主動阻塞read操作,這個時候其實I/O操作還沒有發起。二是I/O操作發起後,調用lock_page對已經加鎖的頁面申請鎖,這時由於頁面已經加鎖,所以加鎖操作被阻塞,從而read操作阻塞,直到I/O操作完成後頁面被解鎖,read操作繼續執行。所以說read是同步的,其阻塞的原因如上。
對於write操作通常是異步的。因爲linux中有page cache機制,所有的寫操作實際上是把文件對應的page cache中的相應頁設置爲dirty,然後write操作返回。這個時候對文件的修改並沒有真正寫到磁盤上去。所以說write是異步的,這種方式下write不會被阻塞。如果設置了O_SYNC標誌的文件,write操作再返回前會把修改的頁flush到磁盤上去,發起真正的I/O請求,這種模式下會阻塞。
2.Direct I/O
linux支持Direct I/O, 以O_DIRCET標誌打開的文件,在read和write的時候會繞開page cache,直接使用user mode的buffer做爲I/O操作的buffer。這種情況下的read和write**直接發起I/O操作,都是同步的,並會被阻塞**。
3.AIO
目前大多數的linux用的AIO是基於2.4內核中的patch,使用librt庫中的接口。這種方式實現很簡單,就是一個父進程clone出子進程幫其做I/O,完成後通過signal或者callback通知父進程。用戶看來是AIO,實質還是SIO。linux kernel中AIO的實現概念類似,只不過是以一組kernel thread去做的。這些kernel thread做I/O的時候使用的是和Direct I/O相同的方式。
4.mmap()
拋開它中講vm_area和page cache映射在一起的機制不說。真正發起I/O時和read、write使用的是相同的機制,同步阻塞。

同步阻塞 I/O
I/O 密集型進程,所執行的 I/O 操作比執行的處理操作更多。
CPU密集型進程,所執行的處理操作比 I/O 操作更多。
最常用的一個模型是同步阻塞 I/O 模型。在這個模型中,用戶空間的應用程序執行一個系統調用,這會導致應用程序阻塞。這意味着應用程序會一直阻塞,直到系統調用完成爲止(數據傳輸完成或發生錯誤)。調用應用程序處於一種不再消費 CPU而只是簡單等待響應的狀態,因此從處理的角度來看,這是非常有效的。
圖 2 給出了傳統的阻塞 I/O 模型,這也是目前應用程序中最爲常用的一種模型。其行爲非常容易理解,其用法對於典型的應用程序來說都非常有效。
這裏寫圖片描述
在調用 read 系統調用時,應用程序會阻塞並對內核進行上下文切換。然後會觸發讀操作,當響應返回時(從我們正在從中讀取的設備中返回),數據就被移動到用戶空間的緩衝區中。然後應用程序就會解除阻塞(read 調用返回)。
從應用程序的角度來說,read 調用會延續很長時間。實際上,在內核執行讀操作和其他工作時,應用程序的確會被阻塞

同步非阻塞 I/O
同步阻塞 I/O 的一種效率稍低的變種是同步非阻塞 I/O。在這種模型中,設備是以非阻塞的形式打開的。這意味着 I/O 操作不會立即完成,read 操作可能會返回一個錯誤代碼,說明這個命令不能立即滿足(EAGAIN 或 EWOULDBLOCK),如圖 3 所示。
這裏寫圖片描述
非阻塞的實現是 I/O 命令可能並不會立即滿足,需要應用程序調用許多次來等待操作完成(輪詢)。這可能效率不高,因爲在很多情況下,當內核執行這個命令時,應用程序必須要進行忙碌等待,直到數據可用爲止,或者試圖執行其他工作。正如圖 3 所示的一樣,這個方法可以引入 I/O 操作的延時,因爲數據在內核中變爲可用到用戶調用 read 返回數據之間存在一定的間隔,這會導致整體數據吞吐量的降低。

異步阻塞 I/O
另外一個阻塞解決方案是帶有阻塞通知的非阻塞 I/O。在這種模型中,配置非阻塞 I/O,然後使用阻塞 select 系統調用來確定一個 I/O 描述符何時有操作。使 select 調用非常有趣的是它可以用來爲多個描述符提供通知,而不僅僅爲一個描述符提供通知。對於每個提示符來說,我們可以請求這個描述符可以寫數據、有讀數據可用以及是否發生錯誤的通知。
這裏寫圖片描述

異步非阻塞 I/O(AIO)】
異步非阻塞 I/O 模型是一種處理與 I/O 重疊進行的模型。讀請求會立即返回,說明 read 請求已經成功發起了。在後臺完成讀操作時,應用程序然後會執行其他處理操作。當 read 的響應到達時,就會產生一個信號或執行一個基於線程的回調函數來完成這次 I/O 處理過程。
這裏寫圖片描述
在一個進程中爲了執行多個 I/O 請求而對計算操作和 I/O 處理進行重疊處理的能力利用了處理速度與 I/O 速度之間的差異。當一個或多個 I/O 請求掛起時,CPU 可以執行其他任務;或者更爲常見的是,在發起其他 I/O 的同時對已經完成的 I/O 進行操作。

AIO異步非阻塞IO

在異步非阻塞 I/O 中,我們可以同時發起多個傳輸操作。這需要每個傳輸操作都有惟一的上下文,這樣我們才能在它們完成時區分到底是哪個傳輸操作完成了。在 AIO 中,這是一個 aiocb(AIO I/O Control Block)結構。這個結構包含了有關傳輸的所有信息,包括爲數據準備的用戶緩衝區。在產生 I/O (稱爲完成)通知時,aiocb 結構就被用來惟一標識所完成的 I/O 操作。這個 API 的展示顯示瞭如何使用它。
【API】
aio_read 請求異步讀操作
aio_write 請求異步寫操作
aio_error 檢查異步請求的狀態
aio_return 獲得完成的異步請求的返回狀態
aio_suspend 掛起調用進程,直到一個或多個異步請求已經完成(或失敗)
aio_cancel 取消異步 I/O 請求
lio_listio 發起一系列 I/O 操作

aiocb 結構中相關的域

struct aiocb {
  int aio_fildes;               // File Descriptor
  int aio_lio_opcode;           // Valid only for lio_listio (r/w/nop)
  volatile void *aio_buf;       // Data Buffer
  size_t aio_nbytes;            // Number of Bytes in Data Buffer
  struct sigevent aio_sigevent; // Notification Structure I/O操作完成時應該執行的操作

  /* Internal fields */
  ...

};

sigevent 結構告訴 AIO 在 I/O 操作完成時應該執行什麼操作。

1、aio_read

int aio_read( struct aiocb *aiocbp );
//如果執行成功,返回值爲0;如果出現錯誤,返回值爲-1,並設置errno的值

請求對一個有效的文件描述符進行異步讀操作。這個文件描述符可以表示一個文件、套接字甚至管道。
aio_read 函數在請求進行排隊之後會立即返回。

3、aio_return

ssize_t aio_return( struct aiocb *aiocbp );

異步 I/O和標準塊 I/O之間的另外一個區別是我們不能立即訪問這個函數的返回狀態,因爲我們並沒有阻塞在 read 調用上。在標準的 read 調用中,返回狀態是在該函數返回時提供的。但是在異步 I/O 中,我們要使用aio_return函數。
只有在 aio_ error 調用確定請求已經完成(可能成功,也可能發生了錯誤)之後,纔會調用這個函數。aio_return 的返回值就等價於同步情況中 read 或 write 系統調用的返回值(所傳輸的字節數,如果發生錯誤,返回值就爲 -1)。

AIO通知
應用程序需要定義信號處理程序,在產生指定的信號時就會調用這個處理程序。應用程序然後配置一個異步請求將在請求完成時產生一個信號。作爲信號上下文的一部分,特定的 aiocb 請求被提供用來記錄多個可能會出現的請求。

參考:AIO 簡介

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章