1. 程式人生 > >linux 多程序程式設計詳解

linux 多程序程式設計詳解

1.建立程序fork()

1.1標頭檔案

#include<unistd.h>  
#include<sys/types.h>   

1.2函式原型

pid_t fork( void);

pid_t 是一個巨集定義,其實質是int 被定義在#include

1.3 返回值:

若成功呼叫一次則返回兩個值,子程序返回0,父程序返回子程序ID;否則,出錯返回-1

1.4 注意以下幾點

  • 在Linux系統中建立程序有兩種方式:一是由作業系統建立,二是由父程序建立程序(通常為子程序)。系統呼叫函式fork()是建立一個新程序的唯一方式,當然vfork()也可以建立程序,但是實際上其還是呼叫了fork()函式。fork()函式是Linux系統中一個比較特殊的函式,其一次呼叫會有兩個返回值。

  • 呼叫fork()之後,父程序與子程序的執行順序是我們無法確定的(即排程程序使用CPU),意識到這一點極為重要,因為在一些設計不好的程式中會導致資源競爭,從而出現不可預知的問題。

  • fork產生子程序的表現就是它會返回2次,一次返回0,順序執行下面的程式碼。這是子程序。一次返回子程序的pid,也順序執行下面的程式碼,這是父程序。

  • 程序建立成功之後,父程序以及子程序都從fork() 之後開始執行,知識pid不同。fork語句可以看成將程式切為A、B兩個部分。(在fork()成功之後,子程序獲取到了父程序的所有變數、環境變數、程式計數器的當前空間和值)。

  • 一般來說,fork()成功之後,父程序與子程序的執行順序是不確定的。這取決於核心所使用的排程演算法,如果要求父子程序相互同步,則要求某種形式的程序間通訊。

1.5 vfork()函式

也用於建立一個程序,返回值與fork()相同。

fork()與vfork()的異同

  • 執行次序:
    fork():對父子程序的排程室由排程器決定的;
    vfork():是先呼叫子程序,等子程序的exit(1)被呼叫後,再呼叫父程序;

  • 對資料段的影響:
    fork():父子程序不共享一段地址空間,修改子程序,父程序的內容並不會受 影響。

    vfork():在子程序呼叫exit之前,它在父程序的空間中執行,也就是說會更改 父程序的資料段、棧和堆。。即共享程式碼區和資料區,且地址和內容都是一 樣的。

2.父程序和子程序異同點

1.相同點

建立子程序之後,子程序拷貝了父程序所有的變數,並且子程序修改變數的值,並不影響父程序的變數值。

2.不同點

fork()的返回值不同。
程序ID不同

3.程式碼示例

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <vector>
#include <iostream>
#include<sys/wait.h>

using namespace std;

// 程序退出函式
void print_exit()  
{  
       printf("the exit pid:[%d] \n",getpid() );  
} 

int main()
{
    string sMatch;
    pid_t pid, child_pid;
    vector<string> provList;
    provList.push_back("taskFace");
    provList.push_back("taskObj");
    provList.push_back("taskAction");
    provList.push_back("taskHat");
    provList.push_back("taskOther");

    cout << "Main process,id=" << getpid() << endl;

    // 迴圈處理"100,200,300,400,500"
    for (vector<string>::iterator it = provList.begin(); it != provList.end(); ++it)
    {
        sMatch = *it;
        atexit( print_exit );
        pid = fork();
        // (*hsq*)子程序退出迴圈,不再建立子程序,全部由主程序建立子程序,這裡是關鍵所在
        if(pid == 0 || pid == -1)
        {
            break;
        }
    }

    if(pid == -1)
    {
        cout<<"Fail to fork!"<<endl;
        exit(1);
    }
    else if(pid == 0)
    {
        // 這裡寫子程序處理邏輯
        cout <<"This is children process,id=" << getpid() << ",start to process " << sMatch << endl;
        sleep(10);
        exit(0);
    }
    else
    {
        // 這裡主程序處理邏輯
        cout << "This is main process,id=" << getpid() <<",end to process "<< sMatch << endl;

        do 
        {   
            // WNOHANG 非阻塞 輪詢 等待帶子程序結束
            child_pid = waitpid(pid, NULL, WNOHANG);  
            if(child_pid != pid)
            {
                printf("---- watpid error!\n");
            }
            printf("I am main progress.The pid progress has not exited!\n");
            sleep(2);

        }while(child_pid == 0);
        exit(0);
    }

    return 0;
}

執行結果

4.父程序子程序從哪裡開始執行的問題

