歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Unix知識 >> Unix基礎知識 >> Unix基礎教程(7)

Unix基礎教程(7)

日期:2017/2/25 10:12:33   编辑:Unix基礎知識

第7章 進程控制與進程間通信

【例7-1】 使用size命令觀察可執行程序文件的指令段和數據段大小。

下面是size命令執行的例子,觀察程序文件grep,awk,cat,more的指令段和數據段大小。text指的是指令段。data指的是初始化的只讀型數據區和初始化的讀寫型數據區兩部分。bss指的是未初始化的數據區。bss來源於早期IBM 740匯編程序的一個偽指令,原意是block started by symbol(由符號開始的塊),由於歷史的原因未初始化的數據區仍叫做bss段。有人喜歡把它解釋為Better Save Space(更有效地節省空間),因為程序文件中沒必要存貯整個bss映像,只需要記住它的大小就可以了。exec系統調用加載新程序時,會根據程序文件中記載的bss段大小,分配內存並將這個區域初始化清為0。dec和hex分別是這三個部分空間之和的十進制和十六進制表示。不同的UNIX系統中size命令的輸出格式會有不同。

$ cd /bin

$ size grep awk cat more

text data bss dec hex filename

72315 660 2456 75431 126a7 grep

284569 6376 20328 311273 4bfe9 awk

12412 468 328 13208 3398 cat

24385 588 10128 35101 891d more

【例7-2】 函數scanf執行期間進程狀態的變化和系統的活動步驟。

【例7-3】 使用time命令統計執行一個命令所占用的CPU時間。

/usr/bin/time find /usr -name '*.c' -print

命令執行完畢後,最終給出的時間報告是

Real 6.06

User 0.36

System 2.13

如果在C-shell中直接使用time命令,那麼使用的C-shell的內部命令

time find /usr -name '*.h' -print

命令執行完畢後,最終給出一個資源使用報告:

0.4u 6.2s 0:10 61% 4+28k 0+0io 0pf+0w

【例7-4】 使用vmstat命令監視整個系統對CPU的占用情況。

$ vmstat 10

procs memory swap io system cpu

r b w swpd free buff cache si so bi bo in cs us sy id

0 0 0 0 55916 6128 38156 0 0 439 118 146 180 8 15 76

0 0 0 0 55252 6160 38160 0 0 0 32 112 54 26 1 73

0 0 0 0 46380 7384 38160 0 0 121 21 136 91 15 8 76

0 0 0 0 46360 7400 38736 0 0 58 19 110 41 1 0 99

^C

其中,對CPU的利用率來說,us是用戶時間,sy是系統時間,id是空閒時間。這裡給出的都是百分比。以第一個10秒為例,用戶時間8%,系統時間15%,CPU空閒率76%。CPU空閒率越大,系統的負載越小。其它字段,in為包括時鐘中斷在內的每秒硬件中斷次數,cs為上下文切換次數(context switch)。其它字段,可以查閱聯機手冊。

【例7-5】 監視一個文件,如果文件被其它進程追加了內容,就把追加的內容打印出來。

下面的例子是一段程序,功能類似tail -f命令。

$ awk '{print NR, $0}' tailf0.c

1 #include <fcntl.h>

2 main(int argc, char **argv)

3 {

4 unsigned char buf[1024];

5 int len, fd;

6 if ( (fd = open(argv[1], O_RDONLY)) < 0) {

7 perror("Open file");

8 exit(1);

9 }

10 lseek(fd, 0, SEEK_END);

11 for(;;) {

12 while ( (len = read(fd, buf, sizeof buf)) > 0)

13 write(1, buf, len);

14 }

15 }

$ cc tailf0.c -o tailf0

$ ./tailf0 /usr/adm/pppd.log

第10行將文件的讀寫位置設置到文件尾,然後進入一個永無休止的循環。循環體內的第12行從文件中讀取數據。如果len的值大於零,那麼,read結束後buf中存儲的len個字符就是讀到的內容,文件描述符1是標准輸出,用write將讀到的len個字符寫到標准輸出文件。如果len的值等於零,那麼標志著遇到文件尾。然後進入下一輪的for循環。

這是一個典型的忙等待例子。如果另一個進程向數據文件pppd.log尾部追加數據的間隔為3秒,那麼,進程tailf0就會在3秒間隔內不停歇地檢查文件pppd.log的尾部是否增加了數據。可以想象這種檢查會每秒執行了成百上千次。這種忙等待的方法浪費了CPU的處理時間。在多任務系統中是不可取的。

