<acronym id="s8ci2"><small id="s8ci2"></small></acronym>
<rt id="s8ci2"></rt><rt id="s8ci2"><optgroup id="s8ci2"></optgroup></rt>
<acronym id="s8ci2"></acronym>
<acronym id="s8ci2"><center id="s8ci2"></center></acronym>
0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

深入探索Linux中的C語言

Linux閱碼場 ? 來源:Linux閱碼場 ? 2023-03-14 16:48 ? 次閱讀

本章將深入探索 Linux 中的 C 語言。在本章中,我們將學到更多關于編譯器、從源碼到二進制程序的 4 個步驟、如何使用 Make 工具以及系統調用和 C 標準庫函數的差別的知識。我們也將學習一些 Linux 中的基礎頭文件、C 語言標準以及可移植操作系統(POSIX)標準,C 語言是和 Linux 緊密結合的,掌握 C 語言可以幫你更好地學習 Linux。

本章中,我們將會學習如何開發 Linux C 語言程序和庫,學習如何編寫通用的Makefile 以及為一些重要的項目編寫高級的 Makefile。在學習的過程中,我們也會學到各種 C 語言標準、它們的區別以及對程序有哪些影響。

本章涵蓋以下主題:

使用 GNU 編譯器套件(GCC)鏈接庫

切換 C 標準

使用系統調用

何時不使用它們

獲取 Linux 和類 UNIX 頭文件信息

定義功能測試宏

編譯過程的 4 個步驟

使用 Make 編譯

使用 GCC 選項編寫一個通用的 Makefile

編寫一個簡單的 Makefile

編寫一個更高級的 Makefile

3.1技術要求

在開始學習之前,你需要 GCC 編譯器、Make 工具。

本章中的所有代碼示例都可以從 GitHub 下載

https://github.com/PacktPublishing/Linux-System-Programming-Techniques/tree/master/ch3

3.2使用 GNU 編譯器套件鏈接庫

本節中,你將會學到如何把程序鏈接到一個外部庫—一個安裝在系統層面,另一個安裝在主目錄中。在鏈接庫之前,我們需要創建它,這也是本節中我們要學習的內容。學習如何鏈接庫可以讓你復用庫提供的大量現成函數,無須編寫所有內容,就可以使用庫文件已提供的功能。通常來說,沒有必要重新發明輪子,這可以節約大量的時間。

3.2.1準備工作

在本范例中,你只需要用到 3.1 節中列出的工具。

3.2.2實踐步驟

我們開始學習如何鏈接系統中的共享庫以及主目錄中的庫。我們從已有的標準庫開始:math 庫。

3.2.2.1鏈接到 math 庫我們會編寫一個計算銀行賬戶復利的小程序,會用到 math 庫中的 pow() 函數:1. 把下面的代碼寫入文件,并將其命名為 interest.c。注意,文件頂部包含 math.h,pow() 函數的第一個參數是基數,第二個參數是指數:

#include 
#include 
int main(void)
{
 int years = 15; /* The number of years you will 
 * keep the money in the bank 
 * account */
 int savings = 99000; /* The inital amount */
 float interest = 1.5; /* The interest in % */
 printf("The total savings after %d years " 
 "is %.2f
", years, 
 savings * pow(1+(interest/100), years));
 return 0;
}

