Operating System Concepts with java 项目: Shell Unix 和历史特点

时间:2022-06-25 22:18:27

线程间通信,fork(),waitpid(),signal,捕捉信号,用c执行shell命令,共享内存,mmap

实验要求:

1.简单shell: 通过c实现基本的命令行shell操作,实现两个函数,main()和setup().

setup读取用户的下一条指令(最多80个字符),然后分解为独立的标记,并执行,用户按ctrl+D后,程序终止.

    Main函数打印提示符COMMAND->,等待用户输入命令,如果用户命令以” &”结尾,则并发执行,否则,父进程需等待子进程

2.创建历史特性: 将命令编号,允许用户访问最近10个输入的命令,当用户按下Ctrl+C时,系统列出这些命令,用户输入”r x”可以运行命令,如果x存在,则输出执行最近的以x为前缀的命令,否则,输出执行最近的命令.如果该即将执行的命令是错误命令,输出用户提示,不把这条命令加入历史缓存中.

 

一.处理Ctrl+C,Ctrl+D信号:这里使用signal()

signal() 转自百度百科

表头文件#include<signal.h>
功 能:设置某一信号的对应动作
函数原型:void (*signal(int signum,void(* handler)(int)))(int);
或者:typedef void (*sig_t)( int );
sig_t signal(int signum,sig_t handler);
参数说明
第一个参数signum指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。
第二个参数handler描述了与信号关联的动作,它可以取以下三种值:
(1)一个无返回值的函数地址
此函数必须在signal()被调用前申明,handler中为这个函数的名字。当接收到一个类型为signum的信号时,就执行handler 所指定的函数。这个函数应有如下形式的定义:
void func(int sig);
(2)SIG_IGN
这个符号表示忽略该信号,执行了相应的signal()调用后,进程会忽略类型为sig的信号。
(3)SIG_DFL
这个符号表示恢复系统对信号的默认处理。
 

二:用c执行shell命令,有3种方法:

  1.system(),继承环境变量,不安全

  2.popen()建立管道,继承环境变量,不安全

  3.exec族函数+fork()

  这里使用exec+fork

  exec族函数共有6个,功能是执行对应的文件,并且输入附加参数,如果执行成功直接结束,如果不成功会返回-1,接着执行下面的代码,错误信息保存在errno中

  本次使用的是execvp,int execvp(const char *file, char *const argv[]);

  其中file传入shell命令,argv是一个c字符串数组,该数组argv[0]是file,arg[1]....arg[n]是分割开的命令参数,arg[n+1]为空指针NULL,(n是命令参数个数),如"ls -l",则应传入file="ls",argv[3]={"ls","-l",NULL}

exec函数族:转载自:http://blog.csdn.net/aile770339804/article/details/7443921

  #include <uniSTd.h>

  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 *path, char *const argv[], char *const envp[]);

  exec函数族装入并运行程序pathname,并将参数arg0(arg1,arg2,argv[],envp[])传递给子程序,出错返回-1。在exec函数族中,后缀l、v、p、e添加到exec后,所指定的函数将具有某种操作能力有后缀:

Operating System Concepts with java 项目: Shell Unix 和历史特点

  其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。

  其实我们留心看一下这6个函数,可以发现前3个函数都是以execl开头的,后3个都是以execv开头的。

  首先来比较前两个函数execv和execl。execv开头的函数是把参数以"char
*argv[]"这样的形式传递命令行参数。而execl开头的函数采用了我们更容易习惯的方式,把参数一个一个列出来,然后以一个NULL表示结束,也
可以写成(char *)0。

  其次紧跟着的2个以p结尾的函数execlp和execvp。与其他几个函数相比,除execlp
和execvp之外的4个函数都要求,它们的第1个参数path必须是一个完整的路径,如"/bin/ls";而execlp和execvp的第1个参数
file可以简单到仅仅是一个文件名,如"ls",这两个函数可以自动到环境变量PATH制定的目录里去寻找。

  最后两个函数execle和execve,都使用了char
*envp[]来传递环境变量。在全部6个函数中,只有execle和execve需要传递环境变量,其它的4个函数都没有这个参数,这并不意味着它们不
传递环境变量,这4个函数将把默认的环境变量不做任何修改地传给被执行的应用程序。而execle和execve会用指定的环境变量去替代默认的那些。

  最后要强调一点,大家在平时的编程中,如果用到了exec函数族,一定记得要加错误判断语句。因为与其他系统调用比起来,exec很容易受伤,被执行文件的位置,权限等很多因素都能导致该调用的失败。最常见的错误是:

  1. 找不到文件或路径,此时errno被设置为ENOENT;

  2. 数组argv和envp忘记用NULL结束,此时errno被设置为EFAULT;

  3. 没有对要执行文件的运行权限,此时errno被设置为EACCES。

  因为如果成功了execvp会直接退出,所以execvp应该在由fork产生的子进程中运行

 fork入门知识 转载自http://blog.csdn.net/jason314/article/details/5640969

一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
    一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

