1. 程式人生 > >Linux環境下編寫簡單的Shell

Linux環境下編寫簡單的Shell

使用程序建立、等待、終止等知識自主實現簡單的Linuxshell命令列,Linux環境:CentOS7.3

程序建立

建立程序的方式

  • 通常使用fork()函式來建立一個新程序,新程序為子程序,而原來的程序即為父程序。在Linux中fork()是一個非常重要的函式,它的標頭檔案為<unistd.h>。
  • 通過vfork()也可以來建立一個子程序,它的返回值與fork()相同

fork()的返回值

fork()函式有兩個返回值,它會給子程序返回0,給父程序返回子程序的pid,如果建立子程序失敗,則會返回-1

寫時拷貝

通過fork()建立的子程序與父程序會共享同一份程式碼,因為程式碼段的資料是隻讀的,不會發生寫入,而當父程序與子程序的資料段不發生寫入時,它們的資料段也是共享的,當子程序或父程序任何一方發生寫入時,就會另外開闢自己的資料段,將原來的資料拷貝過去

fork()與vfork()的區別

  • 通過vfork()函式建立的子程序與父程序共享同一塊地址空間,而通過fork()建立的子程序與父程序各自擁有自己獨立的地址空間。
  • 通過vfork()建立程序會保證子程序先執行,等到子程序呼叫exec(程序替換)或exit(退出)之後,父程序才會被執行。

程序終止

程序終止的情況

正常終止(可以通過echo $?來檢視退出碼,echo $?只可檢視最近一次退出的程序的退出碼)

  • 從main主函式返回(return):呼叫main函式執行時,函式會將main的返回值當做exit()的引數
  • 呼叫exit函式:exit()函式的引數即為退出碼
  • _exit

異常退出

  • 通過對程序傳送訊號終止程序

_exit(int status)與exit(int status)的區別

  • _exit()相比exit()而言,退出的更加粗暴直接,而exit()在程序退出之前,還會做許多收尾工作,但在exit()函式中,最終還是會呼叫_exit()。

程序等待

程序等待的意義

  • 子程序退出,如果父程序不採取措施,那麼子程序就會變成一個殭屍程序,而殭屍程序會造成作業系統記憶體洩露
  • 子程序退出時,會返回子程序任務的執行資訊,父程序需要獲取到子程序的退出資訊

程序等待的方式

wait方法&waitpid方法

#include<sys/types.h>
#include<sys/wait.h>

pid_t wait(int* status);
返回值:
	成功返回則返回被等待程序的ID,失敗返回0
引數:
	獲取子程序的退出狀態,不關心則設定為NULL

pid_t waitpid(pid_t pid, int* status, int options);
返回值:
	當waitpid收集到已退出的子程序時waitpid返回被等待程序的ID
	如果設定了選項WNOHANG,而呼叫waitpid發現沒有已退出的子程序可以被收集時,則返回0
	
引數說明:
	pid:
		pid=-1,等待任意一個子程序,與wait效果相同。
		pid>0,等待程序ID與pid相等的子程序。
	status:
		WIFEXITED(status): status若為正常終止子程序返回的狀態,則為真。
		WEXITSTATUS(status): 若WIFEXITED非零,提取子程序提出碼。
	options:
		WNHANG:若pid指定的子程序沒有結束,則waitpid()函式返回0,不予以等待。若正常結束,則返回該子程序的ID。
		
  • 如果子程序已經退出,則呼叫wait/waitpid時,wait/waitpid會立即返回,並且釋放資源,並獲得子程序退出資訊
  • 如果當呼叫wait/waitpid時子程序存在並且正常執行,則程序可能阻塞
  • 如果不存在該子程序,則立即出錯返回

子程序退出資訊status

  • 子程序的退出資訊會被作業系統儲存在status內,當我們使用wait/waitpid時,如果想知道子程序的退出資訊,就必須定義一個int型別的變數,將該變數的地址傳給wait/waitpid,作業系統就會將收集到的子程序的退出資訊儲存在該變數內
  • 當子程序的退出資訊我們不需要時,亦可以傳遞NULL空指標
  • status雖然是一個整型變數,但是它並不是代表一個簡單的整型數字,它可以當作點陣圖來看待
  • status示意圖:(僅研究status的低16位元位)
    iN2rxx.png