#include <unistd.h> 
#include <sys/types.h>
main () 
{ 
         pid_t pid; 
         printf("hello!\n");  
         pid=fork();
         if (pid < 0) 
                 printf("error in fork!"); 
         else if (pid == 0) 
                 printf("i am the child process, my process id is %d\n ",getpid());
         else 
                 printf("i am the parent process, my process id is %d\n",getpid());
         printf("bye!\n");
} 

這裡可以看出parent process執行了printf(“hello!\n”); 而child process 沒有執行printf(“hello!\n”);
有一個讓人很迷惑的例子:

#include <unistd.h>
#include <sys/types.h>
main () 
{ 
         pid_t pid; 
         printf("fork!");    //printf("fork!\n")
         pid=fork(); 

         if (pid < 0) 
                 printf("error in fork!\n"); 
         else if (pid == 0) 
                 printf("i am the child process, my process id is %d\n",getpid());
         else 
                 printf("i am the parent process, my process id is %d\n",getpid());
} 

此時列印輸出了兩個fork!這不免讓人以為是child process從#include處開始執行,所以也執行了printf(“fork!”); 語句。
其實不然,出現這種問題的原因在於:
這就跟Printf的緩衝機制有關了,printf某些內容時,作業系統僅僅是把該內容放到了stdout的緩衝佇列裡了,並沒有實際的寫到螢幕上 。但是,只要看到有\n, 則會立即重新整理stdout,因此就馬上能夠列印了.mian函式(parent process)運行了printf(“fork!”) 後, “fork!”僅僅被放到了緩衝裡,再執行到fork時,緩衝裡面的 AAAAAA 被子程序(child process)繼承了,因此在子程序度stdout緩衝裡面就也有了”fork!”。所以,你最終看到的會是 “fork!” 被printf了2次!!!! 而mian函式(parent process)執行 printf(“fork!\n”)後,”fork!” 被立即列印到了螢幕上,之後fork到的子程序(child process)裡的stdout緩衝裡不會有”fork!”內容 因此你看到的結果會是”fork!” 被printf了1次!!!!

5.程序間通訊

1.示例程式碼(以訊息佇列為例)

#include <stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
#include<vector>
#include <iostream>
#include<sys/wait.h>

#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>

using namespace std;

typedef struct TASK_LIST_
{
    int taskId;
    const char *taskInfo;
}TASK_LIST;


void print_exit()  
{  
       printf("the exit pid:[%d] \n",getpid() );  
} 


TASK_LIST taskList[5];

#define     SIZE 1024
const   long id = 1;

int main(int argc, char const *argv[])
{
    key_t unique_key;
    int msgid;
    int status;
    char str[SIZE];

    struct msgbuf 
    {
        long msgtype;
        char msgtext[SIZE];

    }sndmsg, rcvmsg;

    TASK_LIST sMatch;
    pid_t pid, child_pid;
    //int status;

    vector<TASK_LIST> provList;
    taskList[0].taskInfo = "1001";
    taskList[1].taskInfo = "2002";
    taskList[2].taskInfo = "3003";
    taskList[3].taskInfo = "4004";
    taskList[4].taskInfo = "5005";
    for (int i = 0; i < 5; i++)
    {
        cout << i << " " << taskList[i].taskInfo << endl;
        taskList[i].taskId = i;

        provList.push_back(taskList[i]);
    }
    cout<<"main process,id="<<getpid()<<endl;

    // 建立一個訊息佇列 
    // IPC_CREAT :建立一個訊息佇列  IPC_EXCL :如果已經存在則報錯
    msgid = msgget(unique_key,IPC_CREAT | IPC_EXCL);



    for (int i = 0; i < provList.size(); i++)
    {
        atexit( print_exit );
        pid = fork();

        // (*hsq*)子程序退出迴圈,不再建立子程序,全部由主程序建立子程序,這裡是關鍵所在
        if (pid == 0 || pid == -1)
        {
            break;
        }
    }

    if(pid == -1)
    {
        cout<<"fail to fork!"<<endl;
        msgctl(msgid, IPC_RMID, 0);
        exit(1);
    }
    else if(pid == 0)
    {
        sleep(5);
        if((status = msgrcv(msgid, (struct msgbuf *)&rcvmsg, sizeof(str) + 1, id, IPC_NOWAIT)) == -1) 
        {
            fprintf(stderr, "msgrcv error!\n");
            exit(4);
        }
        int taskId = atoi(rcvmsg.msgtext);

        if(taskId == 1)
        {
            cout << "start task [" << taskList[1].taskId <<"] :" << taskList[1].taskInfo << endl; 
        }
        printf("The received message is:%s\n", rcvmsg.msgtext);
        // msgctl(msgid, IPC_RMID, 0);   // delete the message queue

        //這裡寫子程序處理邏輯
        cout << "this is children process,id=" << getpid() << endl;

        sleep(2);
        exit(0);
    }
    else
    {   
        cout << "this is main process,pid=" << getpid() << endl;

        sleep(3);

        for (int i = 0; i < 5; ++i)
        {
            sndmsg.msgtype = id;
            char id[4];
            sprintf(id, "%d", i);
            strcpy(str, id);
            sprintf(sndmsg.msgtext, str);
            if(msgsnd(msgid, (struct msgbuf *)&sndmsg, sizeof(str) + 1, 0) == -1) 
            {
                fprintf(stderr, "msgsnd error! \n");
                exit(2);
            }
        }



        // 這裡主程序處理邏輯
        // do 
        // {   
        //     // WNOHANG 非阻塞 輪詢 等你帶子程序結束
        //     child_pid = waitpid(pid, NULL, WNOHANG);  
        //     if(child_pid != pid)
        //     {
        //         printf("---- watpid error!\n");
        //     }
        //     printf("I am main progress.The pid progress has not exited!\n");
        //     sleep(2);

        // }while(child_pid == 0);
        // exit(0);
    }
    return 0;
}