C語言中函數sleep(sec)可以將當前進程設為睡眠狀態,睡眠一段時間,然後醒來。注意不要把C語言應用程序中的sleep()和內核中用於進程控制的原語sleep()混淆。上述的程序中增加sleep(1),在每次while循環將文件尾部所有新增數據輸出到標准輸出之後,讓進程睡眠1秒鐘,然後,再進入下輪for循環,檢查文件尾部是否有新增數據。修改後的程序為:

$ awk '{print NR, $0}' tailf.c

1 #include <fcntl.h>

2 main(int argc, char **argv)

3 {

4 unsigned char buf[1024];

5 int len, fd;

6 if ( (fd = open(argv[1], O_RDONLY)) < 0) {

7 perror("Open file");

8 exit(1);

9 }

10 lseek(fd, 0, SEEK_END);

11 for(;;) {

12 while ( (len = read(fd, buf, sizeof buf)) > 0)

13 write(1, buf, len);

14 sleep(1);

15 }

16 }

$ cc tailf.c -o tailf

$ ./tailf /usr/adm/pppd.log

這樣修改之後,就不再是忙等待。進程會在它生命周期內的絕大多數時間因sleep調用而處於睡眠狀態,不再瘋狂搶奪CPU。帶來的問題是,最糟糕的情況下文件增長了1秒鐘之後才被顯示出來。在這個應用中,這個性能已經足夠滿足要求。盡管如此,這種定期輪詢的方法也不是一種最好的辦法。如果有一種“等待直到事件發生”的機制是最好的。

【例7-6】 使用fork創建子進程。

$ cat fork1.c

int a;

int main(int argc, char **argv)

{

int b;

printf("[1] %s: BEGIN\n", argv[0]);

a = 10;

b = 20;

printf("[2] a+b=%d\n", a + b);

fork();

a += 100;

b += 100;

printf("[3] a+b=%d\n", a + b);

printf("[4] %s: END\n", argv[0]);

}

上述程序在執行時,進程執行到fork()系統調用,創建出一個新的進程。從執行a += 100開始,父子進程各有自己一套執行流在獨立的執行。這條語句操作的是用戶數據區中的變量a,父子進程操作的都是各自私有的數據,互不影響。

b += 100;訪問用戶堆棧區的數據b,同樣的,父子進程互不影響。

printf需要在當前終端上打印,該庫函數最終引用文件描述符1,文件描述符是進程系統數據段裡的內容,在創建新進程的時候,子進程抄寫的跟父進程一樣。因此,子進程也是在當前的終端上打印。而不是輸出到別的終端上。

最後一個printf引用的argv是main函數的實參,在堆棧區內,argv[0]指針指向的存儲空間是堆棧棧底存放的一些數據,盡管父子進程輸出的結果完全相同,但卻是父子進程各自引用自己私有的堆棧段中數據的結果。因為子進程堆棧段抄寫的跟父進程一樣,而且新進程創建後父子進程都未修改它們,所以輸出結果會相同。

下面是上述程序的執行結果:

$ ./fork1

[1] ./fork1: BEGIN

[2] a+b=30

[3] a+b=230

[4] ./fork1: END

[3] a+b=230

[4] ./fork1: END

【例7-7】 fork創建子進程,父子進程執行不同的程序段。

$ cat fork2.c

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

{

int p;

printf("[1] %s: BEGIN\n", argv[0]);

p = fork();

if (p > 0) {

printf("[2] Parent, p=%d\n", p);

} else if (p == 0) {

printf("[3] Child, p=%d\n", p);

} else {

perror("Create new process");

}

printf("[4] My PID %d, Parent's PID %d\n", getpid(), getppid());

printf("[5] %s: END\n", argv[0]);

}

系統調用fork()返回值-1,那麼,創建新進程失敗。程序中的getpid()和getppid()是兩個系統調用,分別用於獲取進程在內核中PCB結構裡記錄的當前進程的PID號和當前進程的父進程的PID號。用戶程序無法直接訪問內核數據,必須使用系統調用。類似的系統調用還有一些,例如:getuid()獲得當前進程的用戶標識號,等等。

上述程序的執行輸出如下:

$ ./fork2

[1] ./fork2: BEGIN

[3] Child, p=0

[4] My PID 20796, Parent's PID 23950

[5] ./fork2: END

[2] Parent, p=20796

[4] My PID 23950, Parent's PID 30320

[5] ./fork2: END

fork返回值大於0,是子進程的PID;子進程的fork返回值為0。內核在實現fork調用時,首先初始化創建一個新的proc結構,然後復制父進程環境(包括user結構和內存資源)給子進程。