程式碼實現簡單的shell

設計思想

  • 建立父程序,父程序的工作為列印shell提示資訊,等待標準輸入輸入命令
  • 當命令輸入之後,父程序建立子程序,子程序的工作為使用exec()函式進行程序替換
  • 在子程序執行的過程中,父程序持續等待子程序,待子程序退出之後,父程序即可繼續迴圈
#include<stdio.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<string.h>
#include<unistd.h>
#define MAX_ARGV 20
#define MAX_CMD 1024
void cut_str(char* argv[],char cmd[])//將輸入的命令分段,儲存至argv[]指標陣列內
{
  char* ptr=cmd;
  int i=0;
  int flag=0;
  while(*ptr!='\0')
  {
    if(*ptr!=' '&&flag==0)
    {
      argv[i++]=ptr;
      flag=1;
    }
    else if(*ptr==' '&&flag==1)
    {
      *ptr='\0'; 
      argv[i++]=ptr+1;
    }
    ptr+=1;
  }
  
  //去掉命令尾部的'\n'
  ptr=argv[i-1];
  while(*ptr!='\0')
  {
    ptr+=1;
  }
  *(ptr-1)='\0';
  
  //以NULL結尾
  argv[i]=NULL;
}

void new_pro(char* argv[])
{
  pid_t id=fork();
  if(-1==id)
  {
    perror("fork");
    exit(-1);
  }
  else if(0==id)//子程序
  {
    execvp(argv[0],argv);
  }
  else//父程序
  {
    int st;//定義st用來儲存子程序的返回資訊
    while(wait(&st)!=id);//使用while迴圈為了確保wait()收集到的子程序是上面所建立的子程序
  }
}
int main()
{
  char cmd[MAX_CMD]={'\0'};//定義陣列來儲存輸入的內容
  char* argv[MAX_ARGV];//定義一個數組來存放argv(execvp()函式的第二個引數)
  while(1)
  {
    printf("[[email protected] ~ ]& ");//列印shell提示符
	
	//此處不能使用scanf()函式來接受,因為scanf()在用“%s”格式輸入字元時,輸入的字串中含有空白字元(space,tab,newline)。字串讀取結束
    fgets(cmd,sizeof(cmd),stdin); //從標準輸入來接受將要執行的命令
    cut_str(argv,cmd);//將輸入的命令切開,並將其的地址存入陣列argv[],最終以NULL結尾
    new_pro(argv);//新程序,在此函式內會建立一個子程序,在子程序執行的時候,父程序會等待子程序,待到子程序執行完畢,父程序才會繼續向下執行
  }
  return 0;
}

封裝fork/wait函式,編寫函式process_create(pid_t* pid,void* fun ,void* arg),func回撥函式就是子程序執行的函式入口,arg是傳遞給func回撥函式的引數

#include<sys/wait.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
void process_create(pid* pid, void func, void* arg)
{
	*pid = fork();
	typedef void (PF*)(void* arg);//重新命名一個返回值為void,只有一個引數為void*函式指標PF,用來對回撥函式強制轉換
	if(-1 == *pid)//建立子程序失敗
	{
		exit(-1);
	}
	else if(0 == *pid)//子程序
	{
		if(NULL == func)//判斷是否傳遞迴調函式地址
		{
			printf("未傳遞迴調函式地址!\n");
			exit(0);
		}
		((PF)func)(arg);//執行func函式,先要進行強制型別轉換
		exit(0);
	}
	else//父程序
	{
		int st;
		while(wait(&st) == *pid);
	}
	
}

fork+exec與system/popen的區別

system函式

#include<stdlib.h>

int system(const char* command);
  • system函式用來在程式中執行shell命令列的命令

system的原始碼

