1、實驗說明javascript
1. 環境登陸html
無需密碼自動登陸,系統用戶名shiyanlou,密碼shiyanlou 若不當心登出後,直接刷新頁面便可前端
2. 環境使用java
完成實驗後能夠點擊桌面上方的「實驗截圖」保存並分享實驗結果到微博,向好友展現本身的學習進度。實驗樓提供後臺系統截圖,能夠真實有效證實您已經完成了實驗。 實驗記錄頁面能夠在「個人主頁」中查看,其中含有每次實驗的截圖及筆記,以及每次實驗的有效學習時間(指的是在實驗桌面內操做的時間,若是沒有操做,系統會記錄爲發呆時間)。這些都是您學習的真實性證實。node
3. 課程來源linux
VIM 在線手冊git
2、vim模式介紹程序員
如下介紹內容來自維基百科Vimgithub
從vi演生出來的Vim具備多種模式,這種獨特的設計容易使初學者產生混淆。幾乎全部的編輯器都會有插入和執行命令兩種模式,而且大多數的編輯器使用了與Vim大相徑庭的方式:命令目錄(鼠標或者鍵盤驅動),組合鍵(一般經過control鍵(CTRL)和alt鍵(ALT)組成)或者鼠標輸入。Vim和vi同樣,僅僅經過鍵盤來在這些模式之中切換。這就使得Vim能夠不用進行菜單或者鼠標操做,而且最小化組合鍵的操做。對文字錄入員或者程序員能夠大大加強速度和效率。正則表達式
Vim具備6種基本模式和5種派生模式,咱們這裏只簡單介紹下6種基本模式:
在普通模式中,用的編輯器命令,好比移動光標,刪除文本等等。這也是Vim啓動後的默認模式。這正好和許多新用戶期待的操做方式相反(大多數編輯器默認模式爲插入模式)。
Vim強大的編輯能來自於其普通模式命令。普通模式命令每每須要一個操做符結尾。例如普通模式命令dd刪除當前行,可是第一個"d"的後面能夠跟另外的移動命令來代替第二個d,好比用移動到下一行的"j"鍵就能夠刪除當前行和下一行。另外還能夠指定命令重複次數,2dd(重複dd兩次),和dj的效果是同樣的。用戶學習了各類各樣的文本間移動/跳轉的命令和其餘的普通模式的編輯命令,而且可以靈活組合使用的話,可以比那些沒有模式的編輯器更加高效的進行文本編輯。
在普通模式中,有不少方法能夠進入插入模式。比較普通的方式是按a(append/追加)鍵或者i(insert/插入)鍵。
在這個模式中,大多數按鍵都會向文本緩衝中插入文本。大多數新用戶但願文本編輯器編輯過程當中一直保持這個模式。
在插入模式中,能夠按ESC鍵回到普通模式。
這個模式與普通模式比較類似。可是移動命令會擴大高亮的文本區域。高亮區域能夠是字符、行或者是一塊文本。當執行一個非移動命令時,命令會被執行到這塊高亮的區域上。Vim的"文本對象"也能和移動命令同樣用在這個模式中。
這個模式和無模式編輯器的行爲比較類似(Windows標準文本控件的方式)。這個模式中,能夠用鼠標或者光標鍵高亮選擇文本,不過輸入任何字符的話,Vim會用這個字符替換選擇的高亮文本塊,而且自動進入插入模式。
在命令行模式中能夠輸入會被解釋成並執行的文本。例如執行命令(:鍵),搜索(/和?鍵)或者過濾命令(!鍵)。在命令執行以後,Vim返回到命令行模式以前的模式,一般是普通模式。
這和命令行模式比較類似,在使用:visual命令離開Ex模式前,能夠一次執行多條命令。
這其中咱們經常使用到就是普通模式、插入模式和命令行模式,本課程也只涉及這三個經常使用模式的內容
2.三種經常使用模式的切換
vim啓動進入普通模式,處於插入模式或命令行模式時只須要按Esc或者Ctrl+[(這在vim課程環境中無論用)便可進入普通模式。普通模式中按i(插入)或a(附加)鍵均可以進入插入模式,普通模式中按:進入命令行模式。命令行模式中輸入wq回車後保存並退出vim。
3、進入vim
1.使用vim命令進入vim界面
vim後面加上你要打開的已存在的文件名或者不存在(則做爲新建文件)的文件名。 打開Xfce終端,輸入如下命令
$ vim practice_1.txt
直接使用vim也能夠打開vim編輯器,可是不會打開任何文件。
$ vim
進入命令行模式後輸入:e 文件路徑 一樣能夠打開相應文件。
2.遊標移動
在進入vim後,按下i鍵進入插入模式。在該模式下您能夠輸入文本信息,下面請輸入以下三行信息:
12345678
abcdefghijk
shiyanlou.com
按Esc進入普通模式,在該模式下使用方向鍵或者h,j,k,l鍵能夠移動遊標。
按鍵 |
說明 |
h |
左 |
l |
右(小寫L) |
j |
下 |
k |
上 |
w |
移動到下一個單詞 |
b |
移動到上一個單詞 |
請嘗試在普通模式下使用方向鍵移動光標到shiyanlou這幾個字母上面。
4、進入插入模式
1.進入插入模式
在普通模式下使用下面的鍵將進入插入模式,並能夠從相應的位置開始輸入
命令 |
說明 |
i |
在當前光標處進行編輯 |
I |
在行首插入 |
A |
在行末插入 |
a |
在光標後插入編輯 |
o |
在當前行後插入一個新行 |
O |
在當前行前插入一個新行 |
cw |
替換從光標所在位置後到一個單詞結尾的字符 |
請嘗試不一樣的從普通模式進入插入模式的方法,在最後一行shiyanlou前面加上www.,注意每次要先回到普通模式才能切換成以不一樣的方式進入插入模式
5、保存文檔
1.命令行模式下保存文檔
從普通模式輸入:進入命令行模式,輸入w回車,保存文檔。輸入:w 文件名能夠將文檔另存爲其餘文件名或存到其它路徑下
6、退出vim
1.命令行模式下退出vim
從普通模式輸入:進入命令行模式,輸入wq回車,保存並退出編輯
如下爲其它幾種退出方式:
命令 |
說明 |
:q! |
強制退出,不保存 |
:q |
退出 |
:wq! |
強制保存並退出 |
:w <文件路徑> |
另存爲 |
:saveas 文件路徑 |
另存爲 |
:x |
保存並退出 |
:wq |
保存並退出 |
2.普通模式下退出vim
普通模式下輸入Shift+zz便可保存退出vim
7、刪除文本
1.普通模式下刪除vim文本信息
進入普通模式,使用下列命令能夠進行文本快速刪除:
命令 |
說明 |
x |
刪除遊標所在的字符 |
X |
刪除遊標所在前一個字符 |
Delete |
同x |
dd |
刪除整行 |
dw |
刪除一個單詞(不適用中文) |
d$或D |
刪除至行尾 |
d^ |
刪除至行首 |
dG |
刪除到文檔結尾處 |
d1G |
刪至文檔首部 |
除此以外,你還能夠在命令以前加上數字,表示一次刪除多行,如:
2dd表示一次刪除2行
咱們來作以下練習:
$ cp /etc/protocols .
$ vim protocols
在普通模式下.
(小數點)表示重複上一次的命令操做
拷貝測試文件到本地目錄
$ cp /etc/protocols .
打開文件進行編輯
$ vim protocols
普通模式下輸入x
,刪除第一個字符,輸入.
(小數點)會再次刪除一個字符,除此以外也能夠重複dd
的刪除操做
進入普通模式輸入N<command>
,N表示重複後面的次數,下面來練習如下:
打開文件文件進行編輯
$ vim protocols
下面你能夠依次進行以下操做練習:
10x
,刪除10個連續字符3dd
,將會刪除3行文本在普通模式下,你還能夠使用dw
或者daw
(delete a word)刪除一個單詞,因此你能夠很容易的聯想到dnw
(n替換爲相應數字)表示刪除n個單詞
普通模式下,下列命令可讓光標快速調轉到指定位置,咱們分別討論快速實現行間跳轉和行內跳轉
命令 |
說明 |
|
遊標移動到第 n 行(若是默認沒有顯示行號,請先進入命令模式,輸入 |
|
遊標移動到到第一行 |
|
到最後一行 |
仍是來練習一下吧:
使用vim打開練習文檔
$ vim protocols
依次進行以下操做練習:
小技巧:你在完成依次跳轉後,能夠使用Ctrl+o
快速回到上一次(跳轉前)光標所在位置,這個技巧很實用,好比當你在寫代碼時,突然想起有個bug,須要修改,這時候你跳過去改好了,只須要按下Ctrl+o
就能夠回到你以前的位置。vim中會用不少相似的小技巧就等着你去發掘。
普通模式下使用下列命令在行內按照單詞爲單位進行跳轉
命令 |
說明 |
|
到下一個單詞的開頭 |
|
到下一個單詞的結尾 |
|
到前一個單詞的開頭 |
|
到前一個單詞的結尾 |
|
到行頭 |
|
到行尾 |
|
向後搜索<字母>並跳轉到第一個匹配的位置(很是實用) |
|
向前搜索<字母>並跳轉到第一個匹配的位置 |
|
向後搜索<字母>並跳轉到第一個匹配位置以前的一個字母(不經常使用) |
|
向前搜索<字母>並跳轉到第一個匹配位置以後的一個字母(不經常使用) |
依次進行以下操做練習:
w
跳轉到一個單詞的開頭,而後使用dw
刪除這個單詞e
跳轉到一個單詞的結尾,並使用~
將遊標所在字母變成大寫或小寫
y
複製yy
複製遊標所在的整行(3yy
表示複製3行)y^
複製至行首,或y0
。不含光標所在處字符。y$
複製至行尾。含光所在處字符。yw
複製一個單詞。y2w
複製兩個單詞。yG
複製至文本末。y1G
複製至文本開頭。p
粘貼p
(小寫)表明粘貼至光標後(下)P
(大寫)表明粘貼至光標前(上)打開文件進入普通模式練習上述命令,這會兒你就能夠隨意yy
了,一 一+
$ vim protocols
其實前面講得dd
刪除命令就是剪切,你每次dd
刪除文檔內容後,即可以使用p
來粘貼,也這一點可讓咱們實現一個很爽快的功能——交換上下行:
ddp
,就這麼簡單,即實現了快速交換光標所在行與它下面的行
這一小節你應該掌握了幾個常用到的操做,包括快速行間移動和快速行內移動,以及剪切和複製粘貼等操做,但願你可以多加練習熟練掌握,一旦當你熟練了這些操做將會極大地提升你的工做效率。
編輯多個文件有兩種形式,一種是在進入vim前使用的參數就是多個文件。另外一種就是進入vim後再編輯其餘的文件。 同時建立兩個新文件並編輯
$ 12vim.txt.txt
默認進入1.txt
文件的編輯界面
:n
編輯2.txt文件,能夠加!
即:n!
強制切換,以前一個文件的輸入沒有保存,僅僅切換到另外一個文件:N
編輯1.txt文件,能夠加!
即:N!
強制切換,以前文件內的輸入沒有保存,僅僅是切換到另外一個文件:e 3.txt
打開新文件3.txt:e#
回到前一個文件:ls
能夠列出之前編輯過的文檔:b 2.txt
(或者編號)能夠直接進入文件2.txt編輯:bd 2.txt
(或者編號)能夠刪除之前編輯過的列表中的文件項目:e! 4.txt
,新打開文件4.txt,放棄正在編輯的文件:f
顯示正在編輯的文件名:f new.txt
,改變正在編輯的文件名字爲new.txt若是由於斷電等緣由形成文檔沒有保存,能夠採用恢復方式,vim -r
進入文檔後,輸入:ewcover 1.txt
來恢復
$ 1vim -r.txt
v
(小寫),進入字符選擇模式,就能夠移動光標,光標走過的地方就會選取。再次按下v會後就會取消選取。Shift+v
(小寫),進入行選擇模式,按下V以後就會把整行選取,您能夠上下移動光標選更多的行,一樣,再按一次Shift+v
就能夠取消選取。Ctrl+v
(小寫),這是區域選擇模式,能夠進行矩形區域選擇,再按一次Ctrl+v
取消選取。d
刪除選取區域內容y
複製選取區域內容拷貝練習文件到當前目錄
$ cp /etc/protocols .
打開練習文件
$ vim protocols
9G
跳轉到第9行,輸入Shift+v
(小寫V),進入可視模式進行行選擇,選中5行,按下>>
縮進,將5行總體縮進一個shiftwidth
Ctrl+v
(小寫v),進入可視模式進行矩形區域選擇,選中第一列字符而後x
刪除整列
vim能夠在一個界面裏打開多個窗口進行編輯,這些編輯窗口稱爲vim的視窗。 打開方法有不少種,例如能夠使用在命令行模式下輸入:new
打開一個新的vim視窗,並進入視窗編輯一個新文件(普通模式下輸入Ctrl+w
也能夠,可是Ctrl+w
在chrome下會與chrome關閉標籤頁的快捷鍵衝突,因此使用該快捷鍵你能夠在IE或其它瀏覽器進行練習),除了:new
命令,下述列舉的多種方法也能夠在命令模式或普通模式下打開新的視窗:
:sp 1.txt
打開新的橫向視窗來編輯1.txt:vsp 2.txt
打開新的縱向視窗來編輯1.txtCtrl-w s
將當前窗口分割成兩個水平的窗口Ctrl-w v
將當前窗口分割成兩個垂直的窗口Ctrl-w q
即 :q 結束分割出來的視窗。若是在新視窗中有輸入須要使用強制符!即:q!Ctrl-w o
打開一個視窗而且隱藏以前的全部視窗Ctrl-w j
移至下面視窗Ctrl-w k
移至上面視窗Ctrl-w h
移至左邊視窗Ctrl-w l
移至右邊視窗Ctrl-w J
將當前視窗移至下面Ctrl-w K
將當前視窗移至上面Ctrl-w H
將當前視窗移至左邊Ctrl-w L
將當前視窗移至右邊Ctrl-w -
減少視窗的高度Ctrl-w +
增長視窗的高度打開練習文件
$ 1vim.txt
:new
打開一個新的vim視窗:sp 2.txt
打開新的橫向視窗來編輯2.txt:vsp 3.txt
打開新的橫向視窗來編輯3.txtCtrl+w
進行視窗間的跳轉:q!
退出多視窗編輯
$ vim -x file1
輸入您的密碼 確認密碼 這樣在下一次打開時,vim就會要求你輸入密碼
在命令行模式中輸入!
能夠執行外部的shell命令
:!ls
用於顯示當前目錄的內容:!rm FILENAME
用於刪除名爲 FILENAME 的文件:w FILENAME
可將當前 VIM 中正在編輯的文件另存爲 FILENAME 文件F1
打開vim
本身預設的幫助文檔:h shiftwidth
打開名爲shiftwidth
的幫助文件:ver
顯示版本及參數能夠在編輯文件的時候進行功能設定,如命令行模式下輸入:set nu
(顯示行數),設定值退出vim後不會保存。要永久保存配置須要修改vim配置文件。 vim的配置文件~/.vimrc
,能夠打開文件進行修改,不過務必當心不要影響vim正常使用
:set
或者:se
顯示全部修改過的配置:set all
顯示全部的設定值:set option?
顯示option的設定值:set nooption
取消當期設定值:set autoindent(ai)
設置自動縮進:set autowrite(aw)
設置自動存檔,默認未打開:set background=dark
或light
,設置背景風格:set backup(bk)
設置自動備份,默認未打開: set cindent(cin)
設置C語言風格縮進更多詳細參數請參考vim手冊
經過這四章的簡單學習,相應你應該掌握了vim的基本操做和使用,但本課程的主要目的是爲了讓你在學習實驗樓上面其餘須要用到vim的課程中不會有任何問題。若是你想單純的學習並熟練掌握vim編輯器,經過各種教程包括本課程的學習是不可以知足的,由於要熟練掌握是跟你我的的選擇有關,這須要你不斷的聯繫並堅持長期使用vim完成各類編輯操做才能達到,同時你還須要掌握如何更改和編寫vim的配置文件及安裝各種vim插件來實現各類強大的功能知足你的各類苛刻的需求,最後但願你在實驗樓玩得愉快
1 Linux命令
若是使用GUI,Linux和Windows沒有什麼區別。Linux學習應用的一個特色是經過命令行進行使用。
登陸Linux後,咱們就能夠在#或$符後面去輸入命令,有的時候命令後面還會跟着選項(options)或參數(arguments)。即Linux中命令格式爲:
command [options] [arguments] //中括號表明是可選的,即有些命令不須要選項也不須要參數
選項是調整命令執行行爲的開關,選項不一樣決定了命令的顯示結果不一樣。
參數是指命令的做用對象。
如ls命令,ls或ls .顯示是當前目錄的內容,這裏「.」就是參數,表示當前目錄,是缺省的能夠省略。咱們能夠用ls -a .顯示當前目錄中的全部內容,包括隱藏文件和目錄。其中「-a」 就是選項,改變了顯示的內容,以下圖所示:
以上簡要說明了選項及參數的區別,但具體Linux中哪條命令有哪些選項及參數,須要咱們靠經驗積累或者查看Linux的幫助了。
2 man命令
不論學習編程仍是Linux命令,掌握幫助文檔的使用都是很重要的,是觸類旁通的重要途徑。 man是manul的縮寫,咱們能夠經過man man來查看man的幫助,以下圖:
幫助文檔包含:
1 Executable programs or shell commands(用戶命令幫助)
2 System calls (系統調用幫助)
3 Library calls (庫函數調用幫助)
4 Special files (usually found in /dev)
5 File formats and conventions eg /etc/passwd(配置文件幫助)
6 Games
7 Miscellaneous (including macro packages and conventions), e.g. man(7), groff(7)
8 System administration commands (usually only for root)
9 Kernel routines [Non standard]
解釋一下:
1是普通的Linux命令
2是系統調用,操做系統的提供的服務接口
3是庫函數, C語言中的函數
5是指文件的格式,好比passwd, 就會說明這個文件中各個字段的含義
6是給遊戲留的,由各個遊戲本身定義
7是附件還有一些變量,好比向environ這種全局變量在這裏就有說明
8是系統管理用的命令,這些命令只能由root使用,如ifconfig
其中1,2,3是咱們學習的重點,區別你們練習一下就知道了,好比printf是C語言的庫函數,也是一個Linux命令,你們嘗試一下man printf,man 1 printf,man 3 printf,體會一下區別。
知道printf 命令也好,printf函數也好,查找幫助文檔都很容易。man有一個-k 選項用起來很是好,這個選項讓你學習命令,編程時有了一個搜索引擎,能夠觸類旁通。 咱們經過一個例子來講明,好比數據結構中學過排序(sort),我不知道C語言中有沒有完成這個功能的函數,能夠經過「man -k sort」來搜索,由於是找C庫函數,咱們關注帶3的,qsort好像是個好選項,以下圖:
結合後面學習的grep 命令和管道,能夠多關鍵字查找:
man -k key1 | grep key2 | grep key3 | ...
以下圖,能夠更好的找到qsort:
3 cheat 命令
man 雖然很重要,但有些命令看了幫助還不會用,初學者須要例子,cheat就是這個身邊的小抄。 cheat 命令不是Linux自帶的,你們參考這篇文章(英文版)安裝,實驗樓課程實驗系統中已經安裝了。
cheat是做弊,小抄的意思。
cheat命令是在GNU通用公共許可證下,爲Linux命令行用戶發行的交互式備忘單應用程序。它提供顯示Linux命令使用案例,包括該命令全部的選項和簡短但尚可理解的功能。
使用cheat命令做弊是能夠的。:)
4 其餘核心命令
和查找相關的核心命令還有find,locate,grep,whereis,which,其中:
Ubuntu中:
VIM是一個很是好的文本編輯器,不少專業程序員使用VIM編輯代碼,即便之後你不編寫程序,只要跟文本打交道,都應該學學VIM,能夠瀏覽參考一下普通人的編輯利器——Vim。
VIM學習曲線很是陡峭,各類編輯器學習曲線以下圖(有調侃的意思):
VI來講,一開始就須要至關大的技能,但一旦掌握這些技能,則你將會愈來愈熟練,這跟五筆打字很相似。咱們建議經過實踐練習來學習具體來講經過VIMTUTOR或玩遊戲(Vim大冒險或PacVim)來學習。
VIMTUTOR是個實踐教程,經過實踐30分鐘讓你對VIM編輯器入門,只要在命令行中輸入vimtutor,而後跟着教程練習就能夠了。以下圖:
而後你能夠把這張圖作計算機的桌面背景,天天學習一兩個鍵:
想當程序員用這張專門給程序員的鍵盤圖:
程序員有幾個鍵提示一下:
:set nu 顯示行號
:set ai 自動縮行
:set ts=4 設置一個 TAB 鍵等於幾個空格
[[ 轉到上一個位於第一列的"{"
]] 轉到下一個位於第一列的"{"
{ 轉到上一個空行
} 轉到下一個空行
gd 轉到當前光標所指的局部變量的定義
深刻學習參考:
GNU CC(簡稱爲gcc)是GNU項目中符合ANSI C標準的編譯系統,可以編譯用C、C++和Object C等語言編寫的程序。gcc又是一個交叉平臺編譯器,它可以在當前CPU平臺上爲多種不一樣體系結構的硬件平臺開發軟件,所以尤爲適合在嵌入式領域的開發編譯。
GCC編譯代碼的過程以下:
咱們能夠把編譯過程分紅四步,以編譯hello.c生成可執行文件hello爲例,以下圖:
編譯過程比較難記,咱們簡化一下,前三步,GCC的參數連起來是「ESc」,相應輸入的文件的後綴是「iso」,這樣記憶起來就容易多了。
學習GCC的另一個重點是:參考教材《深刻理解計算機系統》 7.6,7.10節,學習靜態庫,動態庫的製做。
建議使用CGDB,比GDB好用,熟悉VC的調試方式,能夠使用DDD。 注意使用GCC編譯時要加「-g」參數。 參考gdb參考卡GDB最基本的命令有:
問題:GDB的n(next)命令讓GDB執行下一行,而後暫停。 s(step)命令的做用與此相似,只是在函數調用時step命令會進入函數,那麼實際使用中應該優先選用哪一個?爲何?
其餘幾個我認爲應該掌握的調試命令有:
本實驗全部的源代碼均可下載:
githttp://git.shiyanlou.com/shiyanlou/Linux-c-codeclone
工欲善其事, 必先利其器,所以會從編程工具gcc,gdb入手逐步講解Linux系統編程。本節課程講解 gcc 編譯器的使用。
本實驗環境採用帶桌面的Ubuntu Linux環境,實驗中會用到桌面上的程序: 1.命令行終端: Linux命令行終端,打開後會進入Bash環境,能夠使用Linux命令
2.Firefox及Opera:瀏覽器,能夠用在須要前端界面的課程裏,只須要打開環境裏寫的HTML/JS頁面便可
3.gvim:很是好用的Vim編輯器,最簡單的用法能夠參考課程Vim編輯器
4.gedit及Brackets:若是您對gvim的使用不熟悉,能夠用這兩個做爲代碼編輯器,其中Brackets很是適用於前端代碼開發
後綴 |
源文件 |
.c |
C語言源文件 |
.C .cc .cxx |
C++源文件 |
.m |
Object-C源文件 |
.i |
通過預處理後的C源文件 |
.ii |
通過預處理後的C++源文件 |
.s .S |
彙編語言源文件 |
.h |
預處理文件(頭文件) |
.o |
目標文件 |
.a |
存檔文件 |
Tips:
sudo chmod u+x excutefile
注意:能夠使用GVim編輯器進行代碼輸入,代碼塊中的註釋能夠不需輸入。
打開的gvim環境中輸入i進入編輯模式,輸入如下代碼
// filename: hello.c
#include <stdio.h>
int main(int argc, char **argv)
{
printf"Hello, Shi-Yan-Lou!" ();
}
/**
*在XfceTerminal打開後的界面中輸入:$gcc hello.c -o hello
*若是沒有error,說明編譯成功,將會在當前目錄生成一個可執行文件 hello
*繼續輸入:./hello 就會運行該程序,在bash上打印出 Hello, Shi-Yan-Lou!
**/
保存爲hello.c文件
Tips;
gcc hello.c -o hello
--- 第二個hello爲文件名,名字任意取定(可是不能違反bash的規則) gcc hello.c -o "(-_-|||)"
, 可是做爲一名優秀的程序員仍是取個有意義的名字吧!首先gcc會調用預處理程序cpp,由它負責展開在源程序中定義的宏(上例:#include ),向其中插入#include語句所包含的內容(原地展開stdio.h包含的代碼)
在Xfce終端中輸入
$ gcc -E hello.c -o hello.i
還記得.i後綴嗎?hello.i這是一個通過預處理器處理以後的C源文件,在bash試試這個命令,而後用vim打開它。
gcc的-E參數可讓gcc在預處理結束後中止編譯過程。
第二步,將hello.i編譯爲目標代碼,gcc默認將.i文件當作是預處理後的C語言源代碼,所以它會直接跳過預處理,開始編譯過程。
$ gcc -c hello.i -o hello.o
一樣,用vim打開.o文件看看和.i .c文件有什麼不一樣?應該是一片亂碼,是吧?(它已是二進制文件了)
Tips:
第三步,gcc鏈接器將目標文件連接爲一個可執行文件,一個大體的編譯流程結束
gcc hello.o -o hello
如今不少軟件都是採用的模塊化開發,一般一個程序都是有不少個源文件組成,相應的就造成了多個編譯單元。gcc可以很好的處理這些編譯單元,最終造成一個可執行程序
代碼編輯和輸入參考上述使用gvim程序輸入,並在XfceTerminal界面使用gcc進行編譯。
// hello.h
extern void print();
這是個頭文件,將會在hello_main.c中調用
// hello_print.c
#include <stdio.h>
void print()
{
printf"Hello, Shi-Yan-Lou\n" ();
}
// hello_main.c
#include "hello.h"
void print();
int main(int argc, char **argv)
{
print();
}
// XfceTerminal中 $gcc hello_print.c hello_main.c -o hello 進行編譯
// 將會打印出 Hello, Shi-Yan-Lou
Tips: 以上的gcc hello_print.c hello_main.c -o hello
能夠當作是執行了一下3條命令
$ gcc -c hello_print.c -o hello_print.o
$ gcc -c hello_main.c -o hello_main.o
$ gcc hello_print.o hello_main.o -o hello
GDB 使用
1、實驗說明
1. 課程說明
工欲善其事, 必先利其器,所以會從編程工具gcc,gdb入手逐步講解Linux系統編程。上次咱們講解了 gcc 編譯器的使用,然而沒有什麼事物是天衣無縫的,每每寫出來的程序都會有不一樣程度的缺陷,所以本節課程將講解 gdb 調試器(Debug)的使用,它能夠幫助咱們找出程序之中的錯誤和漏洞等等。
2. 若是首次使用Linux,建議首先學習:
3. 環境介紹
本實驗環境採用帶桌面的Ubuntu Linux環境,實驗中會用到桌面上的程序: 1.命令行終端: Linux命令行終端,打開後會進入Bash環境,能夠使用Linux命令
2.Firefox及Opera:瀏覽器,能夠用在須要前端界面的課程裏,只須要打開環境裏寫的HTML/JS頁面便可
3.gvim:很是好用的Vim編輯器,最簡單的用法能夠參考課程Vim編輯器
4.gedit及Brackets:若是您對gvim的使用不熟悉,能夠用這兩個做爲代碼編輯器,其中Brackets很是適用於前端代碼開發
2、gdb 概 述
當程序編譯完成後,它可能沒法正常運行;或許程序會完全崩潰;或許只是不能正常地運行某些功能;或許它的輸出會被掛起;或許不會提示要求正常的輸入。不管在何種狀況下,跟蹤這些問題,特別是在大的工程中,將是開發中最困難的部分,咱們將學習gdb(GNU debugger)調試程序的方法,該程序是一個調試器,是用來幫助程序員尋找程序中的錯誤的軟件。
gdb是GNU開發組織發佈的一個強大的UNIX/Linux下的程序調試工具。或許,有人比較習慣圖形界面方式的,像VC、BCB等IDE環境,可是在UNIX/Linux平臺下作軟件,gdb這個調試工具備比VC、BCB的圖形化調試器更強大的功能。所謂「寸有所長,尺有所短」就是這個道理。 通常來講,gdb主要幫忙用戶完成下面4個方面的功能:
gdb.c
#include <stdio.h>
int func(int n)
{
int sum=0,i;
for(i=0; i<n; i++) {
sum+=i;
}
return sum;
}
int main(void)
{
int i;
long result = 0;
for(i=1; i<=100; i++) {
result += i;
}
printf("result[1-100] = %ld \n", result );
printf("result[1-250] = %d \n", func(250) );
}
編譯生成執行文件(Linux下):
$ gcc –g gdb.c -o testgdb
使用gdb調試:
$ gdb testgdb <---------- 啓動gdb
.......此處省略一萬行
鍵入 l命令至關於list命令,從第一行開始列出源碼:
$ gdb testgdb
.......此處省略一萬行
(gdb) l
7 {
8 sum+=i;
9 }
10 return sum;
11 }
12
13 int main(void)
14 {
15 int i;
16 long result = 0;
(gdb)
17 for(i=1; i<=100; i++)
18 {
19 result += i;
20 }
21 printf("result[1-100] = %ld \n", result );
22 printf("result[1-250] = %d \n", func(250) );
23 }
(gdb) break 16 <-------------------- 設置斷點,在源程序第16行處。
Breakpoint 1 at 0x804836a: file test.c, line 16.
(gdb) break func <-------------------- 設置斷點,在函數func()入口處。
Breakpoint 2 at 0x804832e: file test.c, line 5.
(gdb) info break <-------------------- 查看斷點信息。
Num Type Disp Enb Address What
1 breakpoint keep y 0x0804836a in main at test.c:16
2 breakpoint keep y 0x0804832e in func at test.c:5
(gdb) r <--------------------- 運行程序,run命令簡寫
Starting program: /home/shiyanlou/testgdb
Breakpoint 1, main () at test.c:16 <---------- 在斷點處停住。
16 long result = 0;
(gdb) n <--------------------- 單條語句執行,next命令簡寫。
17 for(i=1; i<=100; i++)
(gdb) n
19 result += i;
(gdb) n
17 for(i=1; i<=100; i++)
(gdb) n
19 result += i;
(gdb) n
17 for(i=1; i<=100; i++)
(gdb) c <--------------------- 繼續運行程序,continue命令簡寫。
Continuing.
result[1-100] = 5050 <----------程序輸出。
Breakpoint 2, func (n=250) at test.c:5
5 int sum=0,i;
(gdb) n
6 for(i=0; i<n; i++)
(gdb) p I <--------------------- 打印變量i的值,print命令簡寫。
$1 = 1107620064
(gdb) n
8 sum+=i;
(gdb) n
6 for(i=0; i<n; i++)
(gdb) p sum
$2 = 0
(gdb) bt <--------------------- 查看函數堆棧。
#0 func (n=250) at test.c:6
#1 0x080483b2 in main () at test.c:22
#2 0x42015574 in __libc_start_main () from /lib/tls/libc.so.6
(gdb) finish <--------------------- 退出函數。
Run till exit from #0 func (n=250) at test.c:6
0x080483b2 in main () at test.c:22
22 printf("result[1-250] = %d /n", func(250) );
Value returned is $3 = 31125
(gdb) c <--------------------- 繼續運行。
Continuing.
result[1-250] = 31125
Program exited with code 027. <--------程序退出,調試結束。
(gdb) q <--------------------- 退出gdb。
有了以上的感性認識,下面來系統地學習一下gdb。
3、使 用 gdb
gdb主要調試的是C/C++的程序。要調試C/C++的程序,首先在編譯時,必需要把調試信息加到可執行文件中。使用編譯器(cc/gcc/g++)的 -g 參數便可。如:
$ gcc -g hello.c -o hello
$ g++ -g hello.cpp -o hello
若是沒有-g,將看不見程序的函數名和變量名,代替它們的全是運行時的內存地址。當用-g把調試信息加入,併成功編譯目標代碼之後,看看如何用gdb來調試。 啓動gdb的方法有如下幾種:
在先前的課程中,咱們已經學習了 gcc 和 gdb 的使用。本節課程中,咱們將介紹 Makefile 的使用。Makefile帶來的好處就是——「自動化編譯」,一但寫好,只須要一個 make 命令,整個工程即可以徹底編譯,極大的提升了軟件的開發效率(特別是對於那些項目較大、文件較多的工程)。
本實驗環境採用帶桌面的Ubuntu Linux環境,實驗中會用到桌面上的程序: 1.命令行終端: Linux命令行終端,打開後會進入Bash環境,能夠使用Linux命令
2.Firefox及Opera:瀏覽器,能夠用在須要前端界面的課程裏,只須要打開環境裏寫的HTML/JS頁面便可
3.gvim:很是好用的Vim編輯器,最簡單的用法能夠參考課程Vim編輯器
4.gedit及Brackets:若是您對gvim的使用不熟悉,能夠用這兩個做爲代碼編輯器,其中Brackets很是適用於前端代碼開發
讀者常常看到一個C程序的項目經常由不少的文件組成,那麼,多文件的好處到底在哪裏呢?一個最簡單也最直接有力的理由就是,這樣能夠將一個大項目分紅多個小的部分,獨立開來,利於結構化管理。在修改和維護的時候,優點就更明顯了。例如,須要對代碼作一點小的改動,若是這個項目全部的代碼都在一個文件中,那麼就要從新編譯全部這些代碼,這是很耗時的,不只效率低,並且維護難度更大。可是,若是是多個不一樣的文件,那麼只須要從新編譯這些修改過的文件就好了,並且其餘源文件的目標文件都已經存在,沒有必要重複編譯,這樣就會快捷不少。
所以,經過合理有效的劃分,將一個項目分解爲多個易於處理的文件,是很是明智的作法。多文件的管理方式很是正確的選擇。
一個工程中的源文件不可勝數,按其類型、功能、模塊分別放在若干個目錄中。makefile定義了一系列的規則來指定,哪些文件須要先編譯,哪些文件須要後編譯,哪些文件須要從新編譯,甚至進行更復雜的功能操做(由於makefile就像一個shell腳本同樣,能夠執行操做系統的命令)。
makefile帶來的好處就是——「自動化編譯」,一但寫好,只須要一個make命令,整個工程徹底編譯,極大的提升了軟件的開發效率。make是一個命令工具,是一個及時makefile中命令的工具程序。
make工具最主要也是最基本的功能就是根據makefile文件中描述的源程序至今的相互關係來完成自動編譯、維護多個源文件工程。而makefile文件須要按某種語法進行編寫,文件中須要說明如何編譯各個源文件並連接生成可執行文件,要求定義源文件之間的依賴關係。
下面從一個簡單實例入手,介紹如何編寫Makefile。假設如今有一個簡單的項目由幾個文件組成:prog.c、 code.c、 code.h。這些文件的內容以下:
prog.c
#include <stdio.h> #include "code.h" int main(void) { int i = 1; printf ("myfun(i) = %d\n", myfun(i)); }
code.c
#include "code.h" int myfun(int in) { return in + 1; }
code.h
extern int myfun(int);
這些程序都比較短,結構也很清晰,所以使用下面的命令進行編譯:
$ gcc -c code.c -o code.o $ gcc -c prog.c -o prog.o $ gcc prog.o code.o -o test
如上所示,這樣就能生成可執行文件test,因爲程序比較簡單,並且數量也比較少,所以看不出來有多麻煩。可是,試想若是不僅上面的3個文件,而是幾十個或者是成百上千個甚至更多,那將是很是複雜的問題。
那麼如何是好呢?這裏就是makefile的絕佳舞臺,下面是一個簡單的makefile的例子。
首先$ vim Makefile
test: prog.o code.o gcc prog.o code.o -o test prog.o: prog.c code.h gcc -c prog.c -o prog.o code.o: code.c code.h gcc -c code.c -o code.o clean: rm -f *.o test
有了這個Makefile,不論何時修改源文件,只要執行一下make命令,全部必要的從新編譯將自動執行。make程序利用Makefile中的數據,生成並遍歷以test爲根節點的樹;如今咱們以上面的實例,來學習一下Makefile的通常寫法:
test(目標文件): prog.o code.o(依賴文件列表) tab(至少一個tab的位置) gcc prog.o code.o -o test(命令) .......
一個Makefile文件主要含有一系列的規則,每條規則包含一下內容:一個目標,即make最終須要建立的文件,如可執行文件和目標文件;目標也能夠是要執行的動做,如‘clean’;一個或多個依賴文件的列表,一般是編譯目標文件所須要的其餘文件。以後的一系列命令,是make執行的動做,一般是把指定的相關文件編譯成目標文件的編譯命令,每一個命令佔一行,並以tab開頭(初學者務必注意:是tab,而不是空格) 執行以上Makefile後就會自動化編譯:
$ make
gcc -c prog.c -o prog.o
gcc -c code.c -o code.o
gcc prog.o code.o -o test
最後就會多產生: porg.o code.o test這三個文件,執行./test
查看結果
還記得Makefile中的clean
嗎? make clean
就會去執行rm -f *.o test
這條命令,完成 clean 操做。
Makefile還能夠定義和使用宏(也稱作變量),從而使其更加自動化,更加靈活,在Makefile中定義宏的格式爲:
macroname = macrotext
使用宏的格式爲:
$(macroname)
用 「宏」 的方式,來改寫上面的 Makefile 例子。
參考答案:
OBJS = prog.o code.o CC = gcc test: $(BOJS) $(CC) $(OBJS) -o test prog.o: prog.c code.h $(CC) -c prog.c -o prog.o code.o: code.c code.h $(CC) -c code.c -o code.o clean: rm -f *.o test
本節課程介紹 Linux 系統的文件 IO,除了介紹其基本概念,最主要的是講解其基本 APIs,包括 open、close、read、write 等等。
本實驗環境採用帶桌面的Ubuntu Linux環境,實驗中會用到桌面上的程序: 1.命令行終端: Linux命令行終端,打開後會進入Bash環境,能夠使用Linux命令
2.Firefox及Opera:瀏覽器,能夠用在須要前端界面的課程裏,只須要打開環境裏寫的HTML/JS頁面便可
3.gvim:很是好用的Vim編輯器,最簡單的用法能夠參考課程Vim編輯器
4.gedit及Brackets:若是您對gvim的使用不熟悉,能夠用這兩個做爲代碼編輯器,其中Brackets很是適用於前端代碼開發
Linux系統調用(system call)是指操做系統提供給用戶程序的一組「特殊接口」,用戶程序能夠經過這組「特殊」接口來得到操做系統提供的特殊服務。
爲了更好的保護內核空間,將程序的運行空間分爲內核空間和用戶空間,他們運行在不一樣的級別上,在邏輯上是相互隔離的。在Linux中,用戶程序不能直接訪問內核提供的服務,必須經過系統調用來使用內核提供的服務。
Linux中的用戶編程接口(API)遵循了UNIX中最流行的應用編程界面標準——POSIX。這些系統調用編程接口主要是經過C庫(libc)實現的。
對內核而言,全部打開文件都由文件描述符引用。文件描述符是一個非負整數。當打開一個現存文件或建立一個新文件時,內核向進程返回一個文件描述符。當寫一個文件時,用open或create返回的文件描述符標識該文件,將其做爲參數傳送給read或write。
在POSIX應用程序中,整數0、一、2應被代換成符號常數:
這些常數都定義在頭文件<unistd.h>
中,文件描述符的範圍是0~OPEN_MAX。早期的UNIX版本採用的上限值是19(容許每一個進程打開20個文件), 如今不少系統則將其增長至256。
可用的文件I\O函數不少,包括:打開文件,讀文件,寫文件等。大多數Linux文件I\O只須要用到5個函數:open,read,write,lseek以及close。
須要包含的頭文件:<sys/types.h>
, <sys/stat.h>
, <fcntl.h>
函數原型:
int open(const str * pathname, int oflag, [..., mode_t mode])
功能:打開文件 返回值:成功則返回文件描述符,出錯返回-1 參數:
pathname: 打開或建立的文件的全路徑名 oflag:可用來講明此函數的多個選擇項, 詳見後。 mode:對於open函數而言,僅當建立新聞件時才使用第三個參數,表示新建文件的權限設置。
詳解oflag參數: oflag 參數由O_RDONLY(只讀打開)、O_WRONLY(只寫打開)、O_RDWR(讀寫打開)中的一個於下列一個或多個常數 O_APPEND: 追加到文件尾 O_CREAT: 若文件不存在則建立它。使用此選擇項時,需同時說明第三個參數mode,用其說明新聞件的訪問權限 O_EXCL: 若是同時指定O_CREAT,而該文件又是存在的,報錯;也能夠測試一個文件是否存在,不存在則建立。 O_TRUNC: 若是次文件存在,並且爲讀寫或只寫成功打開,則將其長度截短爲0 O_SYNC: 使每次write都等到物理I\O操做完成
用open建立一個文件: open.c
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define FILE_PATH "./test.txt" int main(void) { int fd; if ((fd = open(FILE_PATH, O_RDWR | O_CREAT | O_EXCL, 0666)) < 0) { printf("open error\n"); exit(-1); } else { printf("open success\n"); } return 0; }
若是當前目錄下以存在test.txt,屏幕上就會打印「open error」;不存在則建立該文件,並打印「open success」
須要包含的頭文件:<unistd.h>
函數原型:
ssize_t read(int fd, void * buf, size_t count)
功能:從打開的文件中讀取數據。 返回值:實際讀到的字節數;已讀到文件尾返回0,出錯的話返回-1,ssize_t是系統頭文件中用typedef定義的數據類型至關於signed int 參數: fd:要讀取的文件的描述符 buf:獲得的數據在內存中的位置的首地址 count:指望本次能讀取到的最大字節數。size_t是系統頭文件中用typedef定義的數據類型,至關於unsigned int
須要包含的頭文件:<unistd.h>
函數原型:
ssize_t write(int fd, const void * buf, size_t count)
功能:向打開的文件寫數據 返回值:寫入成功返回實際寫入的字節數,出錯返回-1
不得不提的是,返回-1的常見緣由是:磁盤空間已滿,超過了一個給定進程的文件長度
參數: fd:要寫入文件的文件描述符 buf:要寫入文件的數據在內存中存放位置的首地址 count:指望寫入的數據的最大字節數
read和write使用範例:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void) { char buf[100]; int num = 0; // 獲取鍵盤輸入,還記得POSIX的文件描述符嗎? if ((num = read(STDIN_FILENO, buf, 10)) == -1) { printf ("read error"); error(-1); } else { // 將鍵盤輸入又輸出到屏幕上 write(STDOUT_FILENO, buf, num); } return 0; }
須要包含的頭文件:<unistd.h>
函數原型:int close(int filedes)
功能:關閉一個打開的文件 參數:須要關閉文件的文件描述符
當一個進程終止的時候,它全部的打開文件都是由內核自動關閉。不少程序都使用這一功能而不顯式地調用close關閉一個已打開的文件。 可是,做爲一名優秀的程序員,應該顯式的調用close來關閉已再也不使用的文件。
每一個打開的文件都有一個「當前文件偏移量」,是一個非負整數,用以度量從文件開始處計算的字節數。一般,讀寫操做都是從當前文件偏移量處開始,並使偏移量增長所讀或寫的字節數。默認狀況下,你打開一個文件時(open),除非指定O_APPEND參數,否則位移量被設爲0。
須要包含的頭文件:<sys/types.h>
, <unistd.h>
函數原型:
off_t lseek(int filesdes, off_t offset, int whence)
功能:設置文件內容讀寫位置 返回值:成功返回新的文件位移,出錯返回-1;一樣off_t是系統頭文件定義的數據類型,至關於signed int 參數:
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <fcntl.h> int main(int argc, char * argv[]) { int fd; char buf[100]; if ((fd = open(argv[1], O_RDONLY)) < 0) { perror("open"); exit(-1); } read(fd, buf, 1); write(STDOUT_FILENO, buf, 1); lseek(fd, 2, SEEK_CUR); read(fd, buf, 1); write(STDOUT_FILENO, buf, 1); lseek(fd, -1, SEEK_END); read(fd, buf, 1); write(STDOUT_FILENO, buf, 1); lseek(fd, 0, SEEK_SET); read(fd, buf, 1); write(STDOUT_FILENO, buf, 1); close(fd); printf("\n"); return 0; }
以前的read函數能夠監控一個文件描述符(eg:鍵盤)是否有輸入,當鍵盤沒有輸入,read將會阻塞,直到用戶從鍵盤輸入爲止。用相同的方法能夠監控鼠標是否有輸入。但想同時監控鼠標和鍵盤是否有輸入,這個方法就不行的了。
// /dev/input/mice 是鼠標的設備文件
fd = open("/dev/input/mice", O_RDONLY); read(0, buf, 100); read(fd, buf, 100);
在上面的程序中,當read鍵盤的時候,若無鍵盤輸入則程序阻塞在第2行,此時即便鼠標有輸入,程序也沒有機會執行第3行得到鼠標的輸入。這種狀況就須要select同時監控多個文件描述符。
須要包含的頭文件:<sys/select.h>
函數原型:
int select(int maxfd, fd_set \* readset, fd_set \* writeset, fd_set \* exceptset, const struct timeval \* timeout)
返回值:失敗返回-1,成功返回readset,writeset,exceptset中全部,有指定變化的文件描述符的數目(若超時返回0)
參數: maxfd:要檢測的描述符個數, 所以值應爲最大描述符+1 readset:被監控是否有輸入的文件描述符集。不監控時,設爲NULL writeset:被監控是否能夠輸入的文件描述符集。不監控時,設爲NULL exceptset:被監控是否有錯誤產生的文件描述符集。不監控時,設爲NULL timeval:監控超時時間。設置爲NULL表示一直阻塞到有文件描述符被監控到有指定變化。
Tips: readset,writeset,exceptset這三個描述符集指針均是值—結果參數,調用的時候,被監控描述符相應位須要置1;返回時,未就緒的描數字相應位會被清0,而就緒的會被置1。 下面的系統定義的宏,和select配套使用 FD_ZERO(&rset):將文件描述符集rset的全部位清0FD_SET(4, &reset):設置文件描述符集rset的bit 4FD_CLR(fileno(stdin), &rset):將文件描述符集rset的bit 0清0FD_ISSET(socketfd, &rset):若文件描述符集rset中的socketfd位置1
#include <stdio.h> #include <sys/select.h> #include <fcntl.h> #include <unistd.h> #define MAXNUM 100 #define OPEN_DEV "/dev/input/mice" int main(void) { fd_set rfds; struct timeval tv; int retval, fd; char buf[MAXNUM]; fd = open(OPEN_DEV, O_RDONLY); while (1) { FD_ZERO(&rfds); FD_SET(0, &rfds); FD_SET(fd, &rfds); tv.tv_sec = 5; tv.tv_usec = 0; retval = select(fd+1, &rfds, NULL, NULL, &tv); if (retval < 0) printf ("error\n"); if (retval == 0) printf ("No data within 5 seconds\n"); if (retval > 0) { if (FD_ISSET(0, &rfds)) { printf ("Data is available from keyboard now\n"); read(0, buf, MAXNUM); } if (FD_ISSET(fd, &rfds)) { printf ("Data is available from mouse now\n"); read(fd, buf, MAXNUM); } } } return 0; }
本節課程繼續介紹 Linux 系統的文件 IO。主要介紹 stat 的使用(查看文件相關信息,例如文件類型、文件權限等等),以及目錄相關(打開、讀取、關閉目錄)的操做。
本實驗環境採用帶桌面的Ubuntu Linux環境,實驗中會用到桌面上的程序: 1.命令行終端: Linux命令行終端,打開後會進入Bash環境,能夠使用Linux命令
2.Firefox及Opera:瀏覽器,能夠用在須要前端界面的課程裏,只須要打開環境裏寫的HTML/JS頁面便可
3.gvim:很是好用的Vim編輯器,最簡單的用法能夠參考課程Vim編輯器
4.gedit及Brackets:若是您對gvim的使用不熟悉,能夠用這兩個做爲代碼編輯器,其中Brackets很是適用於前端代碼開發
Linux有個命令,ls -l
,效果以下:
$ ls -l -rw-rw-r-- 1 shiyanlou shiyanlou 978 Sep 19 02:13 hello.c
這個命令能顯示文件的類型、操做權限、硬連接數量、屬主、所屬組、大小、修改時間、文件名。它是怎麼得到這些信息的能,這一節咱們將撥開迷霧。
系統調用stat的做用是獲取文件的各個屬性。
須要包含的頭文件:<sys/types.h>
,<sys/stat.h>
,<unistd.h>
函數原型:
int stat(const char \* path, struct stat \* buf)
功能:查看文件或目錄屬性。將參數path所指的文件的屬性,複製到參數buf所指的結構中。 參數: path:要查看屬性的文件或目錄的全路徑名稱。 buf:指向用於存放屬性的結構體。stat成功調用後,buf的各個字段將存放各個屬性。struct stat是系統頭文件中定義的結構體,定義以下:
struct stat { dev_t st_dev; ino_t st_ino; mode_t st_mode; nlink_t st_nlink; uid_t st_uid; gid_t st_gid; dev_t st_rdev; off_t st_size; blksize_t st_blksize; blkcnt_t st_blocks; time_t st_atime; time_t st_mtime; time_t st_ctime; };
st_ino:節點號 st_mode:文件類型和文件訪問權限被編碼在該字段中 st_nlink:硬鏈接數 st_uid:屬主的用戶ID st_gid:所屬組的組ID st_rdev:設備文件的主、次設備號編碼在該字段中 st_size:文件的大小 st_mtime:文件最後被修改時間
返回值:成功返回0;失敗返回-1
#include <stdio.h> #include <stdlib.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> int main(int argc, char **argv) { struct stat buf; if(argc != 2) { printf("Usage: stat <pathname>"); exit(-1); } if(stat(argv[1], &buf) != 0) { printf("stat error."); exit(-1); } printf("#i-node: %ld\n", buf.st_ino); printf("#link: %d\n", buf.st_nlink); printf("UID: %d\n", buf.st_uid); printf("GID: %d\n", buf.st_gid); printf("Size %ld\n", buf.st_size); exit(0); }
上一小節中struct stat中有個字段爲st_mode
,可用來獲取文件類型和文件訪問權限,咱們將陸續學到從該字段解碼咱們須要的文件信息。 st_mode中文件類型宏定義:
宏定義 | 文件類型 |
---|---|
S_ISREG() | 普通文件 |
S_ISDIR() | 目錄文件 |
S_ISCHR() | 字符設備文件 |
S_ISBLK() | 塊設備文件 |
S_ISFIFO() | 有名管道文件 |
S_ISLNK() | 軟鏈接(符號連接)文件 |
S_ISSOCK() | 套接字文件 |
咱們修改上面的例子:
#include <stdio.h> #include <stdlib.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> int main(int argc, char **argv) { struct stat buf; char * file_mode; if(argc != 2) { printf("Usage: stat <pathname>\n"); exit(-1); } if(stat(argv[1], &buf) != 0) { printf("stat error.\n"); exit(-1); } if (S_ISREG(buf.st_mode)) file_mode = "-"; else if (S_ISDIR(buf.st_mode)) file_mode = "d"; else if (S_ISCHR(buf.st_mode)) file_mode = "c"; else if(S_ISBLK(buf.st_mode)) file_mode = "b"; printf("#i-node: %ld\n", buf.st_ino); printf("#link: %d\n", buf.st_nlink); printf("UID: %d\n", buf.st_uid); printf("GID: %d\n", buf.st_gid); printf("Size %ld\n", buf.st_size); printf("mode: %s\n", file_mode); exit(0); }
文件類型與許可設定被一塊兒編碼在st_mode字段中,同上面同樣,咱們也須要一組由系統提供的宏來完成解碼。
宏定義 | 文件類型 |
---|---|
S_ISUID | 執行時,設置用戶ID |
S_ISGID | 執行時,設置組ID |
S_ISVTX | 保存正文 |
S_IRWXU | 擁有者的讀、寫和執行權限 |
S_IRUSR | 擁有者的讀權限 |
S_IWUSR | 擁有者的寫權限 |
S_IXUSR | 擁有者的執行權限 |
S_IRWXG | 用戶組的讀、寫和執行權限 |
S_IRGRP | 用戶組的讀權限 |
S_IWGRP | 用戶組的寫權限 |
S_IXGRP | 用戶組的執行權限 |
S_IRWXO | 其它讀、寫、執行權限 |
S_IROTH | 其它讀權限 |
S_IWOTH | 其它寫權限 |
S_IXOTH | 其它執行權限 |
當目標是目錄而不是文件的時候,ls -l的結果會顯示目錄下全部子條目的信息,怎麼去遍歷整個目錄呢?答案立刻揭曉!
須要包含的頭文件:<sys/types.h>
,<dirent.h>
函數原型:DIR * opendir(const char * name)
功能:opendir()用來打開參數name指定的目錄,並返回DIR *形態的目錄流 返回值:成功返回目錄流;失敗返回NULL
函數原型:struct dirent * readdir(DIR * dir)
功能:readdir()返回參數dir目錄流的下一個子條目(子目錄或子文件) 返回值: 成功返回結構體指向的指針,錯誤或以讀完目錄,返回NULL
函數執行成功返回的結構體原型以下:
struct dirent { ino_t d_ino; off_t d_off; unsigned short d_reclen; unsigned char d_type; char d_name[256]; };
其中 d_name字段,是存放子條目的名稱
函數原型:int closedir(DIR * dir)
功能:closedir()關閉dir所指的目錄流 返回值:成功返回0;失敗返回-1,錯誤緣由在errno中
咱們來學習一個綜合的例子吧:
#include <stdio.h> #include <stdlib.h> #include <dirent.h> int main(int argc, char *argv[]) { DIR *dp; struct dirent *entp; if (argc != 2) { printf("usage: showdir dirname\n"); exit(0); } if ((dp = opendir(argv[1])) == NULL) { perror("opendir"); exit(-1); } while ((entp = readdir(dp)) != NULL) printf("%s\n", entp->d_name); closedir(dp); return 0; }
本節課程介紹 Linux 系統多進程編程。會先闡述一些理論知識,重點在於內存佈局以及 進程 fork 的知識點。
本實驗環境採用帶桌面的Ubuntu Linux環境,實驗中會用到桌面上的程序: 1.命令行終端: Linux命令行終端,打開後會進入Bash環境,能夠使用Linux命令
2.Firefox及Opera:瀏覽器,能夠用在須要前端界面的課程裏,只須要打開環境裏寫的HTML/JS頁面便可
3.gvim:很是好用的Vim編輯器,最簡單的用法能夠參考課程Vim編輯器
4.gedit及Brackets:若是您對gvim的使用不熟悉,能夠用這兩個做爲代碼編輯器,其中Brackets很是適用於前端代碼開發
進程的概念這裏就再也不過多的贅述了,市面上幾乎關於計算機操做系統的書都有詳細的描述。 在基本的概念裏咱們學習一下Linux進程狀態:
R (TASK_RUNNING),可執行狀態。
只有在該狀態的進程纔可能在CPU上運行。而同一時刻可能有多個進程處於可執行狀態,這些進程的task_struct結構(進程控制塊)被放入對應CPU的可執行隊列中(一個進程最多隻能出如今一個CPU的可執行隊列中)。進程調度器的任務就是從各個CPU的可執行隊列中分別選擇一個進程在該CPU上運行。
不少操做系統教科書將正在CPU上執行的進程定義爲RUNNING狀態、而將可執行可是還沒有被調度執行的進程定義爲READY狀態,這兩種狀態在linux下統一爲 TASK_RUNNING狀態。
S (TASK_INTERRUPTIBLE),可中斷的睡眠狀態。
處於這個狀態的進程由於等待某某事件的發生(好比等待socket鏈接、等待信號量),而被掛起。這些進程的task_struct結構被放入對應事件的等待隊列中。當這些事件發生時(由外部中斷觸發、或由其餘進程觸發),對應的等待隊列中的一個或多個進程將被喚醒。
經過ps命令咱們會看到,通常狀況下,進程列表中的絕大多數進程都處於TASK_INTERRUPTIBLE狀態(除非機器的負載很高)。畢竟CPU就這麼一兩個,進程動輒幾十上百個,若是不是絕大多數進程都在睡眠,CPU又怎麼響應得過來。
D (TASK_UNINTERRUPTIBLE),不可中斷的睡眠狀態。
與TASK_INTERRUPTIBLE狀態相似,進程處於睡眠狀態,可是此刻進程是不可中斷的。不可中斷,指的並非CPU不響應外部硬件的中斷,而是指進程不響應異步信號。 絕大多數狀況下,進程處在睡眠狀態時,老是應該可以響應異步信號的。不然你將驚奇的發現,kill -9居然殺不死一個正在睡眠的進程了!因而咱們也很好理解,爲何ps命令看到的進程幾乎不會出現TASK_UNINTERRUPTIBLE狀態,而老是TASK_INTERRUPTIBLE狀態。
而TASK_UNINTERRUPTIBLE狀態存在的意義就在於,內核的某些處理流程是不能被打斷的。若是響應異步信號,程序的執行流程中就會被插入一段用於處理異步信號的流程(這個插入的流程可能只存在於內核態,也可能延伸到用戶態),因而原有的流程就被中斷了。(參見《linux內核異步中斷淺析》) 在進程對某些硬件進行操做時(好比進程調用read系統調用對某個設備文件進行讀操做,而read系統調用最終執行到對應設備驅動的代碼,並與對應的物理設備進行交互),可能須要使用TASK_UNINTERRUPTIBLE狀態對進程進行保護,以免進程與設備交互的過程被打斷,形成設備陷入不可控的狀態。這種狀況下的TASK_UNINTERRUPTIBLE狀態老是很是短暫的,經過ps命令基本上不可能捕捉到。
linux系統中也存在容易捕捉的TASK_UNINTERRUPTIBLE狀態。執行vfork系統調用後,父進程將進入TASK_UNINTERRUPTIBLE狀態,直到子進程調用exit或exec(參見《神奇的vfork》)。 經過下面的代碼就能獲得處於TASK_UNINTERRUPTIBLE狀態的進程:
$ ps -ax | grep a\.out 4371 pts/0 D+ 0:00 ./a.out 4372 pts/0 S+ 0:00 ./a.out 4374 pts/1 S+ 0:00 grep a.out
而後咱們能夠試驗一下TASK_UNINTERRUPTIBLE狀態的威力。無論kill仍是kill -9,這個TASK_UNINTERRUPTIBLE狀態的父進程依然屹立不倒。
T (TASK_STOPPED or TASK_TRACED),暫停狀態或跟蹤狀態。
向進程發送一個SIGSTOP信號,它就會因響應該信號而進入TASK_STOPPED狀態(除非該進程自己處於TASK_UNINTERRUPTIBLE狀態而不響應信號)。(SIGSTOP與SIGKILL信號同樣,是很是強制的。不容許用戶進程經過signal系列的系統調用從新設置對應的信號處理函數。) 向進程發送一個SIGCONT信號,可讓其從TASK_STOPPED狀態恢復到TASK_RUNNING狀態。
當進程正在被跟蹤時,它處於TASK_TRACED這個特殊的狀態。「正在被跟蹤」指的是進程暫停下來,等待跟蹤它的進程對它進行操做。好比在gdb中對被跟蹤的進程下一個斷點,進程在斷點處停下來的時候就處於TASK_TRACED狀態。而在其餘時候,被跟蹤的進程仍是處於前面提到的那些狀態。
對於進程自己來講,TASK_STOPPED和TASK_TRACED狀態很相似,都是表示進程暫停下來。 而TASK_TRACED狀態至關於在TASK_STOPPED之上多了一層保護,處於TASK_TRACED狀態的進程不能響應SIGCONT信號而被喚醒。只能等到調試進程經過ptrace系統調用執行PTRACE_CONT、PTRACE_DETACH等操做(經過ptrace系統調用的參數指定操做),或調試進程退出,被調試的進程才能恢復TASK_RUNNING狀態。
Z (TASK_DEAD – EXIT_ZOMBIE),退出狀態,進程成爲殭屍進程。
進程在退出的過程當中,處於TASK_DEAD狀態。
在這個退出過程當中,進程佔有的全部資源將被回收,除了task_struct結構(以及少數資源)之外。因而進程就只剩下task_struct這麼個空殼,故稱爲殭屍。 之因此保留task_struct,是由於task_struct裏面保存了進程的退出碼、以及一些統計信息。而其父進程極可能會關心這些信息。好比在shell中,$?變量就保存了最後一個退出的前臺進程的退出碼,而這個退出碼每每被做爲if語句的判斷條件。 固然,內核也能夠將這些信息保存在別的地方,而將task_struct結構釋放掉,以節省一些空間。可是使用task_struct結構更爲方便,由於在內核中已經創建了從pid到task_struct查找關係,還有進程間的父子關係。釋放掉task_struct,則須要創建一些新的數據結構,以便讓父進程找到它的子進程的退出信息。
父進程能夠經過wait系列的系統調用(如wait四、waitid)來等待某個或某些子進程的退出,並獲取它的退出信息。而後wait系列的系統調用會順便將子進程的屍體(task_struct)也釋放掉。 子進程在退出的過程當中,內核會給其父進程發送一個信號,通知父進程來「收屍」。這個信號默認是SIGCHLD,可是在經過clone系統調用建立子進程時,能夠設置這個信號。
$ ps -ax | grep a\.out 10410 pts/0 S+ 0:00 ./a.out 10411 pts/0 Z+ 0:00 [a.out] 0413 pts/1 S+ 0:00 grep a.out
只要父進程不退出,這個殭屍狀態的子進程就一直存在。那麼若是父進程退出了呢,誰又來給子進程「收屍」? 當進程退出的時候,會將它的全部子進程都託管給別的進程(使之成爲別的進程的子進程)。託管給誰呢?多是退出進程所在進程組的下一個進程(若是存在的話),或者是1號進程。因此每一個進程、每時每刻都有父進程存在。除非它是1號進程。
1號進程,pid爲1的進程,又稱init進程。 linux系統啓動後,第一個被建立的用戶態進程就是init進程。它有兩項使命: 一、執行系統初始化腳本,建立一系列的進程(它們都是init進程的子孫); 二、在一個死循環中等待其子進程的退出事件,並調用waitid系統調用來完成「收屍」工做; init進程不會被暫停、也不會被殺死(這是由內核來保證的)。它在等待子進程退出的過程當中處於TASK_INTERRUPTIBLE狀態,「收屍」過程當中則處於TASK_RUNNING狀態。
X (TASK_DEAD – EXIT_DEAD),退出狀態,進程即將被銷燬。
而進程在退出過程當中也可能不會保留它的task_struct。好比這個進程是多線程程序中被detach過的進程(進程?線程?參見《linux線程淺析》)。或者父進程經過設置SIGCHLD信號的handler爲SIG_IGN,顯式的忽略了SIGCHLD信號。(這是posix的規定,儘管子進程的退出信號能夠被設置爲SIGCHLD之外的其餘信號。) 此時,進程將被置於EXIT_DEAD退出狀態,這意味着接下來的代碼當即就會將該進程完全釋放。因此EXIT_DEAD狀態是很是短暫的,幾乎不可能經過ps命令捕捉到。
以上內容均摘自博文:http://blog.csdn.net/huzia/article/details/18946491
獲取進程標誌號(pid)的API,主要有兩個函數:getpid和getppid
須要包含的頭文件:<sys/types.h>
, <unistd.h>
函數原型:pid_t getpid(void) 功能:獲取當前進程ID 返回值:調用進程的進程ID
函數原型:pid_t getppid(void) 功能:獲取父進程ID 返回值:調用進程的父進程ID
pid_ppid.c
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main(void) { pid_t pid = getpid(); pid_t ppid = getppid(); printf ("pid = %d\n", pid); printf ("ppid = %d\n", ppid); return 0; }
text:代碼段。存放的是程序的所有代碼(指令),來源於二進制可執行文件中的代碼部分
initialized data(簡稱data段)和uninitialized data(簡稱bss段)組成了數據段。
其中data段存放的是已初始化全局變量和已初始化static局部變量,來源於二進制可執行文件中的數據部分;bss段存放的是未初始化全局變量和未初始化static局部變量,其內容不來源於二進制可執行文件中的數據部分(也就是說:二進制可執行文件中的數據部分沒有未初始化全局變量和未初始化static局部變量)。根據C語言標準規定,他們的初始值必須爲0,所以bss段存放的是全0。將bss段清0的工做是由系統在加載二進制文件後,開始執行程序前完成的,系統執行這個清0操做是由內核的一段代碼完成的,這段代碼就是即將介紹的exec系統調用。至於exec從內存什麼地方開始清0以及要清0多少空間,則是由記錄在二進制可執行文件中的信息決定的(即:二進制文件中記錄了text、data、bss段的大小)
malloc是從heap(堆)中分配空間的
stack(棧)存放的是動態局部變量。
當子函數被調用時,系統會從棧中分配空間給該子函數的動態局部變量(注意:此時棧向內存低地址延伸);當子函數返回時,系統的棧會向內存高地址延伸,這至關於釋放子函數的動態局部變量的內存空間。咱們假設一下,main函數在調用子函數A後當即調用子函數B,那麼子函數B的動態局部變量會覆蓋原來子函數A的動態局部變量的存儲空間,這就是子函數不能互相訪問對方動態局部變量的根本物理緣由。
內存的最高端存放的是命令行參數和環境變量,將命令行參數和環境變量放到指定位置這個操做是由OS的一段代碼(exec系統調用)在加載二進制文件到內存後,開始運行程序前完成的
Linux下C進程內存佈局能夠由下面的程序的運行結果來得到驗證:
memery.c
1 #include <stdio.h> 2 #include <stdlib.h> 3 4 int global_init_val = 100; 5 int global_noninit_val; 6 extern char **environ; 7 8 int main(int argc, char *argv[], char *envp[]) 9 { 10 static int localstaticval = 10; 11 char *localval; 12 localval = malloc(10); 13 printf("address of text is : %p\n", main); 14 printf("address of data is : %p, %p\n", &global_init_val, &localstaticval); 15 printf("address of bss is : %p\n", &global_noninit_val); 16 printf("address of heap is : %p\n", localval); 17 printf("address of stack is : %p\n", &localval); 18 free(localval); 19 20 printf("&environ = %p, environ = %p\n", &envp, envp); 21 printf("&argv = %p, argv = %p\n", &argv, argv); 22 return 0; 23 }
運行結果,以下:
1 address of text is : 0x8048454 2 address of data is : 0x804a01c, 0x804a020 3 address of bss is : 0x804a02c 4 address of heap is : 0x96e1008 5 address of stack is : 0xbffca8bc 6 &environ = 0xbffca8d8, environ = 0xbffca97c 7 &argv = 0xbffca8d4, argv = 0xbffca974
運行結果分析: 運行結果的第1(二、三、四、五、六、7)行是由程序的第13(1四、1五、1六、1七、20、21)行打印的。 由運行結果的第一、二、三、4行可知,存放的是程序代碼的text段位於進程地址空間的最低端;往上是存放已初始化全局變量和已初始化static局部變量的data段;往上是存放未初始化全局變量的bss段;往上是堆區(heap)。 由運行結果的第七、六、5行可知,命令行參數和環境變量存放在進程地址空間的最高端;往下是存放動態局部變量的棧區(stack)。
壞境變量在內存中一般是一字符串環境變量名=環境變量值的形式存放。對壞境變量含義的急事依賴於具體的應用程序。咱們的程序可能會調用Linux系統的環境變量,甚至修改環境變量,因此,Linux向咱們提供了這種API。
須要包含的頭文件:<stdlib.h>
函數原型: char * getenc(const char * name) 返回字符指針,該指針指向變量名爲name的環境變量的值字符串。
int putenv(const char * str) 將「環境變量=環境變量值」形式的字符創增長到環境變量列表中;若是該環境變量已存在,則更新已有的值。
int setenv(const char * name, const char * value, int rewrite) 設置名字爲name的環境變量的值爲value;若是該環境變量已存在,且rewrite不爲0,用新值替換舊值;rewrite爲0,就不作任何事。
env.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char *argv[], char *envp[]) { char **ptr; for (ptr = envp; *ptr != 0; ptr++) /* and all env strings */ printf ("%s\n", *ptr); printf ("\n\n--------My environment variable-------\n\n"); printf ("USERNAME is %s\n", getenv("USERNAME")); putenv ("USERNAME=shiyanlou"); printf ("USERNAME is %s\n", getenv("USERNAME")); setenv ("USERNAME", "shiyanlou-2", 0); printf ("USERNAME is %s\n", getenv("USERNAME")); setenv ("USERNAME", "shiyanlou-2", 1); printf ("USERNAME is %s\n", getenv("USERNAME")); return 0; }
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main(void) { pid_t pid; if ((pid = fork()) == 0) { getchar(); exit(0); } getchar(); }
父進程調用fork將會產生一個子進程。此時會有2個問題:
子進程的代碼是父進程代碼的一個徹底相同拷貝。事實上不只僅是text段,子進程的所有進程空間(包括:text/data/bss/heap/stack/command line/environment variables)都是父進程空間的一個徹底拷貝。 下一個問題是:誰爲子進程分配了內存空間?誰拷貝了父進程空間的內容到子進程的內存空間?fork當仁不讓!事實上,查看fork實現的源代碼,由4部分工做組成:首先,爲子進程分配內存空間;而後,將父進程空間的所有內容拷貝到分配給子進程的內存空間;而後在內核數據結構中建立並正確初始化子進程的PCB(包括2個重要信息:子進程pid,PC的值=善後代碼的第1條指令地址);最後是一段善後代碼。 因爲子進程的PCB已經產生,因此子進程已經出生,所以子進程就能夠被OS調度到來運行。子進程首次被OS調度時,執行的第1條代碼在fork內部,不過從應用程序的角度來看,子進程首次被OS調度時,執行的第1條代碼是從fork返回。這就致使了fork被調用1次,卻返回2次:父、子進程中各返回1次。對於應用程序員而言,最重要的是fork的2次返回值不同,父進程返回值是子進程的pid,子進程的返回值是0。 至於子進程產生後,父、子進程誰先運行,取決於OS調度策略,應用程序員沒法控制。 以上分析了fork的內部實現以及對應用程序的影響。若是應用程序員以爲難以理解的話,能夠暫時拋開,只要記住3個結論便可:
- fork函數被調用1次(在父進程中被調用),但返回2次(父、子進程中各返回一次)。兩次返回的區別是子進程的返回值是0,而父進程的返回值則是子進程的進程ID。
- 父、子進程徹底同樣(代碼、數據),子進程從fork內部開始執行;父、子進程從fork返回後,接着執行下一條語句。
- 通常來講,在fork以後是父進程先執行仍是子進程先執行是不肯定的,應用程序員沒法控制。
fork.c
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <sys/types.h> 5 6 #define err_sys(info) 7 { 8 printf ("%s\n", info); 9 exit(0); 10 } 11 12 int glob = 6; /* external variable in initialized data */ 13 char buf[ ] = "a write to stdout\n"; 14 15 int main(void) 16 { 17 int var; /* automatic variable on the stack */ 18 pid_t pid; 19 var = 88; 20 21 if ((write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)) 22 err_sys("write error"); 23 24 printf("before fork\n"); /* we don't flush stdout */ 25 26 if ( (pid = fork()) < 0) { 27 err_sys("fork error"); 28 } else if (pid == 0) { /* child */ 29 glob++; /* modify variables */ 30 var++; 31 } else { 32 sleep(2); /* parent */ 33 } 34 35 printf("pid = %d, ppid = %d, glob = %d, var = %d\n", getpid(),getppid(), glob, var); 36 exit(0); 37 }
運行結果:
1 a write to stdout 2 before fork 3 pid = 9009, ppid = 9008, glob = 7, var = 89 4 pid = 9008, ppid = 8979, glob = 6, var = 88
運行結果分析: 結果的第1行是由父進程的21行打印; 結果的第2行是由父進程的24行打印; 因爲父進程在24行睡眠了2秒,所以fork返回後,子進程先於父進程運行是大機率事件,因此子進程運行到25行打印出結果中的第3行。因爲子進程會拷貝父進程的整個進程空間(這其中包括數據),所以當子進程26行從fork返回後,子進程中的glob=6,var=88(拷貝自父進程的數據)。此時子進程中pid=0,所以子進程會執行2九、30行,當子進程到達35行時,將打印glob=7,var=89。
雖然,子進程改變了glob和var的值,但它僅僅是改變了子進程中的glob和var,而影響不了父進程中的glob和var。在子進程出生後,父、子進程的進程空間(代碼、數據等)就是獨立,互不干擾的。所以當父進程運行到35行,將會打印父進程中的glob和var的值,他們分別是6和88,這就是運行結果的第4行。
本節繼續介紹 Linux 系統多進程編程。上節課程主要介紹了 fork,這節課程將介紹另外一個重要的進程相關的 exec。
本實驗環境採用帶桌面的Ubuntu Linux環境,實驗中會用到桌面上的程序: 1.命令行終端: Linux命令行終端,打開後會進入Bash環境,能夠使用Linux命令
2.Firefox及Opera:瀏覽器,能夠用在須要前端界面的課程裏,只須要打開環境裏寫的HTML/JS頁面便可
3.gvim:很是好用的Vim編輯器,最簡單的用法能夠參考課程Vim編輯器
4.gedit及Brackets:若是您對gvim的使用不熟悉,能夠用這兩個做爲代碼編輯器,其中Brackets很是適用於前端代碼開發
右側的表稱爲i節點表,在整個系統中只有1張。該表能夠視爲結構體數組,該數組的一個元素對應於一個物理文件。
中間的表稱爲文件表,在整個系統中只有1張。該表能夠視爲結構體數組,一個結構體中有不少字段,其中有3個字段比較重要:
左側的表稱爲文件描述符表,每一個進程有且僅有1張。該表能夠視爲指針數組,數組的元素指向文件表的一個元素。最重要的是:數組元素的下標就是大名鼎鼎的文件描述符。
open系統調用執行的操做:新建一個i節點表元素,讓其對應打開的物理文件(若是對應於該物理文件的i節點元素已經創建,就不作任何操做);新建一個文件表的元素,根據open的第2個參數設置file status flags字段,將current file offset字段置0,將v-node ptr指向剛創建的i節點表元素;在文件描述符表中,尋找1個還沒有使用的元素,在該元素中填入一個指針值,讓其指向剛創建的文件表元素。最重要的是:將該元素的下標做爲open的返回值返回。
這樣一來,當調用read(write)時,根據傳入的文件描述符,OS就能夠找到對應的文件描述符表元素,進而找到文件表的元素,進而找到i節點表元素,從而完成對物理文件的讀寫。
fork會致使子進程繼承父進程打開的文件描述符,其本質是將父進程的整個文件描述符表複製一份,放到子進程的PCB中。所以父、子進程中相同文件描述符(文件描述符爲整數)指向的是同一個文件表元素,這將致使父(子)進程讀取文件後,子(父)進程將讀取同一文件的後續內容。
案例分析(forkfd.c):
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <fcntl.h> 4 #include <sys/types.h> 5 #include <unistd.h> 6 #include <sys/types.h> 7 8 int main(void) 9 { 10 int fd, pid, status; 11 char buf[10]; 12 if ((fd = open("./test.txt", O_RDONLY)) < 0) { 13 perror("open"); exit(-1); 14 } 15 if ((pid = fork()) < 0) { 16 perror("fork"); exit(-1); 17 } else if (pid == 0) { //child 18 read(fd, buf, 2); 19 write(STDOUT_FILENO, buf, 2); 20 } else { //parent 21 sleep(2); 23 lseek(fd, SEEK_CUR, 1); 24 read(fd, buf, 3); 25 write(STDOUT_FILENO, buf, 3); 26 write(STDOUT_FILENO, "\n", 1); 27 } 28 return 0; 29 }
假設,./test.txt的內容是abcdefg。那麼子進程的18行將讀到字符ab;因爲,父、子進程的文件描述符fd都指向同一個文件表元素,所以當父進程執行23行時,fd對應的文件的讀寫指針將移動到字符d,而不是字符b,從而24行讀到的是字符def,而不是字符bcd。程序運行的最終結果是打印abdef,而不是abbcd。
相對應的,若是是兩個進程獨立調用open去打開同一個物理文件,就會有2個文件表元素被建立,而且他們都指向同一個i節點表元素。兩個文件表元素都有本身獨立的current file offset字段,這將致使2個進程獨立的對同一個物理文件進行讀寫,所以第1個進程讀取到文件的第1個字符後,第2個進程再去讀取該文件時,仍然是讀到的是文件的第1個字符,而不是第1個字符的後續字符。
對應用程序員而言,最重要結論是: 若是子進程不打算使用父進程打開的文件,那麼應該在fork返回後當即調用close關閉該文件。
在forkbase.c中,fork出子進程後,爲了保證子進程先於父進程運行,在父進程中使用了sleep(2)的方式讓父進程睡眠2秒。但實際上這樣作,並不能100%保證子進程先於父進程運行,由於在負荷很是重的系統中,有可能在父進程睡眠2秒期間,OS並無調度到子進程運行,而且當父進程睡醒後,首先調度到父進程運行。那麼,如何才能100%保證父、子進程徹底按程序員的安排來進行同步呢?答案是:系統調用wait!
須要包含的頭文件: <sys/types.h>、<sys/wait.h> 函數原型:pid_t wait(int * status) 功能:等待進程結束。 返回值:若成功則爲子進程ID號,若出錯則爲-1. 參數說明: status:用於存放進程結束狀態。
wait函數用於使父進程阻塞,直到一個子進程結束。父進程調用wait,該父進程可能會:
wait.c
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <sys/types.h> 4 #include <unistd.h> 5 #include <wait.h> 6 void pr_exit(intstatus); 7 int main(void) 8 { 9 pid_t pid; 10 int status; 11 if ( (pid = fork()) < 0) 12 { perror("fork");exit(-1); } 13 else if (pid == 0) { /* child */ 14 sleep(1); 15 printf("inchild\n"); 16 exit(101); 17 } 18 if (wait(&status) != pid) /* wait for child */ 19 { perror("wait");exit(-2); } 20 printf("in parent\n"); 21 pr_exit(status); /* and print itsstatus */ 22 if ( (pid = fork()) < 0) 23 { perror("fork");exit(-1); } 24 else if (pid == 0) /*child */ 25 abort(); /* generates SIGABRT */ 26 if (wait(&status) != pid) /* wait for child */ 27 { perror("wait");exit(-2); } 28 pr_exit(status); /* and printits status */ 29 if ( (pid = fork()) < 0) 30 { perror("fork");exit(-1); } 31 else if (pid == 0) /*child */ 32 status /= 0; /* divide by 0 generates SIGFPE */ 33 if (wait(&status) != pid) /* wait for child */ 34 { perror("wait");exit(-1); } 35 pr_exit(status); /* and printits status */ 36 exit(0); 37 } 38 void pr_exit(int status) { 39 if (WIFEXITED(status)) 40 printf("normallytermination, low-order 8 bit of exit status = %d\n", WEXITSTATUS(status)); 41 else if(WIFSIGNALED(status)) 42 printf("abnormallytermination, singal number = %d\n", WTERMSIG(status)); 43 }
運行結果分析:
11行建立了一個子進程,13行根據fork的返回值區分父、子進程。 咱們先看父進程,父進程從18行運行,這裏調用了wait函數等待子進程結束,並將子進程結束的狀態保存在status中。這時,父進程就阻塞在wait這裏了,這樣就保證了子進程先運行。子進程從13行開始運行,而後sleep 1秒,打印出「in child」後,調用exit函數退出進程。這裏exit中有個參數101,表示退出的值是101。.子進程退出後,父進程wait到了子進程的狀態,並把狀態保存到了status中。後面的pr_exit函數是用來對進程的退出狀態進行打印。接下來,父進程又建立一個子進程,而後又一次調用wait函數等待子進程結束,父進程這時候阻塞在了wait這裏。子進程開始執行,子進程裏面只有一句話:abort(),abort會結束子進程併發送一個SIGABORT信號,喚醒父進程。因此父進程會接受到一個SIGABRT信號,並將子進程的退出狀態保存到status中。而後調用pr_exit函數打印出子進程結束的狀態。而後父進程再次建立了一個子進程,依然用wait函數等待子進程結束並獲取子進程退出時的狀態。子進程裏面就一句status/= 0,這裏用0作了除數,因此子進程會終止,併發送一個SIGFPE信號,這個信號是用來表示浮點運算異常,好比運算溢出,除數不能爲0等。這時候父進程wait函數會捕捉到子進程的退出狀態,而後調用pr_exit處理。 pr_exit函數將status狀態傳入,而後判斷該狀態是否是正常退出,若是是正常退出會打印出退出值;不是正常退出會打印出退出時的異常信號。這裏用到了幾個宏,簡單解釋以下:
WIFEXITED: 這個宏是用來判斷子進程的返回狀態是否是爲正常,若是是正常退出,這個宏返回真。 WEXITSTATUS: 用來返回子進程正常退出的狀態值。WIFSIGNALED: 用來判斷子進程的退出狀態是不是非正常退出,若非正常退出時發送信號,則該宏返回真。 WTERMSIG: 用來返回非正常退出狀態的信號number。 因此這段代碼的結果是分別打印出了三個子進程的退出狀態和異常結束的信號編號。
當一個程序調用fork產生子進程,一般是爲了讓子進程去完成不一樣於父進程的某項任務,所以含有fork的程序,一般的編程模板以下:
if ((pid = fork()) == 0) { dosomething in child process; exit(0); } do something in parent process;
這樣的編程模板使得父、子進程各自執行同一個二進制文件中的不一樣代碼段,完成不一樣的任務。這樣的編程模板在大多數狀況下都能勝任,但仔細觀察這種編程模板,你會發現它要求程序員在編寫源代碼的時候,就要預先知道子進程要完成的任務是什麼。這本不是什麼過度的要求,但在某些狀況下,這樣的前提要求卻得不到知足,最典型的例子就是Linux的基礎應用程序 —— shell。你想想,在編寫shell的源代碼期間,程序員是不可能知道當shell運行時,用戶輸入的命令是ls仍是cp,難道你要在shell的源代碼中使用if--elseif--else if--else if ……結構,並拷貝 ls、cp等等外部命令的源代碼到shell源代碼中嗎?退一萬步講,即便這種弱智的處理方式被接受的話,你仍然會遇到沒法解決的難題。想想,若是用戶本身編寫了一個源程序,並將其編譯爲二進制程序test,而後再在shell命令提示符下輸入./test,對於採用前述弱智方法編寫的shell,它將情何以堪?
看來天字1號雖然很牛,但亦難以獨木擎天,必要狀況下,也須要地字1號予以協做,啊,偉大的團隊精神!
下面就詳細介紹一下進程控制地字第1號系統調用——exec的機制和用法。
在用fork函數建立子進程後,子進程每每要調用exec函數以執行另外一個程序。 當子進程調用exec函數時,會將一個二進制可執行程序的全路徑名做爲參數傳給exec,exec會用新程序代換子進程原來所有進程空間的內容,而新程序則從其main函數開始執行,這樣子進程要完成的任務就變成了新程序要完成的任務了。 由於調用exec並不建立新進程,因此先後的進程ID並未改變。exec只是用另外一個新程序替換了當前進程的正文、數據、堆和棧段。進程仍是那個進程,但實質內容已經徹底改變。呵呵,這是否是和中國A股的借殼上市有殊途同歸之妙? 順便說一下,新程序的bss段清0這個操做,以及命令行參數和環境變量的指定,也是由exec完成的。
函數原型: int execle(const char * pathname,const char * arg0, ... (char *)0, char * const envp [] )
返回值: exec執行失敗返回-1,成功將永不返回(想一想爲何?)。哎,牛人就是有脾氣,天字1號是調用1次,返回2次;地字1號,乾脆就不返回了,你能奈我何?
參數: pathname:新程序的二進制文件的全路徑名 arg0:新程序的第1個命令行參數argv[0],以後是新程序的第二、三、4……個命令行參數,以(char*)0表示命令行參數的結束 envp:新程序的環境變量
echoall.c
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 5 int main(int argc, char*argv[]) 6 { 7 int i; 8 char **ptr; 9 extern char **environ; 10 for (i = 0; i < argc; i++) /* echo all command-line args */ 11 printf("argv[%d]:%s\n", i, argv[i]); 12 for (ptr = environ; *ptr != 0;ptr++) /* and all env strings */ 13 printf("%s\n",*ptr); 21 }
將此程序進行編譯,生成二進制文件命名爲echoall,放在當前目錄下。很容易看出,此程序運行將打印進程的全部命令行參數和環境變量。
!源文件過長,請直接查看源代碼 exec.c
程序運行結果:
1 argv[0]: echoall 2 argv[1]: myarg1 3 argv[2]: MY ARG2 4 USER=unknown 5 PATH=/tmp 6 argv[0]: echoall 7 argv[1]: only 1 arg 8 ORBIT_SOCKETDIR=/tmp/orbit-dennis 9 SSH_AGENT_PID=1792 10 TERM=xterm 11 SHELL=/bin/bash 12 XDG_SESSION_COOKIE=0a13eccc45d521c3eb847f7b4bf75275-1320116445.669339 13 GTK_RC_FILES=/etc/gtk/gtkrc:/home/dennis/.gtkrc-1.2-gnome2 14 WINDOWID=62919986 15 GTK_MODULES=canberra-gtk-module 16 USER=dennis .......
運行結果分析: 1-5行是第1個子進程14行運行新程序echoall的結果,其中:1-3行打印的是命令行參數;四、5行打印的是環境變量。 6行以後是第2個子進程23行運行新程序echoall的結果,其中:六、7行打印的是命令行參數;8行以後打印的是環境變量。之因此第2個子進程的環境變量那麼多,是由於程序23行調用execlp時,沒有給出環境變量參數,所以子進程就會繼承父進程的所有環境變量。
本節是介紹 Linux 系統多進程編程的最後一節課程。會涉及一些 gdb 在調試多進程程序方面的技巧,以及經進程消亡相關的知識點。
本實驗環境採用帶桌面的Ubuntu Linux環境,實驗中會用到桌面上的程序: 1.命令行終端: Linux命令行終端,打開後會進入Bash環境,能夠使用Linux命令
2.Firefox及Opera:瀏覽器,能夠用在須要前端界面的課程裏,只須要打開環境裏寫的HTML/JS頁面便可
3.gvim:很是好用的Vim編輯器,最簡單的用法能夠參考課程Vim編輯器
4.gedit及Brackets:若是您對gvim的使用不熟悉,能夠用這兩個做爲代碼編輯器,其中Brackets很是適用於前端代碼開發
對多進程程序進行調試,存在一個較大的難題,那就是當程序調用fork產生子進程後,gdb跟蹤的是父進程,沒法進入到子進程裏去單步調試子進程。這樣一來,若是子進程中的代碼運行出錯的話,將沒法進行調試。
所以想調試子進程的話,須要一點技巧:
從程序員的角度看,C應用程序從main函數開始運行。但事實上,當C應用程序被內核經過exec啓動時,一個啓動例程會先於main函數運行,它會爲main函數的運行準備好環境後,調用main函數。而main函數正常結束後return語句將使得main函數返回到啓動例程,啓動例程在完成必要的善後處理後將最終調用_exit結束進程。
有5衝方式結束進程: 正常結束: 1.從main函數返回 2.調用exit 3.調用_exit
非正常結束: 4.調用abort 5.被信號停止
須要包含的頭文件:<stdlib.h>
、<unistd.h>
函數原型: void exit(int status)
、void _exit(int status)
這兩個函數的功能都是使進程正常結束。 _exit
:當即返回內核,它是一個系統調用exit
:在返回內核錢會執行一些清理操做,這些清理操做包括調用exit handler,以及完全關閉標準I/O流(這回使得I/O流的buffer中的數據被刷新,即被提交給內核),它是標準C庫中的一個函數。
上一節提到I/O流以及I/O流的buffer,咱們如今來了解一下。
iocache.c
1 #include <stdio.h> 2 #include <unistd.h> 3 4 int main(void) 5 { 6 printf("hello"); 7 //printf("hello\n"); 8 //write(1, "hello", 5); 9 sleep(100); 10 return 0; 11 }
你將會看到的是,沒有任何輸出!爲何呢?
當應用程序調用printf時,將字符串"hello"提交給了標準I/O庫的I/O庫緩存。I/O庫緩存大體能夠認爲是printf實現中定義的全局字符數組,所以它位於用戶空間,可見"hello"並無被提交給內核(因此也不可能出現內核將"hello"打印到屏幕的操做),因此沒有打印出任何東西。只有當某些條件知足時,標準I/O庫纔會刷新I/O庫緩存,這些條件包括:
Tips: 當標準I/O庫緩存時,會調用之前的咱們學過的系統調用,例如:write,將I/O庫緩存中的內容提交給內核。 so,上述代碼也能夠這樣:第6行註釋,第7行註釋,第8行取消註釋。也能夠在屏幕上看見"hello"
Exit handler 是程序員編寫的函數,進程正常結束時,它們會被系統調回。這使程序員具有了在進程正常結束時,控制進程執行某些善後操做的能力。 使用Exit handler,須要程序員完成兩件事情:編寫Exit handler函數;調用atexit或on_exit向系統註冊Exit handler(即告知系統須要回調的Exit handler函數是誰)
須要包含頭的文件:<stdlib.h>
函數原型:
int atexit(void (* func)(void))
int on_exit(void (* func)(int, void *),)
功能: atexit註冊的函數func沒有參數;on_exit註冊的函數func有一個int型參數,系統調用回調func時將向該參數傳入進程的退出值,func的另外一個void *類型參數將會是arg。
ANSI C中,進程最多能夠註冊32個Exit handler函數,這些函數按照註冊時的順序被逆序調用。
實驗截圖:#include <stdio.h> #include <stdlib.h> #include <unistd.h> static void my_exit0(int, void *); static void my_exit1(void); static void my_exit2(void); char str[9]="for test"; int main(void) { //char str[9]="for test"; if (atexit(my_exit2) != 0) { perror("can't register my_exit2"); exit(-1); } if (atexit(my_exit1) != 0) { perror("can't register my_exit1"); exit(-1); } if (on_exit(my_exit0,(void *)str) !=0) { perror("can't register my_exit0"); exit(-1); } printf("main is done\n"); printf("abc"); //_exit(1234); exit(1234); } static void my_exit0(int status, void *arg) { printf("zero exit handler\n"); printf("exit %d\n", status); printf("arg=%s\n",(char *)arg); } static void my_exit1(void) { printf("first exit handler\n"); } static void my_exit2(void) { printf("second exit handler\n"); }
實驗中存在的問題:
主要問題是:GDB部分實驗過程較爲簡略,沒有詳細實驗過程很難弄清楚。