第八章 进程和程序:编写命令解释器sh

本文详细介绍了Unix进程的概念,通过`ps`命令学习了进程状态及其属性。深入探讨了Shell的角色,阐述了如何运行程序、管理输入输出以及编程。文章还讲解了`fork`、`exec`、`wait`和`exit`系统调用的工作原理,以及如何编写简单的Shell程序。通过对进程生命周期的剖析,展示了进程间的通信和控制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

0.摘要

概念与技巧
-Unix shell的功能
-Unix的进程模型
-如何执行一个程序
-如何创建一个进程
-父进程和子进程之间如何通信
相关的系统调用
-fork
-exec
-wait
-exit
相关命令
-sh
-ps

1.什么是进程

进程就是运行的程序.

2.通过命令ps学习进程
ps会列出当前运行的所有进程
ls会列出当前目录下的文件信息

ps显示进程,其中有pid,每一个进程与一个终端相连,每个进程有自己已经运行的时间

ps -a   打印其他终端以及系统的进程
ps -la  打印更加详细的进程信息
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY      TIME CMD
0 S  1000 18642 18626  0  80   0 - 15028 poll_s pts/1    00:00:00 python
0 S  1000 24240 24199  0  80   0 -  5045 wait   pts/21   00:00:00 man
0 S  1000 24252 24240  0  80   0 -  2848 wait_w pts/21   00:00:00 pager
0 S  1000 25891 23683  0  80   0 -  3775 pause  pts/18   00:00:00 bounce_aio
4 R  1000 25901 19808  0  80   0 -  7663 -      pts/19   00:00:00 ps

S表示当前进程状态,S(sleep),R(run),uid表示所属用户id,PID表示进程id,PPID表示父用户ID,PRI和NI表示进程优先级和niceness级别(这两个值和内核中的cpu进程调度有关)。内核通过这两个值来判断进程什么时候执行。niceness就像在排队的时候让其他用户排在自己的前面。SZ表示占用的内存量,当程序在运行的时候动态的增加内存,会改变进程的大小。 WCHAN列出了睡眠的原因。 ADDR和F已经不再使用,为了兼容性还是需要。-ly显示目前使用的进程。

ps -fa  //格式化的显示信息。

将UID替换成用户,cmd替换成了执行路径。在ps中可以看到许许多多的内部程序。

UID        PID  PPID  C STIME TTY          TIME CMD
shengch+  3322  3249  0 105 pts/1   00:01:28 vim -On shm_ts.c shm_t2.
shengch+  4013  3998  0 105 pts/18  00:00:00 man shmget
shengch+  4025  4013  0 105 pts/18  00:00:00 pager
shengch+  6663  6643  0 105 pts/19  00:00:00 man semctl
shengch+  6675  6663  0 105 pts/19  00:00:00 pager
shengch+  8395  8384  0 105 pts/20  00:00:00 man malloc
shengch+  8407  8395  0 105 pts/20  00:00:00 pager
shengch+ 11109 11092  0 09:26 pts/21   00:00:00 ps -fa

当然也可以打印系统进程

ps -ax   打印每个在系统中的进程.
2.1内存和程序

内存可以容纳内核和进程的空间.
进程代表内存中的一些字节。进程被拆分成多块放到内存中。而内核是连续独立的。
内存中,将程序分割存放在不同的页面中。将内存表示成连续的字节数组也是一种抽象.
使用操作系统的神奇,并不仅仅是它的文件系统把一堆圆形磁盘上连续的簇变成有序组织的树状目录结构,而且以相似的机制,它的进程系统将硅片上的一些位组织成一个进程社会.

3. shell:进程控制和程序控制的一个工具

shell是一个管理进程和运行程序的程序.shell主要的功能:
1.运行程序:可以运行如,grep/date/ls/echo/mail.这里这些命令都是c语言写成,并编译成机器语言.其实shell有点类似于程序启动器(program launcher)
2.管理输入和输出:重定向功能
3.可编程

3.1shell是如何运行程序的

shell执行一个a.out的三个步骤
1.用户键入a.out
2.shell创建一个进程来运行这个程序
3.程序从磁盘被读入到内存
4.程序在它结束的时候退出。

3,2shell的主循环

while(!end_of_input)
get command
execute command
wait for command to finish

为了要写一个shell:
1.建立一个程序
2.建立一个进程
3.等待exit();

3.3一个程序如何运行另一个程序

使用execvp函数来执行
过程:
1.程序调用execvp
2.内核从磁盘将程序载入
3.内核将arglist复制到进程
4.内核调用main(argc,argv)

main()
{
    char *arglist[3];

    arglist[0] = "ls";
    arglist[1] = "-l";
    arglist[2] = 0;
    printf("***About to exec ls***\n");
    execvp("ls",arglist);
    printf("*** ls is done,bye\n");
}