我们来看一个例子:

  1. /*
  2. *  fork_test.c
  3. *  version 1
  4. *  Created on: 2010-5-29
  5. *      Author: wangth
  6. */
  7. #include <unistd.h>
  8. #include <stdio.h>
  9. int main ()
  10. {
  11. pid_t fpid; //fpid表示fork函数返回的值
  12. int count=0;
  13. fpid=fork();
  14. if (fpid < 0)
  15. printf("error in fork!");
  16. else if (fpid == 0) {
  17. printf("i am the child process, my process id is %d/n",getpid());
  18. printf("我是爹的儿子/n");//对某些人来说中文看着更直白。
  19. count++;
  20. }
  21. else {
  22. printf("i am the parent process, my process id is %d/n",getpid());
  23. printf("我是孩子他爹/n");
  24. count++;
  25. }
  26. printf("统计结果是: %d/n",count);
  27. return 0;
  28. }

运行结果是:
    i am the child process, my process id is 5574
    我是爹的儿子
    统计结果是: 1
    i am the parent process, my process id is 5573
    我是孩子他爹
    统计结果是: 1
    在语句fpid=fork()之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的几乎完全相同,将要执行的下一条语句都是if(fpid<0)……
    为什么两个进程的fpid不同呢,这与fork函数的特性有关。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
    1)在父进程中,fork返回新创建子进程的进程ID;
    2)在子进程中,fork返回0;
    3)如果出现错误,fork返回一个负值;

在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。

引用一位网友的话来解释fpid的值为什么在父子进程中不同。“其实就相当于链表,进程形成了链表,父进程的fpid(p 意味point)指向子进程的进程id, 因为子进程没有子进程,所以其fpid为0.
    fork出错可能有两种原因:
    1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
    2)系统内存不足,这时errno的值被设置为ENOMEM。
    创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
    每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。
    fork执行完毕后,出现两个进程,

Operating System Concepts with java 项目: Shell Unix 和历史特点

有人说两个进程的内容完全一样啊,怎么打印的结果不一样啊,那是因为判断条件的原因,上面列举的只是进程的代码和指令,还有变量啊。

执行完fork后,进程1的变量为count=0,fpid!=0(父进程)。进程2的变量为count=0,fpid=0(子进程),这两个进程的变量
都是独立的,存在不同的地址中,不是共用的,这点要注意。可以说,我们就是通过fpid来识别和操作父子进程的。
    还有人可能疑惑为什么不是从#include处开始复制代码的,这是因为fork是把进程当前的情况拷贝一份,执行fork时,进程已经执行完了int count=0;fork只拷贝下一个要执行的代码到新的进程。

三.进程间通信

  程序需要记录执行命令是否成功,而exec的结果在fork的子进程内,所以使用mmap匿名映射同一个文件实现共享内存

mmap()及其相关系统调用--转载自http://fengtong.iteye.com/blog/457090

mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。

注:实际上,mmap()系统调用并不是完全为了用于共享内存而设计的。它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作。而Posix或系统V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。

1、mmap()系统调用形式如下:

void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )

数fd为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进行
的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间通信)。len是映射到调用进程地址空间的字节数,它从
被映射文件开头offset个字节开始算起。prot 参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) ,
PROT_WRITE (可写), PROT_EXEC (可执行),
PROT_NONE(不可访问)。flags由以下几个常值指定:MAP_SHARED , MAP_PRIVATE ,
MAP_FIXED,其中,MAP_SHARED ,
MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用。offset参数一般设为0,表示从文件头开始映射。参数addr指定文件应被映射
到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始
地址为该值的有效地址。这里不再详细介绍mmap()的参数,读者可参考mmap()手册页获得进一步的信息。

2、系统调用mmap()用于共享内存的两种方式:

(1)使用普通文件提供的内存映射:适用于任何进程之间;此时,需要打开或创建一个文件,然后再调用mmap();典型调用代码如下:

	fd=open(name, flag, mode);
if(fd<0)
...

ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); 通过mmap()实现共享内存的通信方式有许多特点和要注意的地方,我们将在范例中进行具体说明。

(2)使用特殊文件提供匿名内存映射:适用于具有亲缘关系的进程之间;由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用
fork()。那么在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就可以通过映射区
域进行通信了。注意,这里不是一般的继承关系。一般来说,子进程单独维护从父进程继承下来的一些变量。而mmap()返回的地址,却由父子进程共同维护。

对于具有亲缘关系的进程实现共享内存最好的方式应该是采用匿名内存映射的方式。此时,不必指定具体的文件,只要设置相应的标志即可,参见范例2。

3、系统调用munmap()

int munmap( void * addr, size_t len )
该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将导致段错误发生。

程序代码:

