Unix環境高級編程(六)進程控制
本章介紹Unix的進程控制,包括進程創建,執行程序和進程終止,進程的屬性,exec函數系列,system函數,進程會計機制。
1、進程標識符
每一個進程都有一個非負整數標識的唯一進程ID。ID為0表示調度進程,即交換進程,是內核的一部分,也稱為系統進程,不執行任何磁盤操作。ID為1的進程為init進程,init進程不會終止,他是一個普通的用戶進程,需要超級用戶特權運行。獲取標識符函數如下:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); //調用進程的進程ID
pid_t getppid(void); //調用進程的父進程ID
gid_t getgid(void); //調用進程的實際組ID
gid_t getegid(void); //調用進程的有效組ID
uid_t getuid(void); //調用進程的實際用戶ID
uid_t geteuid(void); //調用進程的有效用戶ID
2、fork函數
一個現有的進程可以調用fork函數創建一個新進程。函數原型為:pid_t fork(void)。有fork創建的新進程稱為子進程,fork函數調用一次返回兩次。子進程的返回值為0,父進程的返回值為新子進程的ID。子進程和父進程舉行執行fork調用後的指令,子進程是父進程的副本。子進程獲得父進程的數據空間、棧和隊的副本,父子進程不共享這些存儲空間部分,共享正文段。子進程相當於父進程克隆了一份自己。 創建新進程成功後,系統中出現兩個基本完全相同的進程,這兩個進程執行沒有固定的先後順序,哪個進程先執行要看系統的進程調度策略。
fork出錯可能有兩種原因:
1)當前的進程數已經達到了系統規定的上限,這時errno的值被設置為EAGAIN。
2)系統內存不足,這時errno的值被設置為ENOMEM。
註意的是:父進程設置的文件鎖不會被子進程繼承。
寫個程序,創建一個子進程,在子進程中改變變量的值,然後父子進程同時輸出變量的值,看看有什麽變化,同時輸出子進程的標識符信息。程序如下:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <sys/types.h>
5 #include <unistd.h>
6 #include <errno.h>
7 //全局變量
8 int global = 100;
9 char buf[]="a write to stdout.\n";
10
11 int main()
12 {
13 int var;
14 pid_t pid;
15
16 var = 90;
17 write(STDOUT_FILENO,buf,sizeof(buf)-1);
18 //創建一個子進程
19 pid = fork();
20 if(pid == -1) //出錯
21 {
22 perror("fork() error");
23 exit(-1);
24 }
25 if(pid == 0) //子進程
26 {
27 printf("This is child process.\n");
28 printf("Child process id is:%d\n",getpid());
29 printf("Father process id is:%d\n",getppid());
30 //子進程修改數據
31 global++;
32 var++;
33 }
34 if(pid > 0) //父進程
35 sleep(3); //等待子進程先執行
36 printf("pid = %d,ppid =%d,global=%d,var=%d\n",getpid(),getppid(),global,var);
37 return 0;
38 }
程序執行結果如下:
從結果可以看出子進程擁有自己的數據空間,不與父進程共享數據空間。
vfork函數的調用序列和返回值與fork相同,但是vfork並不將父進程的地址空間完全復制到子進程中,在調用exec和exit之前在父進程的空間中運行。vfork保證子進程先運行,在它調用exec或exit之後父進程才可能被調度運行。
進程終止最後都會執行內核中的同一段代碼,為相應的進程關閉所有打開描述符,釋放它所使用的存儲器等。子進程可以通過exit函數通知父進程是如何終止的,父進程調用wait或waitpid函數可以獲取終止狀態。子進程是在父進程調用fork後產生的,如果父進程在子進程之前終止,則將子進程的父進程改變為init進程,保證每一個進程都有一個父進程。一個已經終止,但其父進程尚沒有對其進行善後處理的進程稱為僵死進程(zombie)。由init進程領養的子進程不會變成僵死進程,因為init進程在子進程終止的時候會調用一個wait函數取得子進程的終止狀態。
當一個進程正常或者異常終止的時,內核就像其父進程發送SIGCHLD信號。調用wait和waitpid函數可能發生的情況:(1)如果所有子進程都還在運行,則阻塞;(2)如果一個子進程已經終止,正等待父進程獲取進程終止狀態,則取得孩子的終止狀態立刻返回;(3)若果沒有任何子進程,則立即出錯返回。如果在任意時刻調用wait,則進程可能會阻塞。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status); //在一個子進程終止前,wait使調用者阻塞
pid_t waitpid(pid_t pid, int *status, int options); //可以使調用者不阻塞
#include <sys/types.h>
#include <sys/resource.h>
#include <sys/wait.h>
pid_t wait3(int *status, int options,struct rusage *rusage);
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
寫個程序,子進程給出退出狀態,父進程通過wait和waitpid獲取退出狀態。程序如下:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5 #include <sys/types.h>
6 #include <signal.h>
7
8 int main()
9 {
10 pid_t pid;
11 int status;
12 if((pid = fork()) == -1)
13 {
14 perror("fork() error");
15 exit(-1);
16 }
17 if(pid == 0)
18 exit(0);
19 if(wait(&status) == pid)
20 printf("child normal exit,exit status=%d\n",WEXITSTATUS(status));
21 if((pid = fork()) == -1)
22 {
23 perror("fork() error");
24 exit(-1);
25 }
26 if(pid == 0)
27 abort();
28 if(waitpid(pid,&status,0) == pid)
29 printf("child abnormal termination,signal number=%d\n",WTERMSIG(status));
30 }
程序執行結果如下:
waitpid函數中的pid參數取值情況:
pid=-1 等待任一子進程,此時相當於wait
pid>0 等待期進程ID與pid相等的子進程
pid==0 等待期組ID等於調用進程組ID的任一個子進程
pid<-1 等待其組ID等於pid絕對值的任一子進程
另外提供了一種避免僵死進程的方法:調用fork兩次。
exec函數,用fork函數創建子進程後,子進程往往要調用一種exec函數以執行另外一個程序。當調用exec函數時,該進程執行的程序完全替換為新進程,exec函數不新建進程,只是用一個全新的程序替換了當前的正文、數據、堆和棧段。執行完之後,進程ID不會改變。在進程間通信的時候,經常需要調用exec函數啟動另外一個例程。函數原型如下:
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *file, char *const argv[],char *const envp[]);
其中l表示列表(list),v標識矢量(vector)。execl、execlp、execle中每個命令行參數都是一個單獨參數,這種參數以空指針結尾。execv、execvp、execve命令行參數是一個指針數組。e標識環境變量,傳遞參數。寫個程序進行測試,程序分為兩部分,exec調用的程序,exec執行程序。程序如下:
exec調用程序如下,可執行文件名稱為exectest,存放在/home/anker/Programs目錄下。
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int main(int argc,char *argv[])
5 {
6 int i;
7 char **ptr;
8 extern char **environ;
9
10 for(i=0;i<argc;++i)
11 printf("argv[%d]=%s\n",i,argv[i]);
12 for(ptr=environ;*ptr!=0;ptr++)
13 printf("%s\n",*ptr);
14 exit(0);
15 }
exec執行程序如下:存放在/home/anker/Programs目錄下。
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
char *env_init[] = {"LENGTH=100","PAHT=/tmp",NULL};
int main()
{
pid_t pid;
char *path = "/home/anker/Programs/exectest";
char *filename = "exectest";
char *argv[3]= {0};
argv[0] = "exec";
argv[1] = "test";
if((pid=fork()) == -1)
{
perror("fork() error");
exit(-1);
}
else if(pid == 0)
{
printf("Call execle:\n");
execle(path,argv[0],argv[1],(char*)0,env_init);
}
waitpid(pid,NULL,0);
if((pid=fork()) == -1)
{
perror("fork() error");
exit(-1);
}
else if(pid == 0)
{
printf("Call execve:\n");
execve(filename,argv,env_init);
}
waitpid(pid,NULL,0);
exit(0);
}
執行結果如下:
system函數,在程序中執行一個命令字符串。例如system("date > file")。函數原型如下:
#include <stdlib.h>
int system(const char *command);、
system函數實現中調用了fork、exec和waitpid函數,因此有三種返回值。system函數實現一下,沒有信號處理。程序如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
int mysystem(const char * cmdstring);
int main()
{
int status;
status = mysystem("date");
if(status < 0)
{
printf("mysystem() error.\n");
}
status = mysystem("who");
if(status < 0)
{
printf("mysystem() error.\n");
}
exit(0);
}
int mysystem(const char * cmdstring)
{
pid_t pid;
int status;
if(cmdstring == NULL)
return 1;
if((pid = fork()) == -1)
{
status = -1;
}
else if(pid == 0) //在子進程中調用shell腳本
{
execl("/bin/sh","sh","-c",cmdstring,(char *)0);
_exit(127);
}
else
{
while(waitpid(pid,&status,0) < 0)
{
if(errno != EINTR)
{
status = -1;
break;
}
}
}
return status;
}
程序執行結果如下:
system函數可以設置用戶的ID,這是一個安全漏洞。
進程會計:啟用後,每當進程結束時候內核就寫一個會計記錄,包括命令名、所使用的CPU時間總量、用戶ID和組ID、啟動時間等。accton命令啟動會計處理,會計記錄寫到指定的文件中,Linux中位於/var/account/ pacct。會計記錄結構定義在<sys/acct.h>頭文件中。
#define ACCT_COMM 16
typedef u_int16_t comp_t;
struct acct {
char ac_flag; /* Accounting flags */
u_int16_t ac_uid; /* Accounting user ID */
u_int16_t ac_gid; /* Accounting group ID */
u_int16_t ac_tty; /* Controlling terminal */
u_int32_t ac_btime; /* Process creation time(seconds since the Epoch) */
comp_t ac_utime; /* User CPU time */
comp_t ac_stime; /* System CPU time */
comp_t ac_etime; /* Elapsed time */
comp_t ac_mem; /* Average memory usage (kB) */
comp_t ac_io; /* Characters transferred (unused) */
comp_t ac_rw; /* Blocks read or written (unused) */
comp_t ac_minflt; /* Minor page faults */
comp_t ac_majflt; /* Major page faults */
comp_t ac_swaps; /* Number of swaps (unused) */
u_int32_t ac_exitcode; /* Process termination status(see wait(2)) */
char ac_comm[ACCT_COMM+1];/* Command name (basename of last executed command; null-terminated) */
char ac_pad[X]; /* padding bytes */
};
用戶標識,用getlogin函數獲取用戶的登錄名。函數原型如下:char *getlogin(void)。
進程時間:墻上時鐘時間、用戶CPU時間和系統CPU時間。任一個進程都可以調用times函數獲取它自己及已終止子進程時間。進程時間操作函數及結構如下:
#include <sys/times.h>
clock_t times(struct tms *buf);
struct tms {
clock_t tms_utime; /* user time */
clock_t tms_stime; /* system time */
clock_t tms_cutime; /* user time of children */
clock_t tms_cstime; /* system time of children */
};
總結:通過本章的學習。完全的了解Unix的進程控制,掌握了fork、exec簇、wait和waitpid進程控制函數。另外學習了system函數和進程會計。了解了解釋器文件及其工作方式,用戶標識和進程時間。
Unix環境高級編程(六)進程控制