运行结果

***About to exec ls***
total 104
-rwxrwxr-x 1 shengchen shengchen  8704 1222 19:39 a.out
-rwxrwxr-x 1 shengchen shengchen  8712 16 09:57 exec1
-rw-rw-r-- 1 shengchen shengchen   250 16 09:57 exec1.c
-rwxrwxr-x 1 shengchen shengchen  9136 1221 15:27 psh1
-rw-rw-r-- 1 shengchen shengchen   975 1221 15:27 psh1.c
-rwxrwxr-x 1 shengchen shengchen 12080 1222 20:36 psh2
-rw-rw-r-- 1 shengchen shengchen  1314 1222 20:34 psh2.c
-rwxrwxr-x 1 shengchen shengchen  9032 1221 16:15 waitdemo
-rw-rw-r-- 1 shengchen shengchen   569 1221 16:15 waitdemo1.c
-rwxrwxr-x 1 shengchen shengchen  9096 1222 17:03 waitdemo2
-rw-rw-r-- 1 shengchen shengchen   770 1222 17:02 waitdemo2.c
-rwxrwxr-x 1 shengchen shengchen  8984 1221 16:12 waitdemo.c

:第二个printf打印失效了,说明execvp执行后直接返回,并没有接下来执行程序中的代码,调用该函数,内核把对应的命令从文件系统载入,替换了当前执行环境的代码和数据,因此再调用exec之后呢,就执行命令了.不执行原先的程序.

这里需要注意的是,内核将程序载入到进程,这个进程就是当前运行这个程序的进程,这样会覆盖掉之后的信息。
execvp就像是更换大脑一样。

接下来就是编写提示符的shell。一个串一个串的输入到文件中.

#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#define MAXARGLENGTH 100
#define MAXARGNUM 20

void main()
{   
    char argbuf[MAXARGLENGTH];
    char *arglist[MAXARGNUM-1];
    char * makestring(char *);
    int argnum=0;
    int execute(char **);
    while(argnum<MAXARGNUM)
    {
        printf("Args:[%d]?\n",argnum);
        if(fgets(argbuf,MAXARGLENGTH,stdin)&&*argbuf!='\n')
        {
            arglist[argnum++] = makestring(argbuf); //把数据存放到参数列表里面
        }else{
            if(argnum>0)
            {
                arglist[argnum] = NULL; //close the set 
                execute(arglist);
                argnum =0;
            }
        }
    }
}

/*execute the value of the arglist*/
int execute(char **arglist)
{
    execvp(arglist[1],arglist);
    perror("execute error");
    exit(1);
    return 0;
}
/*malloc the virtual memory for the buf*/
char * makestring(char * buf)
{
    char *cp;
    buf[strlen(buf)-1] = '\0'; //set the end of the buf 
    cp = malloc(strlen(buf)+1);
    if(cp ==NULL)
    {
        perror("malloc error");
        exit(-1);
    }
    strcpy(cp,buf);
    return cp;
}

程序存在执行完成就退出的问题,如何让它继续等待下一个命令?

4.如何建立进程来防止退出:fork(一)

fork的执行过程:
1.分配新的内存块和内核数据结构
2.复制原来的进程到新的进程
3.向运行的进程添加新的进程
4.将控制权返回两个进程

子进程返回0,父进程返回孩子进程的id.

4.1父进程等待子进程退出。
pid = wait(&status)  //等待某个状态到来

wait在这里执行通知和通信的作用,并且返回子进程的pid。用来告诉父进程那个结束了,可以位结束的进程继续接下来的执行。
父进程处于wait状态,子进程调用exit()来使父进程退出。当子进程结束,默认调用exit();

/*show how parents pauses until child finishes*/
#include<stdio.h>     
#include<unistd.h>     
#include<stdlib.h>     
#include<sys/wait.h>     
#define DELAY 2     

void child_mode();    
void parent_mode();    
void main()    
{    
    int rf;    
    printf("before fork function\n");    
    rf = fork();    
    if(rf==-1)    
    {    
        perror("fork fail");    
        exit(-1);    
    }else if(rf==0)    
    {    
        child_mode();    
    }else{    
        parent_mode();    
    }    
}    
/*child print process id and the delay*/
void child_mode()    
{    
    printf("this is child %d, and sleep %d\n",getpid(),DELAY);    
    sleep(DELAY);    
}    
/*wait child end, wait return pid of child process*/
void parent_mode()    
{    
    pid_t pid = wait(NULL);    
    printf("this is parent %d, and the child is %d\n ",getpid(),pid);    
}  