2. 編譯并鏈接程序。鏈接庫的選項是 -l,庫的名稱是 m (更多信息請參閱 man 3 pow 手冊頁:

$>gccinterest.c-ointerest-lm

3.運行程序:

$>./interest

The total savingsafter 15 years is 123772.95

3.2.2.2 創建自己的庫

我們開始學習創建自己的共享庫。下一節,我們會鏈接一個進程到這個庫。這個庫的 作用是確定一個數是否為素數。

1. 我們從創建一個簡單的頭文件開始,這個文件只包含一行:函數原型。把以下內容 寫入文件并將其命名為 prime.h:

int isprime(long int number);

2. 現在開始編寫庫文件中的實際函數,把以下代碼寫入文件并將其命名為 prime.c:

int isprime(long int number)
{
long int j;
int prime = 1;
/* Test if the number is divisible, starting
* from 2 */
for(j=2; j

我們需要通過一些手段把其轉換成庫。第一步是把它編譯成一個叫目標文件的對象。我們還需要向編譯器傳遞一些額外的參數使其作為庫運行。更具體一些,我們需要 使其成為位置無關的代碼 ( PIC)。運行以下編譯命令會生成一個叫prime.o 的文 件,使用 ls -l 命令來查看文件,我們將在本章后面學到更多關于目標文件的知識:

$>gcc-Wall-Wextra-pedantic-fPIC -c prime.c

$>ls -lprime.o

-rw-r--r-- 1 jakejake 1296 nov 28 19:18prime.o

4. 現在,我們將目標文件打包成一個庫,在下面的命令中, -shared 參數創建一個共 享庫。-Wl、-soname、libprime .so 參數用于鏈接器。它告訴鏈接器這個共享 庫的名字( soname)叫 libprime .so。-o 參數指定輸出文件名,即 libprime . so。這是動態鏈接庫的標準命名約定,結尾的so 代表共享對象。當庫要在系統范 圍內使用時,通常會添加一個數字來指示版本。在命令的最后,我們加上需要被包 含在共享庫中的目標文件 prime .o:

$>gcc-shared-Wl,-soname,libprime.so-o

>libprime.soprime.o

3.2.2.3 鏈接到主目錄中的庫

有時,你想要鏈接到主目錄(或其他目錄)中的共享庫。它可能是你從網上下載的庫或 者你自己構建的庫。我們將會在本書的后續章節學習更多創建共享庫的知識。這里,我們 使用剛剛創建的 libprime.so 共享庫。

1. 把以下代碼寫入文件并將其命名為is-it-a-prime .c。這個程序將會使用到剛才 創建的共享庫。程序代碼中必須包含剛剛創建的頭文件 prime .h。請注意包含本地 頭文件的不同語法(不是系統級別的頭文件):

#include 
#include 
#include 
#include "prime.h"
int main(int argc, char*argv[])
{
long int num;
/* Only one argument is accepted
*/
if (argc != 2)
{
fprintf(stderr,
"Usage: %s number
",
argv[0]);
return 1;
}
/* Only numbers0-9 are accepted */
if (strspn(argv[1], "0123456789") !=
strlen(argv[1]) )
{
fprintf(stderr, "Only numeric values are"
"accepted
");
return 1;
}
num =atol(argv[1]); /* String to long */
if (isprime(num))
/* Check if num is a prime */
{
printf("%ld
is a prime
", num);
}
else
{
printf("%ld
is not a prime
", num);
}
return 0;
}

2. 編譯并把它鏈接到 libprime .so。由于共享庫在主目錄中,因此需要指定共享庫 的路徑:

$>gcc-L${PWD}is-it-a-prime.c

>-o is-it-a-prime-lprime

3.在運行程序之前,我們需要設置環境變量 $LD_LIBRARY_PATH 為當前目錄(也就

是共享庫所在的目錄)。原因是這個庫是動態鏈接的,它并不在系統庫所在的路徑:

$> export LD_LIBRARY_PATH=${PWD}:${LD_LIBRARY_PATH}

4.運行程序。用一些不同的數字測試一下,看看它們是否為素數:

$>./is-it-a-prime11

11 isaprime

$>./is-it-a-prime13

13 isaprime

$>./is-it-a-prime15

15 is not a prime

$>./is-it-a-prime1000024073

1000024073 is a prime

$>./is-it-a-prime1000024075

1000024075 is not a prime

我們可以通過 ldd 命令查看程序依賴哪些共享庫,如果我們檢查is-it-a-prime 程序,會看到它依賴 libprime .so 庫。當然它也還有其他依賴項,例如 libc.so.6,這是標準的 C 庫:

$>ldd is-it-a-prime

linux-vdso.so.1(0x00007ffc3c9f2000)

libprime.so => /home/jake/libprime.so

(0x00007fd8b1e48000)

libc.so.6=> /lib/x86_64-linux-gnu/libc.so.6

(0x00007fd8b1c4c000)

/lib64/ld-linux-x86-64.so.2 (0x00007fd8b1e54000)

3.2.3 它是如何工作的

在“鏈接到 math 庫”一節中使用的 pow() 函數需要鏈接標準庫中的 math 庫 libm . so。你可以在系統庫路徑中找到這個庫文件,它通常位于 /usr/lib 或 /usr/lib64。對于 Debian 和 Ubuntu 發行版來說,它通常位于 /usr/lib/x86_64-linux-gnu (對于 64 位系統來說)。由于這個文件位于系統默認的庫文件路徑,因此我們可以僅使用 -l 參數 來包含它。math 庫文件的全稱是 libm .so,但是當我們指定庫文件時,只寫了 m (即我們刪除了 lib 和 .so 的擴展名), -l 和 m 之間不應該有空格,所以鏈接時,使用-lm。

我們需要鏈接到 math庫才能使用 pow() 函數的原因是 math庫和標準 C庫 libc .so 是分開的。我們之前使用的函數都是標準庫 libc .so 提供的,這個庫默認就被鏈接,所 以不需要指定,如果想在編譯時顯示指定libc .so 的鏈接,可以執行 gcc -lc some- program.c -o some-program。

pow() 函數接受 2 個參數:x 和 y,例如 pow(x,y)。函數返回 x 的 y 次冪值。比如 pow(2,8)返回 256。返回值類型和參數 x、y 的類型都是 double 浮點數。

計算復利的公式如下所示:

21d3b2f8-c23a-11ed-bfe3-dac502259ad0.png

這里,P是你存入賬戶的起始資本, r 是利率(以百分比表示),y 是資金存在銀行的年數。

鏈接到主目錄中的庫

在 C 程序 is-it-a-prime .c 中,我們需要包含 prime.h 頭文件。頭文件只有一行 isprime() 的函數原型。實際的 isprime()函數在 prime.o 創建的 libprime.so 庫 文件中。 .so 文件叫作共享庫或共享對象文件。共享庫包含已編譯的函數目標文件。我們 將在本章后面介紹什么是目標文件。

當我們鏈接到自己下載或者創建的共享庫時,會比鏈接系統庫麻煩一點,因為它們沒 有安裝在系統庫的默認路徑。

首先,我們需要指定共享庫的名字和路徑,路徑通過-L 參數指定。本章的例子中, 我們指定路徑為當前目錄,也就是我們創建庫文件的地方。我們通過${PWD} 來指定當 前目錄, ${PWD} 是一個 shell 環境變量,它表示當前目錄的絕對路徑。你可以嘗試執行 echo ${PWD} 看看輸出。

現在還不能運行程序, 我們還需要設置另外一個環境變量 $LD_LIBRARY_PATH,把 $LD_LIBRARY_PATH 設置為當前目錄(同時必須包含變量中已有的路徑)。原因是這是 一個動態鏈接庫,庫文件并不包含在程序中,意味著程序運行時需要找到共享庫。環境變 量 $LD_LIBRARY_PATH 的作用,就是告訴程序到哪里找這個庫文件。同時我們也不想覆 蓋 $LD_LIBRARY_PATH 已有的內容,因此設置時需要包含原有內容。如果沒有設置這 個環境變量,在執行程序時會收到一條錯誤消息,即“ error while loading shared libraries:libprime.so ”。當我們使用 ldd 查看程序依賴時,可以看到 libprime .so 位于主目錄中, 而不是系統的路徑。

3.2.4更多

如果你對標準 C 庫有興趣,可以閱讀 libc 的 man 手冊。想了解 pow() 函數,可以 閱讀 man 3 pow。

我也鼓勵你通過 man ldd 閱讀 ldd 的手冊,并使用 ldd 查看進程的依賴,比如在本 節中編寫的interest 進程。你會看到 libm .so 庫及其系統路徑。你也可以嘗試用 ldd查看系統二進制,比如 /bin/ls。

3.3 切換 C標準

在本范例中,我們會學習不同的 C 標準,它們是什么、為什么重要,以及它們如何影 響程序。我們還會學習如何在編譯時選擇 C 標準。

現在幾種最通用的 C 標準是 C89 、C99 和 C11 ( C89 是 1989 年發布的,C11 是 2011 年發布的,以此類推)。很多編譯器仍然默認使用 C89 標準,因為它是兼容性最好,使用 最廣泛,實現最完整的。不過,C99 是一種更加靈活和更加現代化的實現。通常在較新的 Linux 版本里,默認使用 C18 標準以及一些 POSIX 標準。

在本范例中,我們會編寫 2個進程,并分別用 C89 和 C99 編譯, 看看它們的區別。

3.3.1 準備工作

在本范例中你只需要一臺安裝有 Linux 系統的計算機,并且安裝 GCC,最好通過我們 在第 1 章中介紹的軟件包來安裝。

3.3.2 實踐步驟

我們繼續探索不同 C 標準的差異。

1. 把下面的代碼寫入文件,并將其命名為no-return.c。注意,代碼中缺少 return 語句:

#include 
int main(void)
{
printf("Hello, world
");
}

2.用 C89 標準編譯程序:

$> gcc -std=c89 no-return.c -o no-return

3.運行程序并檢查退出碼:

$> ./no-return
Hello, world
$> echo $?
13

4. 仍然使用 C89 標準編譯程序,但是開啟所有類型警告、擴展語法警告,以及pedantic 檢查( -W 表示警告參數, all 表示警告類型,所以用 -Wall),注意 GCC 輸出的錯

誤消息:

$> gcc -Wall -Wextra -pedantic -std=c89 
> no-return.c -o no-return
no-return.c:
In function 'main':
no-return .c:6:1: warning: control reaches end of non-void
function [-Wreturn-type]
}

5. 改用 C99 標準重新編譯程序,并開啟所有類型警告和pedantic 檢查?,F在就不會顯 示錯誤:

$>gcc-Wall-Wextra-pedantic -std=c99

>no-return.c-ono-return

6.重新運行程序,并檢查退出碼??纯春椭暗膮^別。

$>./no-return

Hello, world

$>echo$?

0

7. 把下面的代碼寫入文件,并將其命名為 for-test .c。這個程序在 for 循環內新定 義了一個 i 整型變量,只有 C99 允許這個寫法:

#include 
int main(void)
{
for (int i = 10;
i>0; i--)
{
printf("%d
",
i);
}
return 0;
}

8. 用 C99 標準編譯:

$>gcc-std=c99for-test.c-ofor-test

9.運行這個程序,可以看到它正常工作:

$> ./for-test
10
9
8
7
6
5
4
3
2
1

10. 現在嘗試用 C89 標準編譯。注意 GCC 的報錯明確說明了這個用法只在 C99 或更高 版本被允許。GCC 的報錯都很有用,所以一定要認真看,它可以幫你節約大量時間。

$> gcc -std=c89 for-test.c -o for-test
for-test.c: In function 'main':
for-test .c:5:5: error: 'for' loop initialdeclarations
are only allowed in C99 or C11 mode
for (int i = 10;
i>0; i--)

11. 現在編寫下面的小程序并將其命名為 comments .c。這個程序使用了 C99 注釋(也 稱為 C++ 注釋):

#include 
int main(void)
{
// A C99 comment
printf("hello, world
");
return 0;
}

12.用 C99 編譯程序:

$>gcc -std=c99comments.c-ocomments

13.現在嘗試用 C89 標準編譯程序。注意這里 GCC 的報錯也很有用:

$> gcc -std=c89 comments.c -o comments
comments.c: In function 'main':
comments .c:5:5: error:C++ style comments are not allowed
in ISO C90
// A C99 comment
^
comments .c:5:5: error:
(this will be reported only once
per input file)

3.3.3 它是如何工作的

這只是 C89 和 C99 的一些常見區別,在 Linux 上使用 GCC 還有其他不明顯的差異。我們在 3.3.4節會討論其中的一些不可見差異。

我們通過 GCC 的 -std 參數選擇不同的 C 標準。在本節中,我們測試了2個標準,C89 和 C99。

在第 1~ 6步中,我們看到函數忘記返回值這種情況在編譯時的區別。在C99 中, 由 于未指定其他值,因此假定返回值為 0。但是在 C89 中,忘記返回值是不行的。程序可以 編譯通過,但是程序運行時會返回 13(錯誤碼),這是錯誤的,因為程序中沒有發生錯誤。你測試時返回的實際值可能不同,但錯誤碼始終大于0。當編譯時啟用所有警告、額外警告 和 pedantic 檢查( -Wall -Wextra -pedantic)時,可以看到警告輸出這意味著忘記返 回值是不合法的。所以,在C89 中總是返回一個帶有 return 的值。

在第 7 ~ 10步中,我們看到 C99 可以在 for 循環中聲明一個新變量,這在 C89 中是 不可以的。

在第 11~ 13步中,我們看到一種新的注釋方式:2個斜杠 //。這在 C89 中也是不合法的。

3.3.4更多

除了 C89 和 C99,還有很多的 C 語言標準和方言。例如 C11 、GNU99 ( GNU 的 C99 方言)、 GNU11 ( GNU 的 C11 方言)等,但今天最常用的 C 語言標準是 C89 、C99 和 C11。C18 開始作為某些編譯器和發行版的默認設置。

實際上,C89 和 C99 的差異比我們在這里介紹的更多, 其中一些差異無法使用 Linux 計算機上的 GCC 演示,因為 GCC 已經做了兼容,其他編譯器也是如此。但是也有其他的 一些差異,比如 C89 不支持 long long int 類型,但是C99 是支持的。盡管如此,一些 編譯器(包括 GCC)支持了 C89 使用 long long int 類型,但是在C89 下使用要非常小 心,并非所有編譯器都支持。如果要使用 long long int 類型,最好使用C99 、C11 或 C18。

我們建議你始終使用 -Wall、-Wextra和 -pedantic 選項編譯程序。

3.4 使用系統調用

在任何關于 UNIX 和 Linux 的討論中,系統調用都是一個令人興奮的話題。它是Linux系統編程最底層的部分之一。我們按圖3.1 從上往下看,運行的 shell 和二進制在最上層, 在它的下面是標準C 庫函數,比如 printf()、fgets()、putc() 等。在 C 庫的下面(即 最底層)有系統調用,比如 creat()、write() 等。

21d98fc0-c23a-11ed-bfe3-dac502259ad0.png

圖 3.1 高級函數和底層函數

我在本書中討論的系統調用是指內核提供的C 函數系統調用,而不是實際的系統調用表。我們在這里使用的系統調用是用戶態調用的,但是函數本身是在內核態執行的。

很多標準 C 庫的函數在實現中調用了一個或多個系統調用。putc() 函數是一個很好 的例子,它使用 write() 在屏幕上打印一個字符(這是一個系統調用)。還有一些標準 C 庫函數根本不需要使用系統調用,比如 atoi(),它只需在用戶態執行,不需要內核幫它 執行字符串轉換數字的操作。

一般來說,如果有可用的標準 C 庫函數,我們應該優先使用C 庫函數而不是系統調用。相比較而言,系統調用更難使用并且更加原始。一般來說,把系統調用視為底層接口,把C 庫函數視為高層接口。

但是,在某些情況下,我們必須使用系統調用,或者說在這些場景下使用系統調用更 方便。學習何時以及為什么使用系統調用會讓你成為更好的程序員。比如我們可以通過系 統調用在 Linux 上執行很多文件操作,但是在其他地方是不行的。另一個使用系統調用的 例子是創建進程的時候,詳見后文??傊?,當我們需要對系統進行操作時就需要用到系統 調用。

3.4.1 準備工作

在本節中,我們將使用 Linux 系統調用,所以你需要一臺 Linux 計算機。請注意, sysinfo() 系統調用在 FreeBSD 和 maxOS 下不可用。

3.4.2 實踐步驟

使用 C 庫的函數和使用系統調用實際上沒有太大區別,Linux 中的系統調用在頭文件 unistd .h 中聲明,所以我們在使用系統調用時需要包含這個文件。

1. 在文件中寫入以下代碼并將它命名為 sys-write .c。它用到了 write() 系統調 用。注意,代碼中沒有包含 stdio .h 頭文件,因為不需要 printf() 函數或者任 何標準輸入、標準輸出、標準錯誤文件流。我們直接輸出到1 號文件描述符(標準輸 出)。三個標準文件描述符總是被打開:

#include 
int main(void)
{
write(1,
"hello, world
", 13);
return 0;
}

2. 編譯代碼。為了寫出更好的代碼,從現在開始,我們會始終打開-Wall、-Wextra 和 -pedantic 參數:

$>gcc-Wall-Wextra-pedantic -std=c99

>sys-write.c-o sys-write

3.運行程序:

$> ./sys-write

hello, world

4. 編寫相同的代碼,只是用fputs()函數替代了 write() 函數。注意,我們在這里 包含了 stdio.h,而不是 unistd.h,將程序命名為 write-chars.c:

#include 
int main(void)
{
fputs("hello, world
", stdout);
return 0;
}

5.編譯程序:

$> gcc -Wall -Wextra-pedantic -std=c99

>write-chars .c -o write-chars

6.運行程序:

$> ./write-chars

hello, world

7. 現在,我們編寫一個讀取用戶和系統信息的程序。把程序另存為 my-sys .c。代碼 示例中所有的系統調用都加粗顯示了。這個程序會獲取你的用戶ID、當前工作目 錄、機器總內存和可用隨機存儲內存 (RAM),以及當前的進程 ID (PID ):

#include 
#include 
#include 
#include 
int main(void)
{
char cwd[100] = { 0 }; /* for current dir */
struct sysinfo si;
/* for system information */
aetc叢g(c叢g、 T00):/* get current working dir*/
e入eT對Eo(CeT):/* get system information
* (linux only)
*/
printf("Youruser ID is %d
",aet門Tg());
printf("Your
effective user ID is %d
",
aete門Tg());
printf("Your
current working directory is %s
",
cwd);
printf("Yourmachine has %ld megabytes of "
"total RAM
", si.totalram / 1024  / 1024);
printf("Yourmachine has %ld megabytes of "
"free RAM
", si.freeram / 1024 / 1024);
printf("Currently, there are %d processes "
"running
",si.procs);
printf("Thisprocess ID is %d
",aetbTg());
printf("The
parent process ID is %d

"aetbbTg());
return 0;
}

8.編譯程序:

t> acc -Mg丁丁 -Mext泥g -begg對tTc -etg=coo m入-e入e .c -o /

>m入-e入e

9.運行程序,你會看到用戶信息和機器信息:

t> .m入-e入e
Your user ID is 1000
Your effective user ID is 1000
Your current workingdirectory is /mnt/localnas_disk2/
linux-sys/ch3/code
Your machine has 31033
megabytes of total RAM
Your machine has 6117
megabytes of free RAM
Currently,there are 2496 processes running
This process ID is 30421
The parent processID is 11101

3.4.3 它是如何工作的

在實踐步驟的第 1~ 6步中,我們了解了 write() 和 fputs() 函數之間的區別。區 別可能不那么明顯,但是 write() 系統調用使用了文件描述符而不是文件流。這幾乎適用 于所有的系統調用。文件描述符比文件流更加原始。同樣,自頂而下的方法也適用于文件描述符和文件流。文件流在文件描述符上層,并提供了高級別的接口。但是,有時候我們 也需要直接使用文件描述符,因為它們提供了更多的控制。另外,文件流可以提供更強大和更豐富的輸入和輸出(帶有格式化的輸出,比如, printf())。

在第 7 ~ 9 步中,我們編寫了一個程序來獲取系統信息和用戶信息。在這里包含了三 個特定于系統調用的頭文件:unistd .h、sys/types.h 和 sys/sysinfo.h。

unistd.h 是 UNIX 和 Linux 系統中常見的頭文件。sys/types .h 是系統調用中另 一個常見的頭文件,經常用于從系統取值。這個頭文件包含了特殊的變量類型,比如用于 用戶 ID ( UID)的 uid_t、用于組 ID ( GID)的 gid_t。它們一般是int整型。還有用于 inode 編號的 ino_t、用于 PID的 pid_t 等。

sys/sysinfo .h 頭文件專門用于 sysinfo() 函數,而且這個函數只適用于 Linux 系統調用,所以在其他UNIX 系統(例如 macOS 、Solaris 或 FreeBSD/OpenBSD/NetBSD) 下不起作用。這個頭文件聲明了 sysinfo 結構,我們通過調用 sysinfo() 函數獲取系統 信息。

我們在程序中使用的第一個系統調用是 getcwd(),用于獲取當前工作目錄。函數有 兩個參數:一個表示緩沖區,用來保存路徑;另外一個是緩沖區的長度。

下一個使用的系統調用是只能在Linux 系統下工作的 sysinfo() 函數。這個函數包 含很多信息。當函數執行時,所有的數據都會保存到 sysinfo 數據結構中。包括系統正常運行時間、平均負載、內存總量、可用和已使用內存、總的和可用交換空間、以及正在運 行的進程總數。在 man 2 sysinfo 中,可以找到有關 sysinfo 數據結構中的各種變量 及其數據類型的信息。在示例代碼的下半部分,我們還使用printf() 打印了其中的一些 變量,例如 si .totalram,它表示系統內存的大小。

其余的系統調用都直接從printf() 函數中調用并打印出返回值。

3.4.4更多

在手冊中有很多系統調用的詳細信息。一個好的學習方式是查看 man 2 intro 和 man 2 syscalls。

提示
一般系統調用出錯時都返回 -1,檢查返回值是一個好辦法。

3.5 獲取 Linux 和類 UNIX 頭文件信息

Linux 和其他 UNIX 系統中有很多特定的函數和頭文件,一般來說,它們都是 POSIX 函數,但是只能運行 Linux 上的函數,比如 sysinfo()。我們已經在前面用到了 2 個 POSIX 文件:unistd.h 和 sys/types.h。因為它們是 POSIX標準的,所以適用于所有 類 UNIX 系統,例如 Linux 、FreeBSD 、OpenBSD 、macOS和 Solaris。

在本節中,我們會更多地學習POSIX 頭文件的知識、它們的作用以及如何使用。我們 還會學習如何在手冊中查找這些頭文件的信息。

3.5.1 準備工作

在本范例中,我們要學習在手冊中查找頭文件。如果你使用的是基于Fedora 的系統, 例如 CentOS 、Fedora 或 Red Hat,默認這些手冊頁已經安裝在系統上。如果由于某些原 因它們丟失了,你可以用 root 權限或者 sudo 執行 dnf install man-pages 重新安裝。

如果你使用的是基于 Debian 的系統,例如 Ubuntu 或 Debian,默認是不安裝這些手冊 的,需要按照下面的命令來安裝它們。

Debian

Debian 對于非自由軟件更加嚴格,所以我們需要做一些額外步驟。

1. 以 root 權限打開 /etc/apt/sources .list。

2. 在每行末尾的 main 后面加上 non-free (在 main 和 non-free 之間有一個空格)。

3.保存文件。

4. 以 root 權限執行 apt update。

5. 以 root 權限執行 apt installmanpages-posix-dev 安裝手冊。

Ubuntu

Ubuntu 和其他基于 Ubuntu 的發行版對非自由軟件沒有那么嚴格,所以我們可以直接 安裝對應的軟件包。執行 sudo apt install manpages-posix-dev。

3.5.2 實踐步驟

頭文件非常多,所以重要的是學習哪些頭文件是我們需要的以及如何查找它們的信息。通過閱讀手冊,可以知道如何列出所有的頭文件。接下來我們會介紹這些。

在前面的范例中,我們使用了 sysinfo() 和 getpid() 函數。這里將學習如何找到 系統調用的相關信息以及所需的頭文件。

1.首先,我們閱讀 sysinfo() 的手冊:

$>man 2sysinfo

在 SYNOPSIS 頭文件下面,我們看到下面 2 行:

#include

int sysinfo(structsysinfo *info);

2. 這指我們要包含 sys/sysinfo.h 才能使用 sysinfo() 函數。函數需要一個 sysinfo 的數據結構作為參數。在 DESCRIPTION 中,可以看到 sysinfo 數據結構的組成。

3.查閱 getpid()。這是一個 POSIX 函數,因此有更多的信息:

$>man 2getpid

在 SYNOPSIS 下,需要包含兩個頭文件:sys/types .h 和 unistd .h。另外,該 函數返回一個 pid_t 類型的值。

4.我們繼續學習,打開 sys/types .h 的手冊:

$> mansys_types.h

在 NAME 下,我們看到頭文件包含的數據類型。在 DESCRIPTION 下,可以看到 pid_t 數據類型用于進程 ID 和進程組 ID,但是沒有指明實際的數據類型。所以,讓 我們繼續向下滾動,直到找到一個寫著 Additionally 的副標題。這里寫著 blksize_t、 pid_t 和 ssize_t 應該是有符號整數類型。任務完成,現在我們知道它是一個有 符號整數類型,可以使用 %d 格式化運算符來打印它。

5.我們進一步學習。閱讀 unistd .h 手冊:

$>manunistd.h

6.在手冊中搜索 pid_t,可以找到更多關于它的信息:

輸入一個字符 /,再輸入 pid_t,按回車鍵搜索。按 n 搜索下一個出現單詞的位置。你會發現其他函數也返回 pid_t 類型,如 fork()、getpgrp() 和 getsid() 等。

7. 當你閱讀unistd .h 手冊時,可以看到頭文件中聲明的所有函數;如果找不到,可 以搜索 Declarations。按 /,輸入 Declarations,然后按回車鍵。

3.5.3 它是如何工作的

手冊中 7posix 或 0p 特殊章節取決于你的 Linux 發行版,這部分內容來自 POSIX Programmer’s Manual。比如,當你打開 man unistd .h,可以看到 POSIX Programmer’s Manual,而不像打開 man 2 write 時,你會看到 Linux Programmer’s Manual 。POSIX Programmer’s Manual 來自電氣電子工程師協會( IEEE)和開放組織,而不是來自 GNU 項目或 Linux 社區。

因為 POSIX Programmer’s Manual 不是自由的(就像在開源中一樣),Debian 選擇不把 它放在主軟件源中。這就是我們要添加 non-free庫到 Debian 中的原因。

POSIX 是 IEEE 制定的一組標準。該標準的目的是在所有 POSIX 操作系統(大多數 UNIX 和類 UNIX 系統)中實現一個通用的編程接口。如果你只在程序中使用 POSIX 函數 和 POSIX 頭文件,它將與所有其他UNIX 和類 UNIX 系統兼容。其實際實現可能因系統而 異,但整體功能應該是相同的。

有時,我們需要一些特定信息(比如pid_t 是哪種類型)時,需要閱讀多個手冊。

這里的主要內容是通過函數的手冊找到相應的頭文件,然后根據頭文件的手冊找到更 多信息。

3.5.4更多

POSIX 頭文件手冊是手冊的一個特殊部分,沒有在man man 中列出。在 Fedora 和 CentOS 下,這部分稱為 0p;在 Debian 和 Ubuntu 下,它被稱為 7posix。

提示
你可以通過 apropos . 命令列出指定部分的所有手冊(點表示匹配所有)。
比如,要列出第 2 節中的所有手冊,輸入 apropos -s 2. (包括點,它是命令的一 部分)。要列出 Ubuntu 下 7posix 特殊部分中的所有手冊,輸入 apropos -s 7posix.。

3.6 定義功能測試宏

在本節中,我們將學習一些常見的 POSIX 標準、如何使用它們、為什么要使用它們, 以及如何在功能測試宏中指定它們。

我們已經學習了幾個包含 POSIX 標準以及一些特定 C 標準的示例了。例如,使用 getopt() 時,在源代碼最頂部定義了 _XOPEN_SOURCE 500 (第 2 章中的mph-to- kph_v2.c 示例,它可以使程序更易于編寫腳本)。

功能測試宏控制那些出現在頭文件中的定義。我們可以通過兩種方式來使用,通過功 能測試宏阻止我們使用非標準的定義來構建可移植的應用程序,或者反過來,允許我們使 用非標準的定義。

3.6.1 準備工作

我們將在本范例中編寫 2 個程序:str-posix.c 和 which-c.c。你可以從 https://github.com/PacktPublishing/Linux-System-Programming-Techniques/ tree/master/ch3 下載它們,也可以跟隨下文編寫它們。你還需要我們在第 1章中安裝 的 GCC 編譯器。

3.6.2 實踐步驟

這里,我們將學習功能測試宏、 POSIX 標準、 C 標準,以及其他相關知識的內部原理。

1. 把下面的代碼寫入文件,并將其命名為 str-posix.c。這個程序只簡單地使用 strdup() 復制一個字符串,并打印它。注意,我們在此處需要包含頭文件string.h :

#include 
#include 
int main(void)
{
char a[] = "Hello";
char *b;
b = strdup(a);
printf("b
= %s
", b);
return 0;
}

2.讓我們看看用 C99 標準編譯程序會發生什么。你會看到不止一條錯誤信息:

$> gcc -Wall -Wextra -pedantic -std=c99 
> str-posix.c-o str-posix
str-posix.c: In function 'main':
str-posix .c9: warning: implicit declaration of
function 'strdup'; did you mean 'strcmp'? [-Wimplicit-
function-declaration]
b = strdup(a);
^~~~~~
strcmp
str-posix.c7 assignment to 'char
*' from
'int' makes pointer from integer withouta cast [-Wint-
conversion]
b = strdup(a);

3. 這里產生了一個非常嚴重的警告,不過編譯成功了。如果我們嘗試運行程序,它會 在某些發行版上失敗,但在某些發行版上不會。這就是所謂的未定義行為:

$>./str-posix

Segmentation fault

但是在另一些 Linux 發行版上,我們看到下面的輸出:

$>./str-posix

b = Hello

4. 現在到了最有趣,但同時也令人困惑的部分。這個程序崩潰的原因只有一個,但有

幾個可能的解決方案。我們都會在這里介紹。程序崩潰的原因是strdup()不是 C99 的一部分(我們將在 3.6.3節介紹為什么它有時會生效)。最直接的解決方案是 查看手冊,其中明確指出我們需要將 _XOPEN_SOURCE 功能測試宏設置為 500 或 更高。為了實驗,我們將它設置為 700 (我稍后會解釋原因)。在 str-posix .c 的 最頂部添加以下行,它需要在任何include 語句之前的第一行。否則,它將不起 作用:

#define _XOPEN_SOURCE700

5.現在你已經添加了上述行,我們重新編譯程序:

$>gcc-Wall-Wextra-pedantic -std=c99

>str-posix.c-o str-posix

6. 現在沒有警告了,我們運行程序:

$>./str-posix

b = Hello

7. 這是其中一種最顯而易見的解決方案。接下來刪除文件中的第一行(#define這行)。

8. 刪掉 #define 這行,我們重新編譯程序,但是這次,我們在編譯時設置功能測試 宏。通過 GCC中的 -D 參數設置:

$>gcc-Wall-Wextra-pedantic -std=c99

> -D_XOPEN_SOURCE=700 str-posix.c -o str-posix

9. 再次運行程序:

$>./str-posix

b = Hello

10.這是第二個解決方案。但是當我們用 man feature_test_macros 閱讀功能 測試宏的手冊時,可以看到 _XOPEN_SOURCE 設置成 700或更大的值的效果和 _POSIX_C_SOURCE 設置成 200809L 或更大的值的效果是一樣的。我們現在嘗試 用 _POSIX_C_SOURCE 重新編譯程序:

$>gcc-Wall-Wextra-pedantic -std=c99

> -D_POSIX_C_SOURCE=200809L str-posix.c -o str-posix

11. 這樣也可以工作?,F在,我們用最后一種也是最危險的解決方案。我們不設置任何 C 標準或任何功能測試宏,重新編譯程序:

$>gcc-Wall-Wextra-pedantic str-posix.c

>-ostr-posix

12.沒有警告, 我們運行一下:

$>./str-posix

b = Hello

13. 當我們只定義所有這些宏和標準時,這到底如何工作?好吧,事實證明,當我們不 設置任何 C 標準或功能測試宏時,編譯器有一些默認設置。為了證明這一點,并了

解編譯器的工作原理,我們編寫以下程序。將它命名為 which-c .c。該程序將打 印正在使用的 C 標準和定義的功能測試宏:

#include 
int main(void)
{
#ifdef __STDC_VERSION__
printf("Standard
C version: %ld
",
__STDC_VERSION__);
#endif
#ifdef _XOPEN_SOURCE
printf("XOPEN_SOURCE:
%d
",
_XOPEN_SOURCE);
#endif
#ifdef _POSIX_C_SOURCE
printf("POSIX_C_SOURCE:
%ld
",
_POSIX_C_SOURCE);
#endif
#ifdef _GNU_SOURCE
printf("GNU_SOURCE:
%d
",
_GNU_SOURCE);
#endif
#ifdef _BSD_SOURCE
printf("BSD_SOURCE:
%d
", _BSD_SOURCE);
#endif
#ifdef _DEFAULT_SOURCE
printf("DEFAULT_SOURCE:
%d
",
_DEFAULT_SOURCE);
#endif
return 0;
}

14.我們在不設置任何 C 標準和功能測試宏的情況下編譯并運行程序:

$> gcc -Wall -Wextra -pedantic which-c.c -o which-c
$> ./which-c
Standard C version:
201710
POSIX_C_SOURCE: 200809
DEFAULT_SOURCE: 1

15. 我們指定編譯時使用 C99 標準,然后重新編譯 which .c。這里編譯器會強制執行 嚴格的 C 標準并禁止原來默認會設置的一些功能測試宏:

$> gcc -Wall -Wextra -pedantic -std=c99 
> which-c.c -owhich-c
$> ./which-c
Standard C version:
199901

16.讓我們看看如果設置 _XOPEN_SOURCE 等于 600 會發生什么:

$> gcc -Wall -Wextra -pedantic -std=c99 
> -D_XOPEN_SOURCE=600 which-c.c -o which-c
$> ./which-c
Standard C version:
199901
XOPEN_SOURCE:
600
POSIX_C_SOURCE:
200112

3.6.3 它是如何工作的

在實踐步驟的第 1 ~ 10步中, 我們看到了使用不同的標準和功能測試宏時程序會發生 什么。我們還注意到在沒有指定任何C 標準或功能測試宏的情況下,編譯器也可以出人意 料地正常運行。這是因為 GCC (以及其他編譯器)默認設置了一些功能測試宏和 C 標準。但我們不能依賴這個默認設置。自己指定總是更安全。這樣,我們知道它一定會起作用。

在第 13步中,我們編寫了一個程序來打印編譯時默認設置的功能測試宏。為了防止編 譯器在未設置功能測試宏時產生錯誤,我們將所有printf() 行包裝在 #ifdef 和 #endif 語句中。這些語句是編譯器的if 語句,不是 C程序的 if 語句。例如以下行:

#ifdef _XOPEN_SOURCE
printf("XOPEN_SOURCE:
%d
", _XOPEN_SOURCE);
#endif

如果 _XOPEN_SOURCE 未定義,則編譯的預處理階段后不包含此printf() 行。反之 _XOPEN_SOURCE 被定義,它將被包括在內。我們將在下一范例中介紹什么是預處理。

在第 14步中,我們看到在系統上,編譯器將 _POSIX_C_SOURCE 設置為 200809 時 有效。但是手冊說我們應該將 _XOPEN_SOURCE 設置為 500 或更大。怎么會這樣呢?

如果我們閱讀功能測試宏的手冊( man feature_test_macros),會看到 _XOPEN_ SOURCE 設置成 700 或更大與 _POSIX_C_STANARD 設置為 200809 或更大的效果相同。由 于 GCC 已經默認設置 _POSIX_C_ STANDARD為 200809,所以這和 _XOPEN_SOURCE等于 700 具有相同的效果。

在第 15 步中,我們了解了當指定一個標準,比如 -std=c99 時,編譯器會強制執行 嚴格的 C 標準。這就是 str-posix .c 無法運行(在編譯期間收到警告)的原因。它不是 一個標準的 C 函數,而是 POSIX 函數。這就是為什么我們需要包含 POSIX 標準來使用它。當編譯器使用嚴格的 C 標準時,不會啟用其他功能。當系統中的 C 編譯器支持 C99 時, 這 會使我們編寫的代碼可以移植到所有系統。

在第 16 步中,我們在編譯程序時指定 _XOPEN_SOURCE 等于 600,這樣同時也會將 _POSIX_C_STANDARD 設置為 200112。我們可以在手冊( manfeature_test_macros) 中閱讀相關內容:“ [ 當 ]_XOPEN_SOURCE 定義為大于或等于 500 的值時 [...]以下宏 _POSIX_C_SOURCE 也會被隱式定義 [...]”。

功能宏有什么用呢,它們如何影響代碼 ?

系統頭文件里充滿了#ifdef 語句,功能測試宏是否設置決定了是否啟用和禁用各種 功能和特性。例如,當我們使用 strdup() 函數時,string .h 頭文件有包含在 #ifdef 語句中的strdup() 函數。這些語句檢查是否定義了 _XOPEN_SOURCE 或其他一些 POSIX標準。如果未指定此類標準,則 strdup() 不可見。這就是功能測試宏的工作原理。

但是在第 3 步中,為什么程序在某些發行版上執行會報段錯誤,有些則不會 ? 就像前 文提到的,如果沒有功能測試宏,代碼是沒有 strdup() 的聲明的,發生的事情是不確定 的。由于某些特定的實現細節,它可能會起作用,也可能不起作用。當我們編程時,應該始終避免未定義的行為。某些程序可以在特定的 Linux 發行版上運行,這并不能保證它可 以在其他發行版的計算機上運行。因此,我們應該始終努力按照標準編寫正確的代碼。這樣才能避免未定義的行為。

3.6.4更多

我們定義的所有這些功能測試宏都應該對應于 POSIX 或其他標準。這些標準背后的思 想是在不同的 UNIX 版本和類 UNIX 系統之間創建一個統一的編程接口。

如果你想要深入研究標準和功能測試宏,有一些優秀的手冊可以閱讀。例如:

man 7 feature_test_macros[這里可以閱讀到所有功能測試宏對應的特定標 準,例如 POSIX 、Single Unix Specification 、XPG (X/Open Portability Guide)等 ]

man 7 standards (有關標準的更多信息)

man unistd .h

man 7 libc

man 7 posixoptions

審核編輯:湯梓紅

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • Linux
    +關注

    關注

    87

    文章

    11022

    瀏覽量

    207052
  • C語言
    +關注

    關注

    180

    文章

    7548

    瀏覽量

    131334
  • 源碼
    +關注

    關注

    8

    文章

    586

    瀏覽量

    28682
  • 編譯器
    +關注

    關注

    1

    文章

    1585

    瀏覽量

    48741
  • Makefile
    +關注

    關注

    1

    文章

    124

    瀏覽量

    19112

原文標題:留言送書 | 深入探索 Linux 中的 C 語言(1)

文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    【大語言模型:原理與工程實踐】探索《大語言模型原理與工程實踐》2.0

    《大語言模型“原理與工程實踐”》是關于大語言模型內在機理和應用實踐的一次深入探索。作者不僅深入討論了理論,還提供了豐富的實踐案例,幫助讀者理
    發表于 05-07 10:30

    [推薦]linux下的c語言編程簡介

    第一章本章將簡要介紹一下什么是Linux,C語言的特點,程序開發的預備知識,LinuxC語言
    發表于 04-29 13:50

    Labview 深入探索

    Labview深入探索的很好資料哦
    發表于 04-27 21:29

    LabVIEW_深入探索

    `LabVIEW_深入探索`
    發表于 08-19 13:38

    LabVIEW_深入探索

    LabVIEW_深入探索
    發表于 08-31 13:53

    Labview 深入探索

    Labview 深入探索
    發表于 04-11 18:09

    嵌入式C語言入門與深入

    嵌入式C語言入門與深入
    發表于 04-27 12:23

    LabVIEW 深入探索

    LabVIEW 深入探索
    發表于 07-01 10:54

    嵌入式LinuxC語言高級開發

    體、內存管理。通過本課程的學習,學員的C語言基礎更加扎實、編程能力得到進一步提升知識點一:軟件包管理及shell命令本次課對ubuntu系統的軟件包管理進行了系統的講解,并介紹了shell命令的一些
    發表于 04-03 13:38

    Linux 下學習 C 語言有什么好處?

    很多時候,“學習C語言”指的不是K & R,而是系統編程,。從就業來看,linux的高收入崗位無疑更多。相關書籍《Linux/UNIX系統編程手冊(上、下冊)》或者
    發表于 05-13 12:00

    STM32 單片機C語言課程4-C語言預處理深入剖析1

    本帖最后由 張飛電子學院張角 于 2021-9-13 11:42 編輯 大家上午好!今天為大家講解C語言預處理深入剖析,請持續關注,會持續進行更新!前期回顧:STM32 單片機C
    發表于 09-10 08:31

    STM32 單片機C語言課程5-C語言預處理深入剖析2

    大家上午好!今天為大家講解C語言預處理深入剖析,請持續關注,會持續進行更新!前期回顧:STM32 單片機C語言課程4-
    發表于 09-13 11:40

    使用C語言進行PID算法實現

    前文對PID算法離散化和增量式PID算法原理進行來探索,之后又使用Matlab進行了仿真實驗,對PID三個參數又有了更深入的認識,接下來我們來使用C語言進行PID算法實現,并且結合控制
    發表于 09-15 09:20

    linux系統下C語言開發學習

    本課程是全套課程的第0.2.3課(預科第三課程),主題linux系統下C語言開發學習,總共25小時左右的課程。該視頻是我在聯嵌科技代課期間隨堂真實錄制,***均為根本沒接觸過C
    發表于 12-15 09:10

    linux基本操作與C語言基礎

    目錄C語言基礎C++linux基本操作io操作數據結構進程線程網絡編程實戰項目C語言基礎基本數據類型指針結構體、聯合體、枚舉
    發表于 12-17 07:53
    亚洲欧美日韩精品久久_久久精品AⅤ无码中文_日本中文字幕有码在线播放_亚洲视频高清不卡在线观看
    <acronym id="s8ci2"><small id="s8ci2"></small></acronym>
    <rt id="s8ci2"></rt><rt id="s8ci2"><optgroup id="s8ci2"></optgroup></rt>
    <acronym id="s8ci2"></acronym>
    <acronym id="s8ci2"><center id="s8ci2"></center></acronym>