QT之程序和程序間通訊(IPC)
程序是作業系統的基礎之一。一個程序可以認為是一個正在執行的程式。我們可以把程序當做計算機執行時的一個基礎單位。關於程序的討論已經超出了本章的範疇,現在我們假定你是瞭解這個概念的。
在 Qt 中,我們使用QProcess
來表示一個程序。這個類可以允許我們的應用程式開啟一個新的外部程式,並且與這個程式進行通訊。下面我們用一個非常簡單的例子開始我們本章有關程序的闡述。
//!!! Qt5 QString program = "C:/Windows/System32/cmd.exe"; QStringList arguments; arguments << "/c" << "dir" << "C:\\"; QProcess *cmdProcess = new QProcess; QObject::connect(cmdProcess, &QProcess::readyRead, [=] () { QTextCodec *codec = QTextCodec::codecForName("GBK"); QString dir = codec->toUnicode(cmdProcess->readAll()); qDebug() << dir; }); cmdProcess->start(program, arguments);
這是一段 Qt5 的程式,並且僅能運行於 Windows 平臺。簡單來說,這段程式通過 Qt 開啟了一個新的程序,這個程序相當於執行了下面的命令:
C:\\Windows\\System32\\cmd.exe /c dir C:\\
注意,我們可以在上面的程式中找到這個命令的每一個字元。事實上,我們可以把一個程序看做執行了一段命令(在 Windows 平臺就是控制檯命令;在 Linux 平臺(包括 Unix)則是執行一個普通的命令,比如 ls)。我們的程式相當於執行了 dir 命令,其引數是 C:\,這是由arguments
陣列決定的(至於為什麼我們需要將 dir 命令作為引數傳遞給 cmd.exe,這是由於 Windows 平臺的規定。在 Windows 中,dir 命令並不是一個獨立的可執行程式,而是通過 cmd.exe 進行解釋;這與 ls 在 Linux 中的地位不同,在 Linux 中,ls 就是一個可執行程式。因此如果你需要在 Linux 中執行 ls,那麼program
上面程式的執行結果類似於:
驅動器 C 中的卷是 SYSTEM 卷的序列號是 EA62-24AB C:\ 的目錄 2013/05/05 20:41 1,024 .rnd 2013/08/22 23:22 <DIR> PerfLogs 2013/10/18 07:32 <DIR> Program Files 2013/10/30 12:36 <DIR> Program Files (x86) 2013/10/31 20:30 12,906 shared.log 2013/10/18 07:33 <DIR> Users 2013/11/06 21:41 <DIR> Windows 2 個檔案 13,930 位元組 5 個目錄 22,723,440,640 可用位元組
上面的輸出會根據不同機器有所不同。是在 Windows 8.1 64位機器上測試的。
為了開啟程序,我們將外部程式名字(program
)和程式啟動引數(arguments
)作為引數傳給QProcess::start()
函式。當然,你也可以使用setProgram()
和setArguments()
進行設定。此時,QProcess
進入Starting
狀態;當程式開始執行之後,QProcess
進入Running
狀態,並且發出started()
訊號。當程序退出時,QProcess
進入NotRunning
狀態(也是初始狀態),並且發出finished()
訊號。finished()
訊號以引數的形式提供程序的退出程式碼和退出狀態。如果傳送錯誤,QProcess
會發出error()
訊號
QProcess
允許你將一個程序當做一個順序訪問的 I/O 裝置。我們可以使用write()
函式將資料提供給程序的標準輸入;使用read()
、readLine()
或者getChar()
函式獲取其標準輸出。由於QProcess
繼承自QIODevice
,因此QProcess
也可以作為QXmlReader
的輸入或者直接使用QNetworkAccessManager
將其生成的資料上傳到網路。
程序通常有兩個預定義的通道:標準輸出通道(stdout)和標準錯誤通道(stderr)。前者就是常規控制檯的輸出,後者則是由程序輸出的錯誤資訊。這兩個通道都是獨立的資料流,我們可以通過使用setReadChannel()
函式來切換這兩個通道。當程序的當前通道可用時,QProcess
會發出readReady()
訊號。當有了新的標準輸出資料時,QProcess
會發出readyReadStandardOutput()
訊號;當有了新的標準錯誤資料時,則會發出readyReadStandardError()
訊號。我們前面的示例程式就是使用了readReady()
訊號。注意,由於我們是執行在 Windows 平臺,Windows 控制檯的預設編碼是 GBK,為了避免出現亂碼,我們必須設定文字的編碼方式。
通道的術語可能會引起誤會。注意,程序的輸出通道對應著QProcess
的 讀 通道,程序的輸入通道對應著QProcess
的 寫 通道。這是因為我們使用QProcess
“讀取”程序的輸出,而我們針對QProcess
的“寫入”則成為程序的輸入。QProcess
還可以合併標準輸出和標準錯誤通道,使用setProcessChannelMode()
函式設定MergedChannels
即可實現。
另外,QProcess
還允許我們使用setEnvironment()
為程序設定環境變數,或者使用setWorkingDirectory()
為程序設定工作目錄。
前面我們所說的訊號槽機制,類似於前面我們介紹的QNetworkAccessManager
,都是非同步的。與QNetworkAccessManager
不同在於,QProcess
提供了同步函式:
waitForStarted()
:阻塞到程序開始;waitForReadyRead()
:阻塞到可以從程序的當前讀通道讀取新的資料;waitForBytesWritten()
:阻塞到資料寫入程序;waitForFinished()
:阻塞到程序結束;
注意,在主執行緒(呼叫了QApplication::exec()
的執行緒)呼叫上面幾個函式會讓介面失去響應。
上面我們瞭解了有關程序的基本知識。我們將程序理解為相互獨立的正在執行的程式。由於二者是相互獨立的,就存在互動的可能性,也就是我們所說的程序間通訊(Inter-Process Communication,IPC)。不過也正因此,我們的一些簡單的互動方式,比如普通的訊號槽機制等,並不適用於程序間的相互通訊。我們說過,程序是作業系統的基本排程單元,因此,程序間互動不可避免與作業系統的實現息息相關。
Qt 提供了四種程序間通訊的方式:
- 使用共享記憶體(shared memory)互動:這是 Qt 提供的一種各個平臺均有支援的程序間互動的方式。
- TCP/IP:其基本思想就是將同一機器上面的兩個程序一個當做伺服器,一個當做客戶端,二者通過網路協議進行互動。除了兩個程序是在同一臺機器上,這種互動方式與普通的 C/S 程式沒有本質區別。Qt 提供了 QNetworkAccessManager 對此進行支援。
- D-Bus:freedesktop 組織開發的一種低開銷、低延遲的 IPC 實現。Qt 提供了 QtDBus 模組,把訊號槽機制擴充套件到程序級別(因此我們前面強調是“普通的”訊號槽機制無法實現 IPC),使得開發者可以在一個程序中發出訊號,由其它程序的槽函式響應訊號。
- QCOP(Qt COmmunication Protocol):QCOP 是 Qt 內部的一種通訊協議,用於不同的客戶端之間在同一地址空間內部或者不同的程序之間的通訊。目前,這種機制只用於 Qt for Embedded Linux 版本。
從上面的介紹中可以看到,通用的 IPC 實現大致只有共享記憶體和 TCP/IP 兩種。後者我們前面已經大致介紹過(應用程式級別的 QNetworkAccessManager 或者更底層的 QTcpSocket 等);本章我們主要介紹前者。
Qt 使用QSharedMemory
類操作共享記憶體段。我們可以把QSharedMemory
看做一種指標,這種指標指向分配出來的一個共享記憶體段。而這個共享記憶體段是由底層的作業系統提供,可以供多個執行緒或程序使用。因此,QSharedMemory
可以看做是專供 Qt 程式訪問這個共享記憶體段的指標。同時,QSharedMemory
還提供了單一執行緒或程序互斥訪問某一記憶體區域的能力。當我們建立了QSharedMemory
例項後,可以使用其create()
函式請求作業系統分配一個共享記憶體段。如果建立成功(函式返回true
),Qt 會自動將系統分配的共享記憶體段連線(attach)到本程序。
前面我們說過,IPC 離不開平臺特性。作為 IPC 的實現之一的共享記憶體也遵循這一原則。有關共享記憶體段,各個平臺的實現也有所不同:
- Windows:
QSharedMemory
不“擁有”共享記憶體段。當使用了共享記憶體段的所有執行緒或程序中的某一個銷燬了QSharedMemory
例項,或者所有的都退出,Windows 核心會自動釋放共享記憶體段。 - Unix:
QSharedMemory
“擁有”共享記憶體段。當最後一個執行緒或程序同共享記憶體分離,並且呼叫了QSharedMemory
的解構函式之後,Unix 核心會將共享記憶體段釋放。注意,這裡與 Windows 不同之處在於,如果使用了共享記憶體段的執行緒或程序沒有呼叫QSharedMemory
的解構函式,程式將會崩潰。 - HP-UX:每個程序只允許連線到一個共享記憶體段。這意味著在 HP-UX 平臺,
QSharedMemory
不應被多個執行緒使用。
下面我們通過一段經典的程式碼來演示共享記憶體的使用。這段程式碼修改自 Qt 自帶示例程式(注意這裡直接使用了 Qt5,Qt4 與此類似,這裡不再贅述)。程式有兩個按鈕,一個按鈕用於載入一張圖片,然後將該圖片放在共享記憶體段;第二個按鈕用於從共享記憶體段讀取該圖片並顯示出來。
const char *KEY_SHARED_MEMORY = "Shared";
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent),
sharedMemory(new QSharedMemory(KEY_SHARED_MEMORY, this))
{
QWidget *mainWidget = new QWidget(this);
QVBoxLayout *mainLayout = new QVBoxLayout(mainWidget);
setCentralWidget(mainWidget);
QPushButton *saveButton = new QPushButton(tr("Save"), this);
mainLayout->addWidget(saveButton);
QLabel *picLabel = new QLabel(this);
mainLayout->addWidget(picLabel);
QPushButton *loadButton = new QPushButton(tr("Load"), this);
mainLayout->addWidget(loadButton);
建構函式初始化列表中我們將sharedMemory
成員變數進行初始化。注意我們給出一個鍵(Key),前面說過,我們可以把QSharedMemory
看做是指向系統共享記憶體段的指標,而這個鍵就可以看做指標的名字。多個執行緒或程序使用同一個共享記憶體段時,該鍵值必須相同。接下來是兩個按鈕和一個標籤用於介面顯示,這裡不再贅述。
connect(saveButton, &QPushButton::clicked, [=]()
{
if (sharedMemory->attach())//判斷程序是否載入到共享記憶體段
{
sharedMemory->detach();//執行緒分離,共享記憶體段由系統進行自動釋放
}
QString filename = QFileDialog::getOpenFileName(this);
QPixmap pixmap(filename);
// picLabel->setPixmap(pixmap);
QBuffer buffer;
QDataStream out(&buffer);
buffer.open(QBuffer::ReadWrite);
out << pixmap;
int size = buffer.size();
if (!sharedMemory->create(size))
{
qDebug() << tr("Create Error: ") << sharedMemory->errorString();
}
else
{
sharedMemory->lock();
char *to = static_cast<char *>(sharedMemory->data());
const char *from = buffer.data().constData();
memcpy(to, from, qMin(size, sharedMemory->size()));//把buffer裡的資料匯入共享記憶體
sharedMemory->unlock();
}
});
點選載入按鈕之後,如果sharedMemory
已經與某個執行緒或程序連線,則將其斷開(因為我們就要向共享記憶體段寫入內容了)。然後使用QFileDialog
選擇一張圖片,利用QBuffer
將圖片資料作為char *
格式。在即將寫入共享記憶體之前,我們需要請求系統建立一個共享記憶體段(QSharedMemory::create()
函式),建立成功則開始寫入共享記憶體段。需要注意的是,在讀取或寫入共享記憶體時,都需要使用QSharedMemory::lock()
函式對共享記憶體段加鎖。共享記憶體段就是一段普通記憶體,所以我們使用 C 語言標準函式memcpy()
複製記憶體段。不要忘記之前我們對共享記憶體段加鎖,在最後需要將其解鎖。
connect(loadButton, &QPushButton::clicked, [=]()
{
if (!sharedMemory->isAttached())
{
if(sharedMemory->error()!=QSharedMemory::NoError)
{
qDebug() << tr("Attach Error: ") << sharedMemory->errorString();
}
}
else
{
QBuffer buffer;
QDataStream in(&buffer);
QPixmap pixmap;
sharedMemory->lock();
buffer.setData(static_cast<const char *>(sharedMemory->constData()), sharedMemory->size());//把共享記憶體中資料匯出到緩衝區
buffer.open(QBuffer::ReadWrite);
in >> pixmap;
sharedMemory->unlock();
sharedMemory->detach();
picLabel->setPixmap(pixmap);
}
});
如果共享記憶體段已經連線,還是用QBuffer
讀取二進位制資料,然後生成圖片。注意我們在操作共享記憶體段時還是要先加鎖再解鎖。最後在讀取完畢後,將共享記憶體段斷開連線。
注意,如果某個共享記憶體段不是由 Qt 建立的,我們也是可以在 Qt 應用程式中使用。不過這種情況下我們必須使用QSharedMemory::setNativeKey()
來設定共享記憶體段。使用原始鍵(native key)時,QSharedMemory::lock()
函式就會失效,我們必須自己保護共享記憶體段不會在多執行緒或程序訪問時出現問題。
IPC 使用共享記憶體通訊是一個很常用的開發方法。多個程序間得通訊要比多執行緒間得通訊少一些,不過在某一族的應用情形下,比如 QQ 與 QQ 音樂、QQ 影音等共享使用者頭像,還是非常有用的。
執行結果如下
原始碼路徑 這裡