子进程与父进程的通信:
wait的目的之一是通知父进程子进程结束,其二是告诉父进程子进程何时结束。
当程序产生错误的时候调用exit分配非零的值,这个值和错误分配相关程序员可以自己定义。
exit中的status参数,由3个部分组成:8bit记录退出值,7bit是记录信号序号(当低7位被kill会产生),另外一个bit致命发生错误并产生内核映像(core dump)

#include<stdio.h>     
#include<unistd.h>     
#include<stdlib.h>     
#include<sys/wait.h>     
#define DELAY 5     

void child_mode();    
void parent_mode();    
void main()    
{    
    int rf;    
    printf("before fork function\n");    
    rf = fork();    
    if(rf==-1)    
    {    
        perror("fork fail");    
        exit(-1);    
    }else if(rf==0)    
    {    
        child_mode();    
    }else{    
        parent_mode();    
    }    
}    

void child_mode()    
{    
    printf("this is child %d, and sleep %d\n",getpid(),DELAY);    
    sleep(DELAY);    
    exit(23);    
}    
void parent_mode()    
{    
    int wait_rv;    
    int child_status;    
    int high_8,bit_7,low_7;    
    wait_rv = wait(&child_status);    
    high_8 = child_status>>8;    /*1111 1111 0000 0000*/
    low_7 = child_status & 0x7f; /*0000 0000 0111 1111*/   
    bit_7 = child_status &0x80;   /*0000 0000 1000 0000*/
    printf("status of child is high8 %d, low_7 is %d, bit_7 is %d",high_8,low_7,bit_7);    
}    

当测试的时候,可以在程序运行时,kill当前运行程序的pid,之后会在父进程中返回status的低7位的值.
具体流程

具体流程

5.实现一个shell : psh2.c
/*prompting shell version 2*/

#include<stdio.h>
#include<signal.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>
#define MAXARGS 20  /*cmdline args*/
#define ARGLEN 100  /*token length*/

int main()
{   
    int execute(char **);
    char * arglist[MAXARGS];
    char buf[ARGLEN];
    int numargs = 0;
    char* makebuf(char *); 
    while(numargs < MAXARGS)
    {   
        printf("arg number is %d\n",numargs);
        if(fgets(buf,ARGLEN,stdin)&& *buf!='\n')
        {   
            printf("buf is %s",buf);
            arglist[numargs++] = makebuf(buf);
        }else{
            if(numargs)//try the numargs>0
            {   
                arglist[numargs] =NULL;
                execute(arglist); 
                numargs =0; 
            }   

        }   
    }   
    return 0;
}
int execute(char **arglist)
{
    int pid,status;
    pid = fork();
    switch(pid)
    {   
        case -1: 
            perror(" fork failed");
            break;
        case 0:
            execvp(arglist[0],arglist);
            perror("execute failed");
            exit(1);
            break;
        default:
            signal(SIGINT,SIG_IGN);
            while(wait(&status)!=pid);
            printf("child exited with status %d, %d\n",status>>8,status &0377);
    }
}
/*copy string from stack to heap*/
char * makebuf(char *buf)
{
    buf[strlen(buf)-1] = '\0';  //替换最后的结尾的'\n'
    char * cp = malloc(strlen(buf)+1);
    if(cp==NULL)
    {
        perror("no memory\n");
        exit(1);
    }
    strcpy(cp,buf);
    return cp;
}

键盘发送给所有的ctrl+c的SIGINT信号给所有连接的进程。如何让自己编写的shell不被杀死,只杀死运行的程序

6.思考:用进程编程

1.execvp/exit就像call/return
(1)call/return:调用函数,传入参数,执行操作,返回值
(2)exec/exit:通过fork/exec来执行传入参数,完成之后exit退出,exit中的参数n被传出,到达wait(),得到对应的status
2.全局变量和fork/exec
fork/exit, exit/wait

7.exit和exec的其他细节

进程死亡:exit和_exit
exit退出调用由atexit和on_exit注册的函数,执行当前系统定义的其他与exit相关的操作。然后调用_exit。

已经死亡但是没有给exit赋值的进程称为幽灵(zombie)进程。ps列出并标记位defunct.
_exit()小结如下。
1.关闭所有文件描述符和目录描述符
2.将该进程的PID置为init进程的PID.
3.如果父进程调用wait或waitpid来等待子进程结束,则通知父进程
4.向父进程发送SIGCHLD

如果父进程在子进程之前退出,那么子进程将继续运行,而不会称为“孤儿”,它们将是init进程的”子女”,有了国家监管的感觉。注意,即使父进程没有调用wait,内核也会向它发送SIGCHLD消息。

exec家族

execvp是调用execve,它本身只是一个库函数,真正的系统调用是execve.具体参看手册

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值