61 /* 消費過程:消費掉進程私有緩沖區buf中數據,一個真正的消費過程可能 */

62 /* 需要較長時間,例如:打印或通過低速網絡向遠程發送數據庫查詢結果,等等 */

63 sleep(1 + random() % 5);

64 }

65

66 void consumer(void)

67 {

68 for(;;) {

69 P(FULL);

70 P(MUTEX);

71 /* 將環行隊列中緩沖區內容復制到進程私有緩沖區buf中 */

72 strcpy(buf, q->buf[q->head]);

73 printf("Consumer %d Read Buffer #%d, Data: %s\n",

74 getpid(), q->head, q->buf[q->head]);

75 q->head = (q->head + 1) % N;

76 V(MUTEX);

77 V(EMPTY);

78 consume_data(); /* 消費過程:消費掉進程私有緩沖區buf中的數據 */

79 }

80 }

81

82 int main(int argc,char **argv)

83 {

84 int shm_id;

85

86 srandom(getpid());

87 sem_id = semget(SEM_KEY, 0, 0); /* 獲取已創建好的信號量組ID */

88 if (sem_id == -1) {

89 perror("Get Semaphore ID");

90 exit(1);

91 }

92 shm_id = shmget(SHM_KEY, 0, 0); /* 獲取共享內存段的ID */

93 if (shm_id == -1) {

94 perror("Get Semaphore ID");

95 exit(1);

96 }

97 q = (QUEUE *)shmat(shm_id, 0, 0);/* 獲取指向共享內存段的指針 */

98 if ((int)q == -1) {

99 perror("Attach Share Memory");

100 exit(1);

101 }

102

103 if (strstr(argv[0], "consumer"))

104 consumer();

105 if (strstr(argv[0], "producer"))

106 producer();

107 }

$ ln -s producer-consumer producer

$ ln -s producer-consumer consumer

$ cc ctl.c -o ctl

$ cc producer-consumer.c -o producer-consumer

$ ./ctl create

Create Semaphores: OK

Initialize Semaphores: OK

Create Share memory: OK

Initialize QUEUE: OK

$ ./producer & ./consumer & ./producer & ./producer & ./consumer &

[1] 21736

[2] 31342

[3] 24322

[4] 29164

[5] 27446

Producer 21736 Write Buffer #0, Data: PRODUCER-21736 ID-0

Consumer 31342 Read Buffer #0, Data: PRODUCER-21736 ID-0

Producer 24322 Write Buffer #1, Data: PRODUCER-24322 ID-0

Consumer 27446 Read Buffer #1, Data: PRODUCER-24322 ID-0

Producer 29164 Write Buffer #2, Data: PRODUCER-29164 ID-0

Consumer 31342 Read Buffer #2, Data: PRODUCER-29164 ID-0

Producer 21736 Write Buffer #3, Data: PRODUCER-21736 ID-1

Consumer 27446 Read Buffer #3, Data: PRODUCER-21736 ID-1

Producer 24322 Write Buffer #4, Data: PRODUCER-24322 ID-1

Producer 29164 Write Buffer #5, Data: PRODUCER-29164 ID-1

Consumer 31342 Read Buffer #4, Data: PRODUCER-24322 ID-1

Producer 21736 Write Buffer #6, Data: PRODUCER-21736 ID-2

Consumer 27446 Read Buffer #5, Data: PRODUCER-29164 ID-1

Producer 24322 Write Buffer #7, Data: PRODUCER-24322 ID-2

Producer 29164 Write Buffer #0, Data: PRODUCER-29164 ID-2

Consumer 31342 Read Buffer #6, Data: PRODUCER-21736 ID-2

Producer 21736 Write Buffer #1, Data: PRODUCER-21736 ID-3

Producer 24322 Write Buffer #2, Data: PRODUCER-24322 ID-3

【例7-32】 同一個程序的多個進程訪問同個磁盤文件產生錯誤的例子。

設某航空公司把UNIX用於售票系統,假設使用文件sanya記錄飛往三亞的某航班的空座位數,文件中含有4個字節,存儲一個二進制的四字節整數。售票程序booking的源程序如下:

$ awk '{printf("%2d %s\n",NR,$0)}' booking.c

1 #include <stdio.h>

2 #include <fcntl.h>

3 int main(void)

