Linux环境下编写简单的Shell

版权声明:本文为博主原创文章,如果喜欢欢迎收藏转载!如有错误,请指出! https://blog.csdn.net/h___q/article/details/83443875

使用进程创建、等待、终止等知识自主实现简单的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("[ahao@AHAOAHA ~ ]& ");//打印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信号。

猜你喜欢

转载自blog.csdn.net/h___q/article/details/83443875
今日推荐