上一章我們了解了有關進程的基本知識。我們將進程理解為相互獨立的正在運行的程序。由於二者是相互獨立的,就存在交互的可能性,也就是我們所說的進程間通信(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 與此類似,這里不再贅述)。程序有兩個按鈕,一個按鈕用於加載一張圖片,然后將該圖片放在共享內存段;第二個按鈕用於從共享內存段讀取該圖片並顯示出來。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
//!!! Qt5
class QSharedMemory;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
private:
QSharedMemory *sharedMemory;
};
|
頭文件中,我們將MainWindow
添加一個sharedMemory
屬性。這就是我們的共享內存段。接下來得實現文件中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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
看做是指向系統共享內存段的指針,而這個鍵就可以看做指針的名字。多個線程或進程使用同一個共享內存段時,該鍵值必須相同。接下來是兩個按鈕和一個標簽用於界面顯示,這里不再贅述。
下面來看加載圖片按鈕的實現:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
connect(saveButton, &QPushButton::clicked, [=]() {
if (sharedMemory->isAttached()) {
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()));
sharedMemory->unlock();
}
});
|
點擊加載按鈕之后,如果sharedMemory
已經與某個線程或進程連接,則將其斷開(因為我們就要向共享內存段寫入內容了)。然后使用QFileDialog
選擇一張圖片,利用QBuffer
將圖片數據作為char *
格式。在即將寫入共享內存之前,我們需要請求系統創建一個共享內存段(QSharedMemory::create()
函數),創建成功則開始寫入共享內存段。需要注意的是,在讀取或寫入共享內存時,都需要使用QSharedMemory::lock()
函數對共享內存段加鎖。共享內存段就是一段普通內存,所以我們使用 C 語言標准函數memcpy()
復制內存段。不要忘記之前我們對共享內存段加鎖,在最后需要將其解鎖。
接下來是加載按鈕的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
connect(loadButton, &QPushButton::clicked, [=]() {
if (!sharedMemory->attach()) {
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 影音等共享用戶頭像,還是非常有用的。