4 {

5 int fd, cnt;

6 if ((fd = open("sanya", O_RDWR)) == -1) {

7 perror("Open file");

8 exit(1);

9 }

10 for(;;) {

11 printf("按回車鍵售出一張票, 按Ctrl-D退出 . . .");

12 if (getchar() == EOF) exit(0);

13 lseek(fd, SEEK_SET, 0);

14 read(fd, &cnt, sizeof(int));

15 if (cnt > 0) {

16 printf("飛往三亞機票, 票號:%d\n", cnt);

17 cnt--;

18 lseek(fd, SEEK_SET, 0);

19 write(fd, &cnt, sizeof(int));

20 } else

21 printf("無票\n");

22 }

23 }

在售票窗口的終端上執行程序booking售出一張票。設有兩個售票窗口,每個窗口使用一個UNIX終端,都啟動程序booking,這樣,系統中就有兩個售票進程,設為進程A和進程B。那麼,這個售票程序就是危險的。設當前sanya文件中記錄的機票數為1,那麼,在進程A執行完第14行的read後,進程A的私有變量cnt的值為1,文件sanya內容尚未被改寫,仍為1,執行第15行的判斷之前,正巧發生了進程調度,操作系統調度進程B,進程B在執行期間一直執行第12行至22行,由於進程B執行時文件sanya的內容仍為1,這樣,進程B把最後一張票賣出,並把文件sanya的內容改寫為0。隨後,UNIX再次調度進程A,恢復A的現場,進程A的私用變量cnt仍然為1,繼續執行第15行至第21行,這樣進程A又把最後一張票賣出,並把文件sanya的內容改寫為0,盡管文件中的內容已經為0。出現一張票賣給兩個人的情況,顯然這是不允許的。

【例7-33】 使用文件鎖修改前面的訂票程序,並設計專門的查詢程序。

修改後的訂票程序為新的booking2.c,程序中使用互斥鎖。專門用於查詢的程序為query.c,使用共享鎖,允許多進程的同時查詢。

在這兩個程序中,訪問文件中的記錄前執行上鎖操作,fcntl執行的上鎖操作會使得進程在記錄已被鎖定的前提下被阻塞而睡眠等待。訪問完畢後,解鎖。根據需要使用互斥鎖或共享鎖。

使用文件鎖鎖定磁盤文件中的一個區域,由於是磁盤文件操作,從上鎖到解鎖的時間一般都很短。程序中增加了sleep(3)故意延長鎖定時間段,在程序調試階段,這種方法經常被使用,人為制造訪問沖突。在多個不同的終端上,分別運行query和booking2的多個進程,那麼,就可以看到文件鎖的作用。

為了簡化程序,源程序中省略了很多錯誤條件下的處理,這在實用軟件中是不允許的。

$ awk '{printf("%2d %s\n",NR,$0)}' booking2.c

1 #include <stdio.h>

2 #include <fcntl.h>

3

4 int main(void)

5 {

6 int fd, cnt;

7 struct flock lock;

8

9 if ((fd = open("sanya", O_RDWR)) == -1) {

10 perror("Open file");

11 exit(1);

12 }

13

14 for(;;) {

15 printf("按回車鍵售出一張票, 按Ctrl-D退出 . . .");

16 if (getchar() == EOF) exit(0);

17

18 lock.l_type = F_WRLCK;

19 lock.l_whence = SEEK_SET;

20 lock.l_start = 0;

21 lock.l_len = sizeof(int);

22 if (fcntl(fd, F_SETLKW, &lock) == -1) exit(1);

23 printf("進程%d獲得寫鎖\n", getpid());

24

25 lseek(fd, SEEK_SET, 0);

26 read(fd, &cnt, sizeof(int));

27 sleep(3); /* 故意拖延時間,觀察多進程互斥情況 */

28 if(cnt>0) {

29 printf("飛往三亞機票, 票號:%d\n", cnt);

30 cnt--;

31 lseek(fd, SEEK_SET, 0);

32 write(fd, &cnt, sizeof(int));

33 } else

34 printf("無票\n");

35

36 printf("進程%d開鎖(寫鎖)\n\n", getpid());

37 lock.l_type = F_UNLCK;

38 lock.l_whence = SEEK_SET;

39 lock.l_start = 0;

40 lock.l_len = sizeof(int);

41 fcntl(fd, F_SETLKW, &lock);

42 }

43 }

$ awk '{printf("%2d %s\n",NR,$0)}' query.c

1 #include <stdio.h>

2 #include <fcntl.h>

3

4 int main(void)