#include <signal.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include <sys/wait.h>
#include <queue>
using namespace std;
#define BUFFER_SIZE 50
#define MAXLINE 80 char buffer[BUFFER_SIZE];
struct mes{//用于保存命令的参数,参数个数,是否并发执行,执行后是否错误
char** args;//参数,arg[0]为命令本身
int len;//参数个数
bool bg;//是否并发
int erno;//执行后是否错误
mes():args(NULL),len(0),bg(false){}
mes(char **_args,int _len,bool _bg):\
args(_args),len(_len),bg(_bg){}
void show(){//打印命令
if(erno==-1)cout<<"ERROR:";
for(int i=0;i<len;i++)cout<<args[i]<<" ";
if(bg)cout<<"&"<<endl;
else cout<<endl;
}
~mes(){
for(int i=0;i<len&&args[i];i++){
delete[] args[i];
args[i]=NULL;
}
delete[] args;
args=NULL;
}
};
queue <mes*> rec,quetmp;//rec用于存储最近10条命令,quetmp只是在遍历rec时暂时存储rec中的变量
static int quelen=0;//rec内变量个数
static int reclen=0;//已执行命令总条数
static int srclen=0;//已经读取字符串长度
bool read2(char * des,char *src,bool init){//从字符串src中读取非空字符串des,失败返回false, 从同一个src字符串读取应该一次性读完, init为true时代表重新读取新的src字符串
if(init)srclen=0;
bool fl=false;
for(;src[srclen];srclen++){
if(src[srclen]!=' '&&src[srclen]!='\t'&&src[srclen]!='\n'){
fl=true;
for(int i=0;src[srclen];srclen++,i++){
if(src[srclen]==' '||src[srclen]=='\t'||src[srclen]=='\n')break;
des[i]=src[srclen];
des[i+1]=0;
}
break;
}
}
return fl;
} void setup(char inputBuffer[],char*args[],bool background){//执行命令inputBuffer –args[1] –arg[2]…,background为true代表并发
int len=0;
for(len=0;len<MAXLINE/2+1&&args[len]!=NULL;len++){}//重新新建args数组,以保证数据形式满足execvp
char **_args=new char*[len+1];
for(int i=0;i<len;i++){
_args[i]=new char[strlen(args[i])+1];
strcpy(_args[i],args[i]);
}
_args[len]=NULL;
mes* pmes=new mes(_args,len,background);
int * p_errno=(int*)mmap(NULL,sizeof(int)*2,PROT_READ|PROT_WRITE,\
MAP_SHARED|MAP_ANONYMOUS,-1,0);//共享内存以便传递子进程的执行结果 int pid=fork();
if(pid==0){
int erno=execvp(inputBuffer,_args);
*p_errno=erno;
_exit(0);//子进程及时退出
}
else{
if(!background){
waitpid(pid,NULL,0);
}
}
pmes->erno=*p_errno;
if(quelen<10){
rec.push(pmes);quelen++;
}
else {
delete rec.front();
rec.pop();
rec.push(pmes);
}
reclen++;
} void handle_SIGINT(int sig){//历史缓存工具
int ci=reclen-quelen,ind=0;
mes* ls[10];
cout<<endl;
while(!rec.empty()){
cout<<++ci<<": ";
rec.front()->show();
quetmp.push(rec.front());
ls[ind++]=rec.front();
rec.pop();
}
  while(!quetmp.empty()){
    rec.push(quetmp.front());quetmp.pop();
  }
cout<<"Exit record input \"q\", repeat command,\
input \"r\" or \"r x\"(x is the prefix of command)"<<endl;
char buff[MAXLINE],buff2[MAXLINE];
int exeNum=ind-1;
while(true){
fgets(buff,MAXLINE,stdin);
read2(buff2,buff,true);
if(strcmp(buff2,"q")==0)break;
if(strcmp(buff2,"r")!=0){
cout<<"No such command"<<endl;
continue;
}
if(read2(buff2,buff,false)){
for(;exeNum>=0;exeNum--){
bool fl=true;
for(int i=0;buff2[i]!=0;i++){
if(buff2[i]!=ls[exeNum]->args[0][i]){
fl=false;
break;
}
}
if(fl)break;
}
if(exeNum<0)cout<<"No such prefix"<<endl;
}
if(exeNum>=0){
ls[exeNum]->show();
if(ls[exeNum]->erno==-1){
cout<<"It is an error command!"<<endl;
}
else {
setup(ls[exeNum]->args[0],ls[exeNum]->args,ls[exeNum]->bg);
}
}
}
cout<<"record has quitted"<<endl;
} void handle_SIGTSTP(int sig){//Ctrl+d退出
write(STDOUT_FILENO,buffer,strlen(buffer));
exit(0);
}
int main(int argc,char * arg[]){
char inputBuffer[MAXLINE],buff[MAXLINE];
bool background;
char * args[MAXLINE/2+1];
memset(args,0,sizeof(args)); signal(SIGINT,handle_SIGINT);
signal(SIGTSTP,handle_SIGTSTP);
while(true){
background=false;
cout<<"COMMAND->";
fgets(inputBuffer,MAXLINE,stdin);//读取拆分命令
int len=0;
while(read2(buff,inputBuffer,len==0)){
if(args[len])delete[] args[len];
args[len++]=new char[strlen(buff)];
strcpy(args[len-1],buff);
}
if(args[len])delete[] args[len];
if(len>0&&args[len-1][0]=='&'){
delete[] args[len-1];
args[len-1]=NULL;
len--;
background=true;
}
setup(args[0],args,background);
for(int i=0;i<len;i++){
delete args[i];
args[i]=NULL;
}
}
return 0;
}