int system(const char *command)
{
    struct sigaction sa_ignore, sa_intr, sa_quit;
    sigset_t block_mask, orig_mask;
    pid_t pid;

    sigemptyset(&block_mask);
    sigaddset(&block_mask, SIGCHLD);
    sigprocmask(SIG_BLOCK, &block_mask, &orig_mask);        //1. block SIGCHLD

    sa_ignore.sa_handler = SIG_IGN;
    sa_ignore.sa_flags = 0;
    sigemptyset(&sa_ignore.sa_mask);
    sigaction(SIGINT, &sa_ignore, &sa_intr);                //2. ignore SIGINT signal
    sigaction(SIGQUIT, &sa_ignore, &sa_quit);                //3. ignore SIGQUIT signal

    switch((pid = fork()))
    {
        case -1:
            return -1;
        case 0:
            sigaction(SIGINT, &sa_intr, NULL); 
            sigaction(SIGQUIT, &sa_quit, NULL); 
            sigprocmask(SIG_SETMASK, &orig_mask, NULL);
            execl("/bin/sh", "sh", "-c", command, (char *) 0);
            exit(127);
        default:
            while(waitpid(pid, NULL, 0) == -1)    //4. wait child process exit
            {
                if(errno != EINTR)
                {
                    break;
                }
            }
    }
}

  • system函式在執行的過程中,在子程序沒有退出前,父程序會一直等待,直到子程序執行結束,所以system是序列執行
  • system對SIGCHLD、SIGINT、SIGQUIT訊號做了處理

popen函式

#include<stdio.h>
FILE* popen(const char* command, const char* type);
int pclose(FILE* stream);

popen()也是執行shell命令並且通過管道和shell命令進行通訊。

popen原始碼

static pid_t    *childpid = NULL;  
                        /* ptr to array allocated at run-time */  
static int      maxfd;  /* from our open_max(), {Prog openmax} */  

#define SHELL   "/bin/sh"  

FILE *  
popen(const char *cmdstring, const char *type)  
{  
    int     i, pfd[2];  
    pid_t   pid;  
    FILE    *fp;  

            /* only allow "r" or "w" */  
    if ((type[0] != 'r' && type[0] != 'w') || type[1] != 0) {  
        errno = EINVAL;     /* required by POSIX.2 */  
        return(NULL);  
    }  

    if (childpid == NULL) {     /* first time through */  
                /* allocate zeroed out array for child pids */  
        maxfd = open_max();  
        if ( (childpid = calloc(maxfd, sizeof(pid_t))) == NULL)  
            return(NULL);  
    }  

    if (pipe(pfd) < 0)  
        return(NULL);   /* errno set by pipe() */  

    if ( (pid = fork()) < 0)  
        return(NULL);   /* errno set by fork() */  
    else if (pid == 0) {                            /* child */  
        if (*type == 'r') {  
            close(pfd[0]);  
            if (pfd[1] != STDOUT_FILENO) {  
                dup2(pfd[1], STDOUT_FILENO);  
                close(pfd[1]);  
            }  
        } else {  
            close(pfd[1]);  
            if (pfd[0] != STDIN_FILENO) {  
                dup2(pfd[0], STDIN_FILENO);  
                close(pfd[0]);  
            }  
        }  
            /* close all descriptors in childpid[] */  
        for (i = 0; i < maxfd; i++)  
            if (childpid[ i ] > 0)  
                close(i);  

        execl(SHELL, "sh", "-c", cmdstring, (char *) 0);  
        _exit(127);  
    }  
                                /* parent */  
    if (*type == 'r') {  
        close(pfd[1]);  
        if ( (fp = fdopen(pfd[0], type)) == NULL)  
            return(NULL);  
    } else {  
        close(pfd[0]);  
        if ( (fp = fdopen(pfd[1], type)) == NULL)  
            return(NULL);  
    }  
    childpid[fileno(fp)] = pid; /* remember child pid for this fd */  
    return(fp);  
}
  • popen在執行的過程中,父程序並沒有等待子程序執行完成,所以popen是並行執行
  • 使用popen函式時,必須在呼叫popen函式之後呼叫pclose函式來回收子程序,否則子程序就會變成一個殭屍程序,會造成作業系統記憶體洩漏
  • popen函式沒有遮蔽SIGCHLD、SIGINT、SIGQUIT訊號。