5 {

6 int fd, cnt;

7 struct flock lock;

8

9 if ((fd = open("sanya", O_RDONLY)) == -1) {

10 perror("Open file");

11 exit(1);

12 }

13

14 for(;;) {

15 printf("按回車鍵查詢, 按Ctrl-D退出 . . .");

16 if (getchar() == EOF) exit(0);

17

18 lock.l_type = F_RDLCK;

19 lock.l_whence = SEEK_SET;

20 lock.l_start = 0;

21 lock.l_len = sizeof(int);

22 if (fcntl(fd, F_SETLKW, &lock) == -1) exit(1);

23 printf("進程%d獲得讀鎖\n", getpid());

24

25 lseek(fd, SEEK_SET, 0);

26 read(fd, &cnt, sizeof(int));

27 sleep(3); /* 故意拖延時間,以觀察多進程互斥情況 */

28 printf(" 還剩%d張票\n", cnt);

29

30 printf("進程%d開鎖(讀鎖)\n\n", getpid());

31 lock.l_type = F_UNLCK;

32 lock.l_whence = SEEK_SET;

33 lock.l_start = 0;

34 lock.l_len = sizeof(int);

35 fcntl(fd, F_SETLKW, &lock);

36 }

37 }

【例7-34】 打印出文件的所有已上鎖區域。

下面的一段程序是這個功能的一種簡單實現。

$ awk '{printf("%2d %s\n",NR,$0)}' lockls.c

1 #include <stdio.h>

2 #include <fcntl.h>

3 #include <sys/stat.h>

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

5 {

6 int fd, cnt;

7 struct flock lock;

8 struct stat st;

9 off_t start = 0;

10

11 if (argc < 2) {

12 printf("Usage: %s <filename>\n", argv[0]);

13 exit(1);

14 }

15 if ((fd = open(argv[1], O_RDWR)) == -1) {

16 perror("Open file");

17 exit(1);

18 }

19 fstat(fd, &st);

20

21 printf(" TYPE OFFSET LENGTH PID\n");

22 while (start < st.st_size) {

23 lock.l_type = F_WRLCK;

24 lock.l_whence = SEEK_SET;

25 lock.l_start = start;

26 lock.l_len = 0;

27 if(fcntl(fd, F_GETLK, &lock) == -1) {

28 perror("get lock");

29 exit(1);

30 }

31 if (lock.l_type == F_UNLCK) break;

32 printf("%s %08x %6d %5d\n",

33 lock.l_type==F_RDLCK?"RDLCK":"WRLCK",

34 lock.l_start, lock.l_len, lock.l_pid);

35 if (lock.l_len == 0) break;

36 start = lock.l_start + lock.l_len;

37 }

38 }

$ cc lockls.c -o lockls

$ lockls mydata

TYPE OFFSET LENGTH PID

WRLCK 000032c8 1208 25600

RDLCK 0002e6f0 296 25550

WRLCK 00038648 1600 31754

RDLCK 000534d0 576 30632

RDLCK 0006e488 4150 28344

這樣的輸出結果還不能令人滿意。因為還需要手工根據PID,查找ps命令輸出中的PID號和CMD的對應,找出進程對應的程序名。一個小小技巧可以解決這個問題。流編輯sed命令可以編輯輸出流中的內容,進行字符串替換。這樣,根據ps -e命令的輸出格式,用awk生成sed流替換需要的命令文件,編寫一段腳本程序,就可以根據PID號查出對應程序名。下面的腳本程序還允許利用文件通配符等功能,腳本程序執行時可以指定多個命令行參數。利用UNIX多個命令之間的組合,可以提供很多便利的功能。UNIX的使用風格崇尚多個命令共同合作提供更強的功能。在設計命令時,應當兼顧到這些可能的用法,所以,上述程序把PID號放在最末尾,便於使用正則表達式的$符選中行尾的單詞進行流編輯的替換。

$ ps -e | head -3

PID TTY TIME CMD

1 - 0:37 init

2430 - 0:00 ssa_daemon

$ chmod u+x lkls; cat lkls

#!/bin/sh

# 內置變量$$是執行腳本文件的shell進程的PID號

SEDCMDS=/usr/tmp/$$.sed

ps -e | awk '{printf("s/ %s$/ %s %s/g\n",$1,$1,$4)}'>$SEDCMDS

for name

do

echo "[File: $name]"

./lockls $name | sed -f $SEDCMDS

done

rm $SEDCMDS

$ ./lkls mydata

[File: mydata]

TYPE OFFSET LENGTH PID CMD

WRLCK 000032c8 1208 25600 wlock

RDLCK 0002e6f0 296 25550 rlock

WRLCK 00038648 1600 31754 wprog

RDLCK 000534d0 576 30632 rprog

RDLCK 0006e488 4150 28344 lk

Copyright © Linux教程網 All Rights Reserved