2.介面分析

1.1msgget函式

該函式用來建立和訪問一個訊息佇列

int msgget(key_t, key, int msgflg);

與其他的IPC機制一樣,程式必須提供一個鍵來命名某個特定的訊息佇列。msgflg是一個許可權標誌,表示訊息佇列的訪問許可權,它與檔案的訪問許可權一樣。msgflg可以與IPC_CREAT做或操作,表示當key所命名的訊息佇列不存在時建立一個訊息佇列,如果key所命名的訊息佇列存在時,IPC_CREAT標誌會被忽略,而只返回一個識別符號,它返回一個以key命名的訊息佇列的識別符號(非零整數),失敗時返回-1.

1.2msgsnd函式

該函式用來把訊息新增到訊息佇列中。

int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);

msgid是由msgget函式返回的訊息佇列識別符號。
msg_ptr是一個指向準備傳送訊息的指標,但是訊息的資料結構卻有一定的要求,指標msg_ptr所指向的訊息結構一定要是以一個長整型成員變數開始的結構體,接收函式將用這個成員來確定訊息的型別。所以訊息結構要定義成這樣:

struct my_message
{
    long int message_type;
    /* The data you wish to transfer*/
};

msg_sz是msg_ptr指向的訊息的長度,注意是訊息的長度,而不是整個結構體的長度,也就是說msg_sz是不包括長整型訊息型別成員變數的長度。

msgflg用於控制當前訊息佇列滿或佇列訊息到達系統範圍的限制時將要發生的事情。

如果呼叫成功,訊息資料的一分副本將被放到訊息佇列中,並返回0,失敗時返回-1.

1.3 msgrcv函式

該函式用來從一個訊息佇列獲取訊息,它的原型為
int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg);

msgid, msg_ptr, msg_st的作用也函式msgsnd函式的一樣。

msgtype可以實現一種簡單的接收優先順序。如果msgtype為0,就獲取佇列中的第一個訊息。如果它的值大於零,將獲取具有相同訊息型別的第一個資訊。如果它小於零,就獲取型別等於或小於msgtype的絕對值的第一個訊息。

msgflg用於控制當佇列中沒有相應型別的訊息可以接收時將發生的事情。

呼叫成功時,該函式返回放到接收快取區中的位元組數,訊息被複制到由msg_ptr指向的使用者分配的快取區中,然後刪除訊息佇列中的對應訊息。失敗時返回-1.

1.4msgctl函式

該函式用來控制訊息佇列,它與共享記憶體的shmctl函式相似,它的原型為:
int msgctl(int msgid, int command, struct msgid_ds *buf);

command是將要採取的動作,它可以取3個值,
IPC_STAT:把msgid_ds結構中的資料設定為訊息佇列的當前關聯值,即用訊息佇列的當前關聯值覆蓋msgid_ds的值。
IPC_SET:如果程序有足夠的許可權,就把訊息列隊的當前關聯值設定為msgid_ds結構中給出的值
IPC_RMID:刪除訊息佇列

buf是指向msgid_ds結構的指標,它指向訊息佇列模式和訪問許可權的結構。msgid_ds結構至少包括以下成員:

struct msgid_ds
{
    uid_t shm_perm.uid;
    uid_t shm_perm.gid;
    mode_t shm_perm.mode;
};

成功時返回0,失敗時返回-1.

以上內容是作者在學習中,參考他人經驗,以及《Unix高階環境程式設計》等書籍總結而來,希望可以對正在學習這部分的初學者有所幫助。歡迎交流,共同學習!