操作系统实验

实验五 进程和线程同步

实验目的

加深对进程概念的理解,认识并发执行的实质,分析进程征用资源的现象,学习进程、线程互斥的方法。 

实验内容

线程是计算机中独立运行的 小单位,运行时占用很少的系统资源。由于每个线程占用的CPU时间是由系统分配的,因此可以把线程看成操作系统分配CPU 时间的基本单位。在用户看来,多个线程是同时执行的,但从操作系统调度上看,各个线程是交替执行的。系统不停地在各个线程之间切换,每个线程只有系统分配给它的时间片内才能取得CPU的控制权,执行线程中的代码。 

注意,这里只是针对单CPU单核的情况,在多CPU多核的主机上,多个线程是可以同时运行的。Linux操作系统是支持多线程的,他在一个进程内生成了许多个线程。一个进程可以拥有一至多个线程。 

虽然线程在进程内部共享地址空间、打开的文件描述符等资源。但是线程也有其私有的数据信息,包括:线程号:(thread ID):每个线程都有一个唯一的线程号与其对应 

寄存器(包括程序计数器和堆栈指针)、堆栈、信号掩码、优先级、线程私有的存储空间。Linux系统支持POSIX多线程接口,称为pthread(Posix Thread的简称)。编写Linux下的多线程应用程序,需要使用头文件pthread.h,链接时需要使

用库libpthread.a 

创建线程: 

线程创建函数pthread_create

前面的程序实例都是单线程的。单线程的程序都是按照一定的顺序执行的,如果在主线程里面创建线程,程序就会在创建线程的地方产生分支,变成两个程序执行。这似乎和多进程一样,其实不然。子进程是通过拷贝父进程的地址空间来实现的;而线程与进程内的线程共享程序代码,一段代码可以同时被多个线程执行。 

线程的创建通过函数pthead_create来完成,该函数的声明如下: 

#include<pthread.h>

int  pthread_create(pthread_t *thread, pthread_attr_t * attr,void*(*start_routine)(void *),void

*arg);

注意:线程创建成功时,pthread_create函数返回0若不为0则说明创建进程失败常见的错误码为EAGAINEINVAL。前者表示系统限制创建新的线程,例如,线程数目过多后者表示第2个参数代表的线程属性非法。线程创建成功后,新创建的线程开始运行第3个参数所指向的函数,原来的线程继续运行。 

pthead_create函数的第2个参数attr是一个指向pthread_attr_t结构体的指针,该结构体指明待创建线程的属性。创建线程的其他系统函数 

函数 

说明 

pthread_t pthread_selfvoid 

获取本线程的线程ID

int pthread_equal(pthread_t threadpthread_thread2)

判断两个线程ID是否指向同一线程 

int pthread_once(pthread_once_t *once_control,void(*init_routine)(void))

用来保证init_routine 线程函数在进程中仅执行一次 

下面通过5-1讲述线程的创建过程。 

5-1 createthread.c

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>  

#include <pthread.h> int * thread(void * arg)

{

  pthread_t newthid;   newthid = pthread_self();   printf("this is a new thread, thread ID = %d\n", newthid);   return NULL;

} int main(void)

{   pthread_t thid;

 

  printf("main thread ,ID is %d\n",pthread_self());   if(pthread_create(&thid, NULL, (void *)thread, NULL) != 0) {     printf("thread creation failed\n");     exit(1);

  }

  exit(0);

}

编译并运行$gcc-o createthread createthread.c –lpthread 

          $./createthread 

程序首先打印出主线程的ID,然后打印新创建线程的ID。在某些情况下,函数执行次数要被限制为一次,这种情况下就要使用 pthread_once函数。下面通过例5-2说明。该实例中创建两个线程,两个线程分别通过pthread_once调用同一个函数,结果被调用的函数只被执行了一次。例5-2 oncerun.c

#include <stdio.h>  

#include <pthread.h>  pthread_once_t once = PTHREAD_ONCE_INIT;  void run(void)  

{     printf("Fuction run is running in thread %d\n",pthread_self());  

}  void * thread1(void *arg)  

{     pthread_t thid=pthread_self();     printf("Current thread's ID is %d\n", thid);     pthread_once(&once,run);     printf("thread1 ends\n");  

}  void * thread2(void *arg)  

{     pthread_t thid=pthread_self();     printf("Current thread's ID is %d\n", thid);     pthread_once(&once, run);     printf("thread2 ends\n");   

}  int main(void)  

{     pthread_t thid1,thid2;  

 

   pthread_create(&thid1,NULL,thread1,NULL);     pthread_create(&thid2,NULL, thread2,NULL);     sleep(3);     printf("main thread exit! \n");     exit(0);

} 请编译并运行,通过结果分析线程的执行情况。线程同步线程大的特点就是资源的共享性,然而资源共享中的同步问题是多线程编程的难点。Linux系统提供了多种方式处理线程间的同步问题,其中 常用的有互斥锁、条件变量和异步信号。下面将重点介绍这3种同步技术的使用。 

1.互斥锁 

互斥锁通过锁机制来实现线程的同步。在同一时刻它通常只允许一个线程执行关键部分的代码。下表列举了操作互斥锁的几个函数。这些函数均声明在头文件pthread.h中。 

互斥锁函数 

函数 

功能 

pthread_mutex_int函数 

初始化一个互斥锁 

pthread_mutex_destroy函数 

注销一个互斥锁 

pthread_mutex_lock函数 

加锁,如果不成功,阻塞等待 

pthread_mutex_unlock函数 

解锁 

pthread_mutex_trylock函数 

测试加锁,如果不成功则立即返回,错误码为EBUSY

使用互斥锁前必须先进行初始化操作。初始化有两种方式,一种是静态赋值

法,将宏结构常量PTHREAD_MUTEX_INITIALIZER赋给互斥锁,操作语句如下: pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;

另外一种方式是通过pthread_mutex_init函数初始化互斥锁,该函数原型如下: int pthread_mutex_init (pthread_mutex_t *mutex,const pthread_mutexattr_t

*mutexattr);    

函数中的参数mutexattr表示互斥锁的属性,如果为NULL则使用默认属性。互斥锁的属性及意义见下表: 

属性值 

意义 

PTHREAD_MUTEX_TIMED_NP

普通锁:当一个线程加锁后,其余请求锁的线程形成等待队列,解锁后按优先级获得锁 

PTHREAD_MUTEX_RECUSIVE_NP

嵌套锁:允许一个线程对同一个锁多次加锁,

并通过多次unlock解锁。如果是不同线程请求,则在解锁时重新竞争 

PTHREAD_MUTEX_ERRORCHECK_NP

检错锁:在同一个线程请求同一个锁的情况下,返回 EDEADLK ,否则执行的动作与类型

PTHREAD_MUTEX_TIMED_NP

PTHREAD_MUTEX_ADAPTIVE_NP

适应锁:解锁后重新竞争 

初始化以后,就可以给互斥锁加锁了。加锁有两个函数:pthread_mutex_lock() pthread_mutex_trylock()。它们的原型如下: 

int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex);

pthread_mutex_lock()加锁时,如果mutex已经被锁住,当前尝试加锁的线程就会阻塞,直到互斥锁被其他线程释放。当pthread_mutex_lock函数返回时,说明互斥锁已经被当前线程成功加锁。pthread_mutex_trylock函数则不同,如果 mutex已经被加锁,它将立即返回,返回的错误码为EBUSY,而不是阻塞等待。 

注意:加锁时,不论哪种类型的锁,都不可能被两个不同的线程同时得到,其中一个必须等待解锁。在同一进程中的线程,如果加锁后没有解锁,则其他线程将无法再获得该锁。 

函数pthread_mutex_unlock函数解锁时,要满足两个条件:一是互斥锁必须处于加锁状态,二是调用本函数的线程必须是给互斥加锁的线程。解锁后如果有其它线程在等待互斥锁,等待队列中的第一个线程将获得互斥锁。   

当一个互斥锁使用完毕后,必须进行清除。清除互斥锁使用函数 pthread_mutex_destroy,该函数原型如下: 

int pthread_mutex_destroy(pthread_mutex_t *mutex);

清除一个互斥锁意味着释放它所占用的资源。清除锁时要求当前处于开放状态,若锁处于锁定状态,函数返回EBUSY,该函数成功执行时返回0。由于在Linux 中,互斥锁并不占用内存,因此pthread_mutex_destroy()除了解除到互斥锁的状态以外没有其他操作。 

2.条件变量 

条件变量是利用线程间共享的全局变量进行同步的一种机制。条件变量宏观上类似if语句,符合条件就能执行某段程序,否则只能等待条件成立。 

使用条件变量主要包括两个动作:一个等待使用资源的线程等待“条件变量被设置为真”;另一个线程在使用完资源后“设置条件为真”,这样就可以保证线程间的同步了。这样就存在一个关键问题,就是要保证条件变量能被正确的修改,条件变量要受到特殊的保护,实际使用中互斥锁扮演着这样一个保护者的角色。Linux也提供了一系列对条件变量操作的函数,如下表所示:操作条件变量的函数 

          函数 

功能 

pthread_cond_init函数 

初始化条件变量 

pthread_cond_wait函数 

基于条件变量阻塞,无条件等待 

pthread_cond_timedwait函数 

阻塞直到指定时间发生,计时等待 

pthread_cond_signal函数 

解除特定线程的阻塞,存在多个等待线程时按入队顺序激活其中一个 

pthread_con_broadcast函数 

解除所有的线程阻塞 

pthread_cond_destroy函数 

清除条件变量 

与互斥锁一样,条件变量的初始化也有两种方式,一种是静态赋值法,将宏结构常量PTHREAD_COND_INITIALIZER赋予互斥锁,操作语句如下: 

pthread_cond_t cond=PTHREAD_COND_INITIALIZER; 另一种方式是使用pthread_cond_init,它的原型如下: 

int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr); 其中,cond_attr参数是条件变量的属性,由于其并没有得到实现,所以它的

值通常是NULL 

等待条件成立有两个函数:pthread_cond_waitpthread_cond_timedwait。它

们的原型如下: 

int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);

int pthread_cond_timedwaitpthread_cond_t *cond, pthread_mutex_t

*mutex,const struct timespec *abstime;

pthread_cond_wait函数释放由mutex指向互斥锁,同时使当前线程关于cond 指向的条件变量阻塞,直到条件被信号唤醒。通常条件表达式在互斥锁的保护下求值,如果条件表达式为假,那么线程基于条件变量阻塞。当一个线程改变条件变量的值时,条件变量获得一个信号,使得等待条件变量的线程退出阻塞状态。 pthread_con_timewait函数和pthread_cond_wait函数用法类似,差别在于 pthread_con_timewait函数将阻塞直到条件变量获得信号或者经过abstime指定时间,也就是说,如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待。 

线 程 被 条 件 变 量 阻 塞 后 , 可 通 过 函 数 pthread_cond_signal

pthread_cond_broadcast激活,它们的原型如下: int pthread_cond_signal(pthread_con_t *cond); int pthread_cond_broadcast(pthread_cond_t *cond);

pthread_cond_signal()激活一个等待条件成立的进程,存在多少个等待线程

时,按如对顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程。当一个条件变量不再使用时,需要将其清除。清除一个条件变量通过调用

pthread_cond_destroy()实现,函数原型如下: 

int pthread_cond_destroy(pthread_cond_t *cond)

pthread_cond_destroy函数清除由cond指向的条件变量。注意:只有在没有线程等待该条件变量的时候才能清除这个条件变量,否则返回EBUSY 

通过例5-3来演示条件变量的使用方法,在例子中,有两个线程被启动,并

等待同一个条件变量。     

5-3 condition.c

#include <stdio.h>  #include <unistd.h>

#include <pthread.h>   

 

pthread_mutex_t  mutex; pthread_cond_t cond;  

 

void *thread1(void *arg)  

{  

  pthread_cleanup_push (pthread_mutex_unlock, &mutex);  

 

  while(1) {      printf ("thread1 is running\n");      pthread_mutex_lock (&mutex);      pthread_cond_wait (&cond, &mutex);      printf ("thread1 applied the condition\n");      pthread_mutex_unlock (&mutex);      sleep (4);  

  }  

 

  pthread_cleanup_pop (0);  

}  

 

void *thread2(void *arg)  

{    while(1) {      printf ("thread2 is running\n");      pthread_mutex_lock (&mutex);      pthread_cond_wait (&cond, &mutex);      printf ("thread2 applied the condition\n");      pthread_mutex_unlock (&mutex);      sleep (1);  

  }  

}

 

int main(void)  {    pthread_t tid1, tid2;  

 

  printf ("condition variable study! \n");    pthread_mutex_init (&mutex, NULL);    pthread_cond_init (&cond, NULL);    pthread_create (&tid1, NULL, (void *) thread1, NULL);    pthread_create (&tid2, NULL, (void *) thread2, NULL);  

 

  do {      pthread_cond_signal (&cond);    } while (1);  

 

  sleep (50);  

  pthread_exit (0);  

}

编译执行程序、结果是什么??请分析thread1thread2同步运行的情况。   

3.异步信号 

Linux操作系统中,线程是在内核外实现的,它不像进程那样在内核中实现。Linux线程本质上是轻量级的进程。信号可以被进程用来进行互相通信,一个进程通过信号通知另一个进程发生了某事件,比如该进程所需要的输入数据已经就绪。线程同进程一样也可以接受和处理信号,信号也是一种线程同步手段。 

信号(SIGINTSIGIO)与任何线程都是异步的,也就是说信号到达线程的时间是不定的。如果有多个线程可以接受异步信号,则只有一个被选中,如果并发的多个同样的信号被送到一个进程,每一个将被不同的线程处理。如果所有的线程都屏蔽该信号,则这些信号将被挂起,直到有信号解除屏蔽来处理它们。 

Linux多线程扩展函数中有三个函数用于处理异步信号: 

int pthread_kill(pthread_t threadid,int signo);

int pthread_sigmask(int how,const sigset_t *newmask,sigset_t *oldmask); int sigwait(const sigset_t *set,int *sig);  

其中,函数 pthread_kill 用来向特定的线程发送信号 signo 。函数 pthread_sigmask用来设置线程的信号屏蔽码,但对不允许屏蔽的Cancel信号和不允许响应的Restart信号进行了保护。函数sigwait用来阻塞线程,等待set中指定的信号之一到达,并将到达的信号存入*sig中。     

作业

  1. 编写一个多线程程序:要求主线程创建3个子线程,3个子线程在执行时都修改一个它们的共享变量,观察共享变量的值,看看可以得出什么结论。 
  2. 编写一个多进程多线程的程序:要求创建4个子进程,每个子进程都分别创建两个线程,进程和线程的功能不做要求,可以只提供简单的打印语句。 

 

实验六 进程间通信

实验目的

了解Linux系统的通信机制,管道、消息队列、信号量的创建与使用、共享内存。   

实验内容

进程间通信概述: 

进程的地址空间是各自独立的,因此进程之间交互数据必须采用专门的通信机制,特别是在大型的应用系统中,往往需要多个进程相互协作共同完成一个任务,这就需要使用进程间通信(Internet Process ConnectionIPC)编程技术。 

Linux下进程间通信的方法基本上是从UNIX平台继承来的。Linux操作系统不但继承了system V IPC通信机制,还集成了基于套接字(socket)的进程间通信机制。前者是贝尔实验室对UNIX早期的进程间通信手段的改进和扩充,其通信的进程局限于单台计算机内;后者则突破了这一局限,通信的进程可以运行在不同的主机上,也就是进行网络通信。 

管道(pipe):管道是一种半双工的通信方式,数据只能单方向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。 

有名管道(named pipe):有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。 

信号量(semophore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止当某进程正在访问共享资源时,其它进程也访问该资源。因此主要作为进程间以及同一进程内不同线程之间的同步手段。 

消息队列(message queue):消息队列是由消息的链表,存放着内核中并由消息队列标识符标识。消息队列克服了信号传递信息量少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 

信号(signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已发生。 

共享内存(shared memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建但是多个进程都可以访问。共享内存是快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。 

套接字(socket):套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。 

6.1 管道管道:管道是一种两个进程间进行进行通信的机制。管道这一特点决定了其

使用的局限性。 

数据只能由一个进程流向另一个进程;如果要进行全双工通信,需要建立两个管道。管道只能用于父子进程或者兄弟进程间的通信,也就是管道只能用于具有亲缘关系的进程间的通信,无亲缘关系的进程不能使用管道。   

1.管道的创建: 

Linux下创建管道可以通过函数pipe来完成。该函数如果调用成功返回0,并且数组中将包含两个新的文件描述符;如果有错误发生,返回-1。该函数原型如下: 

#include<unistd.h> int pipe(int fd[2])

注意:管道只有通信双方同时存在时,才可以进行通信。例6-1,说明管道

的创建、以及对管道的读写是如何进行的。 

6-1 pipe.c

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <sys/types.h>

#include <unistd.h>

 

/*读管道*/

void read_from_pipe (int fd)

{         char message[100];         read (fd,message,100);         printf("read from pipe:%s",message);

}

 

/*写管道*/

void write_to_pipe (int fd)

{         char *message = "Hello, pipe!\n";         write (fd, message,strlen(message)+1);

}

 

int main(void)

{         int     fd[2];         pid_t   pid;         int     stat_val;

 

        if (pipe (fd))         {                 printf ("create pipe failed!\n");                 exit (1);

        }

 

        pid = fork();         switch (pid)         {                 case -1:

                        printf ("fork error!\n");                         exit (1);                 case 0:

                        /*子进程关闭fd1*/                         close (fd[1]);                         read_from_pipe (fd[0]);                         exit (0);                 default:

                        /*父进程关闭fd0*/                         close (fd[0]);                         write_to_pipe (fd[1]);                         wait (&stat_val);                         exit (0);

        }

 

        return 0;

}

程序说明:在该程序中,父进程向管道中写数据,子进程从管道中获取数据。可以看到,对管道的读写和对一般文件的读写没什么区别。 

程序的运行结果如下: 

$./pipe

$Hello,pipe!

注意:必须在系统调用fork()之前调用pipe(),否则子进程将不会继承管道的

文件描述符。  

管道的一种常见法是:在父进程创建子进程后向子进程传递参数。例如,一个应用软件有一个主进程和很多个不同的子进程。主进程创建子进程后,在子进程调用exec函数执行一个新程序前,通过管道给即将执行的程序传递命令行参数,子进程根据传来的参数进行初始化或其他操作。下面通过例6-2和例6-3来演示这种用法,这个程序包括一个主进程程序和一个子进程中要执行的新程序。 

6-2 monitor.c

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <sys/types.h>

#include <sys/stat.h>

 

int main(int arg, char *argv[],char ** environ)

{

  int fd[2];   pid_t pid;   int stat_val;

   

  if (arg < 2)

  {

    printf("wrong parameters \n");     exit(0);

  }

  if (pipe(fd))

  {

    perror("pipe failed");     exit(1);

  }

 

  pid = fork();   switch (pid)

  {

    case -1:

      perror ("fork failed!\n");

      exit(1);     case 0:       close (0);       dup (fd[0]);

      execve ("ctrlprocess",(void *)argv,environ);

      exit (0);     default:

      close(fd[0]);

      /*将命令行第一个参数写进管道*/

      write (fd[1], argv[1], strlen(argv[1]));

      break;

  }

  wait (&stat_val);   exit (0); }   6-3 ctrlprocess.c #include <stdio.h>

#include <unistd.h>

 

int main(int arg, char * argv[])

{

 int   n;  char  buf[1024];

 

  while (1)

  {

    if ((n = read (0, buf, 1024)) > 0)  // 标准输入stdin的描述符为0

    {

      buf[n] = '\0';

      printf ("ctrlprocess receive: %s\n",buf);

 

      if (!strcmp (buf,"exit"))

        exit(0);

 

      if (!strcmp (buf, "getpid"))

      {

        printf ("My pid:%d\n", getpid());

        sleep (3);         exit (0);

      }

    }

  }

} 程序说明:主进程向管道中写一个命令行参数,子进程从标准输入里面读出该参数,进行相应的操作,首先编译ctlprocess.c 

$gcc –o ctrlprocess ctrlprocess.c 在编译主程序monitor.c:

$gcc –o monitor monitor.c

分别运行./monitor exit ./monitor getpid,结果分别如下: 

$./monitor exit ctrlprocess receive exit $./monitor getpid ctrlprocess receive getpid

My pid:17031

可见,被监控子进程接受监控主进程命令,执行不同的操作。这在实际项目中是经常见到的,监控进程启动加载多个具有不同功能的子进程,并通过管道的形式向子进程传递参数。 

6.2 消息队列 

消息队列是一个存放在内核中的消息链表,每个消息队列由消息队列标识符标识。与管道不同的是消息队列存放在内核中,只有在内核重启或者显式地删除一个消息队列时,该消息队列才会被真正的删除。 

操作消息队列时,需要用到一些数据结构,熟悉这些数据结构是掌握消

息队列的关键。 

1.消息的创建与读写创建消息队列消息队列是随着内核的存在的,每个消息队列在系统范围内对应唯一的键值。要获得一个消息队列的描述符,只需提供该消息队列的键值即可,该键值通常由函数ftok返回。该函数定义在头文件sys/ipc.h中: 

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

key_t ftok(const char *pathname,int proj_id);

ftok函数根据pathnameproj_id这两个参数生成惟一的键值。该函数执行成

功会返回一个键值,失败返回-1。例如6-4是一个获取键值的例子:例6-4 ftok.c

#include <stdio.h>

#include <sys/types.h>

#include <sys/ipc.h>

  

int main( void )

{

 int i;

 for ( i = 1; i <= 5; i++ )    printf( "key[%d] =  %ul \n", i, ftok( ".", i) );    exit(0);

}

u 程序运行的结果是什么?请简单分析。 

注意:参数pathname在系统中一定要存在且进程有权访问,参数proj_id的取值范围为1~255ftok()返回的键值可以提供给函数msggetmsgget()根据这个键值创建一个新的消息队列或者访问一个已存在的消息队列。msgget定义在头文件 sys/msg.h中: 

int msgget(key_t key,int msgflg);

msgget的参数key即为ftok函数的返回值。msgflg是一个标志参数。以下是

msgflg的可能取值。 

IPC_CREATE:如果内核中不存在键值与key相等的消息队列,则新建一个消息队列;如果存在这样的消息队列,返回该消息队列的描述符。 

IPC_EXCL:和IPC_CREATE一起使用,如果对应键值的消息队列已经存在,则出错,返回-1。注意:IPC_EXCL单独使用是没有任何意义的。 

该函数如果调用成功返回一个消息队列的描述符,否则返回-1 

2. 写消息队列 

创建了一个消息队列后,就可以对消息队列进行读写了。函数msgsnd用于向

消息队列发送(写)数据。该函数定义在头文件sys/msg.h中: 

int msgsnd(int msqid,struct msgbuf *msgp,size_t msgsz,int magflg);

msgsnd各参数含义如下: msqid:函数向msgid标识的消息队列发送一个消息。 

msgp:msgp指向发送的消息。 

msgsz:要发送的消息的大小,不包含消息类型占用4个字节。 

magflg:操作标志位。可以设置为0或者IPC_NOWAIT。如果msgflg0,则当消息队列msgsnd函数成功返回0,失败返回-1.常见错误有:EAGAIN,说明消息队列已满;EIDRM,说明消息队列已被删除;EACCESS,说明无权访问消息队列。例6-5演示了如何向消息队列发送消息。例6-5 sendmsg.c

#include <stdio.h>

#include <stdlib.h>

#include <sys/ipc.h>

#include <sys/msg.h>

 

#define BUF_SIZE 256

#define PROJ_ID   32

#define PATH_NAME "."

int main(void)

{

  /*用户自定义消息缓冲*/

  struct mymsgbuf { long msgtype; char ctrlstring[BUF_SIZE];

} msgbuffer;

int   qid;    /*消息队列标识符*/ int   msglen; key_t msgkey;

 

/*获取键值*/

if((msgkey = ftok (PATH_NAME, PROJ_ID)) == -1)

{    perror ("ftok error!\n"); exit (1);

}

 

/*创建消息队列*/

  if((qid = msgget (msgkey, IPC_CREAT|0660)) == -1)

  {

 perror ("msgget error!\n");  exit (1);

 }

/*填充消息结构,发送到消息队列*/

msgbuffer. msgtype = 3; strcpy (msgbuffer.ctrlstring , "Hello,message queue"); msglen = sizeof(msgbuffer) - 4; if(msgsnd (qid, &msgbuffer, msglen, 0) == -1)

{  perror ("msgget error!\n");  exit (1);

}

 

exit(0);

}

编译运行该程序,从输出的结果可以观察到哪些信息?? 

3.读消息队列 

消息队列中放入数据后,其他进程就可以读取其中的消息了。读取消息的系统调用为msgrcv(),该函数定义在头文件sys/msg.h中,其原型如下: 

int msgrcv(int msqid,struct msgbuf *msgp,size_t msgsz,long int msgtyp,int

msgflg);

该函数有5个参数,含义如下: msqid:消息队列描述符。 

msgp:读取的消息存储到msgp指向的消息结构中。 msgsz:消息缓冲区的大小。 msgtyp:为请求读取的消息类型。 

msgflg Msgflg

IPC_NOWAIT,IPC_EXCEPT,IPC_NOERROR 此 时 错 误 码 为 ENOMSG

IPC_EXCEPT,与msgtyp配合使用,返回队列中第一个类型不为msgtyp的消息; IPC_NOERROR,如果队列中满足条件的消息内容大于所请求的msgsz字节,则把该消息截断,截断部分将被丢弃。 

调用msgrcv函数的时候,成功会返回读出消息的实际字节数,否则返回-1

常见错误码有:E2BIG,标识消息的长度大于msgszEIDRM,标识消息队列已被删除;EINVAL,说明msqid无效或msgsz小于0.

下面通过例6-5来说明如何从消息队列读消息,该例读取例6-4写入消息队列

的数据。 

例:6-5 rcvmsg.c

#include <stdio.h>

#include <stdlib.h>

#include <sys/ipc.h>

#include <sys/msg.h>

 

#define BUF_SIZE 256

#define PROJ_ID   32

#define PATH_NAME "."

 

int main(void)

{

  /*用户自定义消息缓冲区*/

  struct mymsgbuf{     long msgtype;     char ctrlstring[BUF_SIZE];

  } msgbuffer;

  int   qid;     /*消息队列标识符*/

  int   msglen;   key_t msgkey;

 

  /*获取键值*/

  if((msgkey = ftok(PATH_NAME, PROJ_ID)) == -1)

  {

    perror("ftok error!\n");     exit(1);

  }

 

  /*获取消息队列标识符*/

  if((qid = msgget(msgkey, IPC_CREAT|0660)) == -1)

  {

    perror ("msgget error!\n");     exit (1);

  }

 

  msglen = sizeof(struct mymsgbuf) - 4;   if (msgrcv(qid, &msgbuffer, msglen, 3, 0) == -1)  /*读取数据*/

  {

    perror ("msgrcv error!\n");     exit (1);

  }

  printf("Get message %s\n", msgbuffer.ctrlstring);

 

  exit(0);

}

编译运行,并分析程序结果。 

6.3 信号量 

信号量是一个计数器,常用于处理进程或线程的同步问题,特别是对临界资源的访问的同步。临界资源可以简单地理解为在某一时刻只能由一个进程或线程进行操作的资源,这里的资源可以是一段代码、一个变量或某种硬件资源。信号量的值大于或等于0时表示可供并发进程使用的资源实体数;小于0是代表正在等待使用临界资源的进程数。 

1.信号量的创建与使用 

1)信号量集的创建或打开 

Linux下使用系统函数semget创建或打开信号集。这个函数定义在头文件

sys/sem.h中,函数原型如下: 

int semget(key_t key, int nsems, int semflg);

该函数执行成功则返回一个信号集的标识符,失败返回-1。函数的第一个参数是有ftok得到的键值;第二个参数nsems指明要创建的信号集包含的信号个数,如果只是打开信号集,把nsems设置为0即可;第三个参数semflg为操作标志,可以取如下值.

IPC_CREATE:调用semget()时,它会将此值与系统中其他信号集的key进行对比,如过存在相同的key,说明信号集已存在,此时返回该信号集的标识符,否则新建一个信号集并返回其标识符。 

IPC_EXCL:该宏须和IPC_CREATE一起使用,否则而没有意义。当semflg ipc_create|IPC_EXCL时,表示如果发现信号集已经存在,则返回错误,错误码

EEXIST 

2)信号量的操作 

信号量的值与相应资源的使用情况有关,当它的值大于0时,表示当前可用资源的数量,当它的值小于0时,其绝对值表示等待使用资源的进程个数。信号量的值只能由PV操作来改变。在Linux下,PV操作通过调用函数semop实现。该函数定义在头文件sys/sem.h,原型如下: 

int semop(int semid,struct sembuf *sops,size_t nsops);

函数的参数semid为信号集的标识符;参数sops指向进行操作的结构体数组首地址:参数nsops指出将要进行操作的信号的个数。semop函数调用成功返回0,否则返回-1 

semop的第二个参数sops指向的结构体数组中,每个sembuf结构体对应一个特定信号的操作。因此对信号进行操作必须熟悉该数据结构,该结构体定义在 linux/sem.h,如下所示: 

struct sembuf{

         ushort sem_num;  /*信号在信号集中的索引*/           short sem_op;   /*操作类型*/

          short sem_flg; /*操作标志*/

}

sem_op的取值及意义 

取值范围 

操作意义 

sem_op>0

信号加上sem_op的值,表示进程释放控制的资源 

sem_op=0

如果没有设置IPC_NOWAIT则调用进程进入睡眠状态,直到信号值为0;否则进程不会睡眠,直接返回EAGAIN

sem_op<0

信号加上sem_op的值,若没有设置IPC_NOWAIT,则调用进程阻塞,直到资源可用;否则进程直接返回EAGAIN

信号量的应用实例: 

信号量一般用于处理访问临界资源的同步问题。下面通过例6-6和例6-7中的 server.cclient.c程序来演示信号量如何控制对资源的访问。Server创建一个信号集,并对信号集循环减1,相当于分配资源。client执行时检查信号量,如果其值大于0代表有资源可用,继续执行,如果小于等于0代表资源已经分配完毕,进程 client退出。     

6-6 server.c

#include <sys/types.h>

#include <linux/sem.h>

 

#define MAX_RESOURCE 5

 

int main(void)

{

 key_t  key;  int  semid;

  struct sembuf sbuf = {0, -1, IPC_NOWAIT};   union semun semopts;

 

  if ((key = ftok(".", 's')) == -1)

  {

    perror ("ftok error!\n");     exit (1);

  }

 

  if ((semid = semget (key,1,IPC_CREAT|0666)) == -1)

  {

    perror ("semget error!\n");     exit (1);

  }

 

  semopts.val = MAX_RESOURCE;

  if (semctl (semid,0,SETVAL, semopts) == -1)

  {

    perror ("semctl error!\n");     exit (1);

  }

 

  while (1)

  {

    if(semop(semid, &sbuf, 1) == -1)

    {

      perror ("semop error!\n");       exit (1);

    }

    sleep (3);

  }

 

  exit (0);

} 6-7 client.c

#include <sys/types.h>

#include <linux/sem.h>

 

int main(void)

{

  key_t   key;

  int   semid, semval;   union semun semopts;

 

  if((key = ftok (".",'s')) == -1)

  {

    perror ("ftok error!\n");     exit (1);

  }

 

  if((semid = semget (key, 1, IPC_CREAT | 0666)) == -1)

  {

    perror ("semget error!\n");     exit (1);

  }

 

  while(1)

  {

    if ((semval = semctl(semid, 0, GETVAL, 0)) == -1)

    {

      perror ("semctl error!\n");       exit (1);

    }

    if (semval > 0)

    {

      printf ("Still %d resources can be used\n", semval);

    }

    else

    {

      printf ("No more resources can be used!\n");       break;

    }

 

    sleep (3);

  }

   

  exit (0);

}

首先在终端上编译并运行server.c再在另外一个终端上编译运行client.c,观察client的运行结果。分析程序的执行过程。 

6.4 共享内存 

共享内存就是分配一块能被其他进程访问的内存。每个共享内存段在内核中维护着一个内部结构shmid_ds(和消息队列、信号量一样),该结构定义在头文

linux/shm.h中。 

1.共享内存的创建和操作 

LINUX下使用函数shmget来创建一个共享内存区,或者访问一个已存在的共享内存区。该函数定义在头文件linux/shm.h中,原型如下: 

int shmget(key_t key,size_t size,int shmflg);  

函数中:参数key是由ftok()得到的键值;参数size以字节为单位指定内存的大小;shmflg为操作标志位,它的值为一些宏,如下所示: 

IPC_CREATE:调用shmget时,系统将辞职与其他所有共享内存区的key进行比较,如果存在相同的key,说明共享内存区已存在,此时返回该共享内存区的标识符,否则新建一个共享内存区并返回其标识符。 

IPC_EXCL:该宏必须和IPC_CREATE一起使用,否则没有意义。当shmflgIPC_CREATE|IPC_EXCL时,表示如果发现信号集已经存在,则返回-1,错误码

EEXIST 

注意:当创建一个新的共享内存区是,size值必须大于0;如果是访问一个已存在的共享内存区,置size0.

2.共享内存区的操作 

在使用共享内存区前,必须通过shmat函数将其附加到进程的地址空间。进程与共享内存就建立了连接。shmat调用成功后就会返回一个指向共享内存区的指针,使用该指针就可以访问共享内存区了,如果失败返回-1。该函数声明在 linux/shm.h文件中,具体结构代码原型如下:   

void * shmat(int shmid,const void *shmaddr,int shmflg);

参数shmidshmget的返回值;参数shmflg为存取权限标志;参数shmaddr为共享内存的附加点。参数shmaddr不同取值情况的含义说明如下: 

如果为空,则由内核选择而一个空闲的内存区;如果非空,返回地址取决于调用者是否给shmflg参数指定了SHM_RND值,如果没有指定,则共享内存区附加到有shmaddr向下舍入一个共享内存低端边界地址后的地址(SHMLBA,一个常址)。 

通常将参数shmaddr设置为NULL 

当进程结束使用共享内存区时,要通过函数shmdt断开与共享内存的链接。

该函数声明在sys/shm.h文件中,具体结构代码原型如下: 

int shmdt(const void* shmaddr);

参数shmaddrshmat函数的返回值。该函数调用成功后,返回0,否则返回 -1。进程脱离共享内存区后,数据结构shmid_dsshm_nattch就会减1。但是共享内存段依然存在,只有shm_attch0后,即没有任何进程再使用该共享内存区,共享内存区才能在内核中被删除。一般来说,当一个进程终止时,它所附加的共享内存区都会自动脱离。 

3.共享内存区的控制 

Linux对共享内存区的控制是通过调用函数shmctl来完成的,该函数定义在头文件sys/shm.h中,原型代码如下所示: 

int shmctl(int shmid,int cmd, struct shmid_ds *buf);

函数中:参数shmid为共享内存区的标识符;buf为指向shmid_ds结构体的指针;cmd为操作标志位,支持一下3种控制操作。 

IPC_RMID:从系统中删除有shmid标识的共享内存区。 

IPC_SET:设置共享内存区的shmid_ds结构。 

IPC_STAT:读取共享内存区的shimid_ds结构,并将其存储到buf指向的地址中。 

6-8和例6-9writerreader程序,两程序在进入共享内存区之前,首先都检查信号集中的信号的值是否为1(相当于是否能进入共享内存区),如果不为1

调用sleep()进入睡眠状态直到信号的值变为1。进入共享内存区之后,将信号的值减1(相当于加锁),这样就实现了互斥的访问共享资源。在退出共享内存时,将信号值加1(相当于解锁)。 

6-8 writer.c

#include "sharemem.h"

#define SHM_SIZE 1024

 

int main()

{  int   semid, shmid;  char  *shmaddr;  char  write_str[SHM_SIZE];

  

 if ((shmid = createshm (".", 'm', SHM_SIZE)) == -1)

 {

   exit(1);

 }

  

 if ((shmaddr = shmat (shmid, (char *)0, 0)) ==(char *)-1)

 {

   perror ("attach shared memory error!\n");    exit (1);

 }

  

 if ((semid = createsem (".", 's', 1, 1)) == -1)

 {

   exit (1);

 }

  

 while (1)

 {

   wait_sem (semid, 0);    sem_p (semid, 0);  /*P操作*/

    

   printf ("writer: ");    fgets (write_str, 1024, stdin);    int len = strlen (write_str) - 1;    write_str[len] = '\0';    strcpy (shmaddr, write_str);

   sleep (10);  /*使reader处于阻塞状态*/

    

   sem_v (semid, 0);  /*V操作*/    sleep (10);  /*等待reader进行读操作*/

    

 }

}

6-9 reader.c

#include "sharemem.h"

 

int main() {  int semid, shmid;  char *shmaddr;

  

 if ((shmid = createshm(".", 'm', SHM_SIZE)) == -1)

 {

   exit (1);

 }

  

 if((shmaddr = shmat (shmid, (char *)0, 0)) == (char *)-1)

 {

   perror ("attach shared memory error!\n");    exit (1);

 }

  

 if((semid = opensem("." ,'s')) == -1)

 {

   exit (1);

 }

  

 while(1)

 {

   printf("reader: ");

   wait_sem(semid,0);   /* 等待信号值为1 */    sem_p(semid,0);     /* P操作 */

    

   printf("%s\n", shmaddr);

   sleep(10);     /* 使writer处于阻塞状态 */

       sem_v(semid,0);     /* V操作 */    sleep(10);     /* 等待writer进行写操作 */

 }

} 编译两个程序,同时在两个终端运行writerreader,在writer端输入字符串 “helloreader”,等待一会,看到reader端输出后,在writer.c端的提示符后面输入字符串“new information”运行的结果是什么?请分析程序的执行过程。附:sharememory.h

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

#include <sys/shm.h>

#include <errno.h>

 

#define SHM_SIZE 1024

 

union semun{

  int     val;

 struct semid_ds  *buf;  unsigned short  *array;

};

 

/*创建信号量函数*/

int createsem (const char * pathname,  int proj_id,  int members,  int init_val)

{

  key_t   msgkey;   int   index,  sid;   union semun semopts;

       

  if ((msgkey = ftok(pathname, proj_id)) == -1)

  {

    perror ("ftok error!\n");     return -1;

  }

 

  if ((sid = semget (msgkey, members, IPC_CREAT | 0666)) == -1)

  {

  perror ("semget call failed.\n");     return -1;

  }

 

  /*初始化操作*/

  semopts.val = init_val;   for (index = 0; index < members; index++)

  {

    semctl (sid, index, SETVAL, semopts);

  }

  return (sid);

}

 

/*打开信号量函数*/

int opensem(const char * pathname, int proj_id)

{

 key_t msgkey;  int sid;          

                                                                                    if ((msgkey = ftok(pathname, proj_id)) == -1)

  {

    perror ("ftok error!\n");     return -1;

  }

 

  if ((sid = semget(msgkey, 0, IPC_CREAT | 0666)) == -1)

  {

    perror("semget call failed.\n");     return -1;

  }

 

  return (sid);

 

}

 

/*P操作函数*/

int sem_p(int semid, int index)

{

struct sembuf buf = {0,-1,IPC_NOWAIT};

 

  if (index < 0)

  {

    perror("index of array cannot equals a minus value!");     return -1;

  }

 

  buf.sem_num = index;   if (semop (semid ,& buf,1) == -1)

  {

    perror ("a wrong operation to semaphore occurred!");     return -1;

  }

  return 0;

}  

 

/*v操作函数*/

int sem_v (int semid, int index)

{

  struct sembuf buf = {0, +1, IPC_NOWAIT};

 

  if (index < 0)

  {

    perror("index of array cannot equals a minus value!");     return -1;

  }

 

  buf.sem_num = index;   if (semop (semid,& buf,1) == -1)

  {

    perror ("a wrong operation to semaphore occurred!");     return -1;

  }

 

  return 0;

}  

 

/*删除信号集函数*/

int sem_delete (int semid)

{   return (semctl(semid, 0, IPC_RMID));

}

 

/*等待信号为1*/

int wait_sem( int semid, int index)

{

   

  while (semctl (semid, index, GETVAL, 0) == 0)

  {

    sleep (1);

  }

 

  return 1 ;

}

 

 

/*创建共享内存函数*/

int createshm( char * pathname, int proj_id, size_t size)

{

  key_t shmkey;   int   sid;

       

  /*获取键值*/

  if ((shmkey = ftok(pathname, proj_id)) == -1)

  {

    perror("ftok error!\n");     return -1;

  }

 

  if ((sid = shmget(shmkey, size, IPC_CREAT | 0666)) == -1)

  {

         

    perror ("shmget call failed.\n");     return -1;

  }

  return (sid);

}     

实验七 存储器管理

实验目的

理解内存页面调度原理、掌握几种理论页面置换算法的实现方法、通过实验比较各种调度算法的优劣; 

页面置换算法是虚拟存储管理实现的关键,通过本次实验理解内存页面调度的机制,在模拟实现FIFOLRUNRUOPT几种经典的页面置换算法的基础上,比较各种置换算法的效率及优缺点,从而了解虚拟存储实现的过程。 

在试验中要用到的Linux函数有: 

int getpid()       获得当前进程的id

void srand(int a) a为种子产生随机数 

int rand()          根据前面的种子,返回一个随机数实验内容

对比下列几种算法的命中率: 

  1. 先进先出的算法FIFO
  2. 近 少使用的算法LRU
  3. 近未使用的算法NUR
  4. 理想型算法OPT 实验指导

拥有页面交换机制的操作系统总是把当前进程中急需处理的部分页面换入到内存中,而把更多暂时不需要处理的页面放置在外存中。由于进程需要处理的页面顺序不同,因此必须在内存与外存之间进行页面交换,本实验并没有进入系统空间对实际进程页面进行控制,而是在用户空间用线性表的连续存储方式对进程页面交换进行模拟。 

1. FIFO页面置换算法 

在分配内存页面数(AP)小于进程页面数(PP)时,当然是 先进入的AP 个页面放入内存。这时有需要处理新的页面,则将原来在内存中的AP个页面中先进的调出,然后将新页面放入。以后如果再有新页面调入都按照前面的规则进行。算法的特点是:所使用的内存页面构成一个队列。 

假设某个进程在硬盘上被划分为5个页面(PP5),以12345分别表示,处理机调用它们的顺序(这取决于进程本身)为:14253324

25

如果内存可以控制的页面数为3AP=3),那么在使用FIFO算法时,这3个页面的内存使用情况如下图所示: 

不难看出本例共换入页面8次,若用变量diseffect标识页面换入的次数,则

diseffect=8。算法实现提示: 

要得到命中率,必然应该有一个常量total_instruction来记录页面总共使用的次数;此外还需要一个变量记录总共换入页面的次数(需要换出页面,总是因

diseffect

1- ´100%

为缺页中断而产生)diseffect。利用公式 total _instruction 可以得到命中率。 

  1. 初始化。设置两个数组pagge[ap]pagecontrol[pp]分别标识进程页面数和内存分配的页面数,并产生一个随机数序列main[total_instruction](这个序列由page[]得下标随机构成)表示待处理的进程页面顺序,diseffect置成0 
  2. main[]中是否有下一个元素,若有,就由main[]中获取该页面下表,并转到(7)。 
  3. 如果该page已在内存中,就转到(2);否则就到(4),同时未命

中的diseffect1 

  1. 观察pagecontrol是否占满,如果占满必须将使用队列(在第(6)步中建立的)中 先进入的(就是队列第一个单元)pagecontrol单元清干净,同时将对应的page[]单元置为不在内存中 
  2. 将该page[] pagecontrol[]建立关系(可以改变pagecontrol[]的标志位,也可以采用指针链接,总之至少要使对应的pagecontrol单元包含两个信息:一是它被使用了,二是哪个page[]单元使用的。page[]单元也包含两个信息:对应的pagecontrol单元号和本page[]单元已在内存中。 
  3. 将用到的pagecontrol置入使用队列(这里的队列是一种先进先出的

数据结构,而不是泛指),返回(2)。 

diseffect

1- ´100%

  1. 显示计算 total _instruction  的结果,完成。 

2.LRU页面置换算法 

当分配内存页面数(AP)小于进程页面数(PP)时,当然是把 先执行的 AP个页面放入内存。当需要调入页面进入内存,而当前分配的内存页面全部不空闲时,选择将其中 长时间没有用到的那个页面调出,以空出内存来放置新调入内存的页面(简称为LRU)。算法特点:每个页面都有属性来表示有多长时间未被CPU使用的信息。 

图表描述: 

为了便于比较学习,我们采用的例子和前面的一样。某进程在硬盘上被划分为5个页面,用12345表示,处理机处理它们的顺序为: 

1425332425而内存可以控制的页面数为3AP=3),那么在使用LRU算法时,这三个页面的内存使用情况如下图所示: 

页面换入次数为7diseffect=7  

算法实现提示: 

与前面的算法一样,只有先得到diseffect才能获得 终的命中率 

  1. 初始化。主要是进程页面page[]和分配的内存页面pagecontrol[],同时产生随机序列main[]diseffect0 
  2. 看序列main[]是否有下一个元素,如果有,就从main[]获取该page[]的下标,并转到(3);如果没有就转到(6)。 
  3. 如果该page[]单元在内存中便改变页面属性,使它保留“ 近使用”的信息,转到(2);否则转到(4),同时diseffect1 
  4. 判断是否有空闲的内存页面,如果有,就返回页面指针,转到(5);否则,在内存页面中找出 长时间没有使用的页面,将其清干净,并返回该页面指针。 
  5. 在需要处理的page[]与(4)中得到pagecontrol[]之间建立联系,同时让对应的page[]单元保存新使用的信息。返回(2)。 

diseffect

1- ´100%

  1. 如果序列处理完成,就输出计算 total _in struction 的结果,并结束。 

3. NUR页面置换算法 

所谓近未使用,首先是要对作一个界定,比如CLEAR_PERIOD=50,便是指在CPU近的50次进程页面处理工作中,都没有处理到的页面。那么可能会有一下几种情况: 

  1. 如果这样的页面只有一个,就将其换出,放入需要处理的新页面。 
  2. 如果有这样的页面不止一个,就在这些页面中任取一个换出(可以是下标 小的或者是下标 大的),放入需要处理的页面。 
  3. 如果没有一个这样的页面,就随意换出一个页面(可以是下标 小的,或者是下标 大的)。 

算法特点:有一个循环周期,每到达这个周期,所有页面存放是否被CPU处理的信息的属性均被置于初始态(没有被访问)。 

图表描述: 

还是用前面的例子,某进程在硬盘上被划分为5个页面,用12345 表示,而处理机处理它们的顺序为:1425332425而内存可以控制的页面数为3AP=3),CLEAR_PERIOD5;在循环周期内,如果所有内存页面均为CPU处理或者有多个页面未被CPU处理,取页码 小的页面换出。该算法实现过程如图。     

算法实现提示: 

  1. 初始化。设置两个数组page[ap]pagecontrol[pp]分别标识进程页面数和内存分配的页面数,并产生一个随机数序列main[total_instruction](当然这个序列由page[]的下标随机构成)表示待处理的进程页面顺序,diseffect0,设定循环

周期CLEAR_PERIOD 

  1. main[] 是否有下一个元素。若有,就从main[]中获得一个CPU将处理页面的序号;如果没有就转到(8)。 
  2. 如果待处理的页面在内存之中,就转到(2);否则diseffect1,并转到(4)。 
  3. 看是否有空闲的内存页面,如果有,返回空闲内存页面指针,并转到
  4. ;否则,在所有没有被访问且位于内存中的页面中按任意规则(或者取近的一个页面;或者取下标 小的......)取出一个,返回清空后的内存页面指针。 
  1. 在待处理进程页面与内存页面之间建立联系,并标注该页被访问。 
  2. 如果CPU已处理了CLEAR_PERIOD个页面,就将所有页面均设为未访问 
  3. 返回(2)。 

diseffect

1- ´100%

  1. 如果CPU所有处理工作完成,就返回 total_instruction 的结果,并结束。 

参考源程序代码: 

1. page.h文件 

#ifndef _PAGE_H

#define _PAGE_H

 

class CPage { public:

  int m_nPageNumber,     m_nPageFaceNumber,     m_nCounter,     m_nTime;

};

#endif

1.  PageControl.h文件 

#ifndef _PAGECONTROL_H

#define _PAGECONTROL_H

 

class CPageControl

{ public:

 int m_nPageNumber,m_nPageFaceNumber;  class CPageControl * m_pNext;

};

 

#endif

2.memory.h文件 

#ifndef _MEMORY_H

#define _MEMORY_H

 

class CMemory

{ public:

 CMemory();

 void initialize(const int nTotal_pf);  void FIFO(const int nTotal_pf);  void LRU(const int nTotal_pf);  void NUR(const int nTotal_pf);  void OPT(const int nTotal_pf); private:

 vector<CPage> _vDiscPages;  vector<CPageControl> _vMemoryPages;

 CPageControl *_pFreepf_head,*_pBusypf_head,*_pBusypf_tail;  vector<int> _vMain,_vPage,_vOffset;  int _nDiseffect;

  

};

 

CMemory::CMemory():_vDiscPages(TOTAL_VP),

          _vMemoryPages(TOTAL_VP),

          _vMain(TOTAL_INSTRUCTION),

          _vPage(TOTAL_INSTRUCTION),

          _vOffset(TOTAL_INSTRUCTION)

{  int S,i,nRand;  srand(getpid()*10);  nRand=rand()%32767;

  

 S=(float)319*nRand/32767+1;  for(i=0;i<TOTAL_INSTRUCTION;i+=4)

 {

   _vMain[i]=S;    _vMain[i+1]=_vMain[i]+1;    nRand=rand()%32767;

   _vMain[i+2]=(float)_vMain[i]*nRand/32767;    _vMain[i+3]=_vMain[i+2]+1;    nRand=rand()%32767;

   S=(float)nRand *(318-_vMain[i+2])/32767+_vMain[i+2]+2;

 }  for(i=0;i<TOTAL_INSTRUCTION;i++)

 {

   _vPage[i]=_vMain[i]/10;

   _vOffset[i]=_vMain[i]%10;

   _vPage[i]%=32;

 }

}

 

void CMemory::initialize(const int nTotal_pf)

{  int ix;

 _nDiseffect=0;  for(ix=0;ix<_vDiscPages.size();ix++)

 {

   _vDiscPages[ix].m_nPageNumber=ix;

   _vDiscPages[ix].m_nPageFaceNumber=INVALID;

   _vDiscPages[ix].m_nCounter=0;

   _vDiscPages[ix].m_nTime=-1;

 }  for(ix=1;ix<nTotal_pf;ix++)

 {

   _vMemoryPages[ix-1].m_pNext=&_vMemoryPages[ix];

   _vMemoryPages[ix-1].m_nPageFaceNumber=ix-1;

 }

 _vMemoryPages[nTotal_pf-1].m_pNext=NULL;

 _vMemoryPages[nTotal_pf-1].m_nPageFaceNumber=nTotal_pf-1;

 _pFreepf_head=&_vMemoryPages[0];

}

 

void CMemory::FIFO(const int nTotal_pf)

{  int i;

 CPageControl *p;  initialize(nTotal_pf);

 _pBusypf_head=_pBusypf_tail=NULL;  for(i=0;i<TOTAL_INSTRUCTION;i++)

 {    if(_vDiscPages[_vPage[i]].m_nPageFaceNumber==INVALID)

     {

       _nDiseffect+=1;        if(_pFreepf_head==NULL)  //no empty pages

   {

     p=_pBusypf_head->m_pNext;

     

_vDiscPages[_pBusypf_head->m_nPageNumber].m_nPageFaceNumber=INVALI D;

     _pFreepf_head=_pBusypf_head;

     _pFreepf_head->m_pNext=NULL;

     _pBusypf_head=p;

   }

       p=_pFreepf_head->m_pNext;        _pFreepf_head->m_pNext=NULL;

       _pFreepf_head->m_nPageNumber=_vPage[i];

       

_vDiscPages[_vPage[i]].m_nPageFaceNumber=_pFreepf_head->m_nPageFaceNu mber;        if(_pBusypf_tail==NULL)

   _pBusypf_head=_pBusypf_tail=_pFreepf_head;        else

   {

     _pBusypf_tail->m_pNext=_pFreepf_head;

     _pBusypf_tail=_pFreepf_head;

   }

       _pFreepf_head=p;

      }     }   cout<<"FIFO: "<<1-(float)_nDiseffect/320;

}

 

void CMemory::LRU(const int nTotal_pf)

{   int i,j,nMin,minj,nPresentTime(0);   initialize(nTotal_pf);

  for(i=0;i<TOTAL_INSTRUCTION;i++)

    {       if(_vDiscPages[_vPage[i]].m_nPageFaceNumber==INVALID)

 {

   _nDiseffect++;    if(_pFreepf_head==NULL)

     {        nMin=32767;        for(j=0;j<TOTAL_VP;j++) //get the subscribe of the least used page

   //after the recycle iMin is the number of times

   //used of the least used page while minj is its subscribe

 

 if(nMin>_vDiscPages[j].m_nTime&&_vDiscPages[j].m_nPageFaceNumber!=IN VALID)

     {

       nMin=_vDiscPages[j].m_nTime;         minj=j;

     }

       

_pFreepf_head=&_vMemoryPages[_vDiscPages[minj].m_nPageFaceNumber];

       _vDiscPages[minj].m_nPageFaceNumber=INVALID;

       _vDiscPages[minj].m_nTime=-1;

       _pFreepf_head->m_pNext=NULL;

     }

   

_vDiscPages[_vPage[i]].m_nPageFaceNumber=_pFreepf_head->m_nPageFaceNu mber;

   _vDiscPages[_vPage[i]].m_nTime=nPresentTime;

   _pFreepf_head=_pFreepf_head->m_pNext;

 }       else

 _vDiscPages[_vPage[i]].m_nTime=nPresentTime;

             nPresentTime++;

    }   cout<<"LRU: "<<1-(float)_nDiseffect/320;

}

 

void CMemory::NUR(const int nTotal_pf)

{   int i,j,nDiscPage,nOld_DiscPage;   bool bCont_flag;   initialize(nTotal_pf);   nDiscPage=0;   for(i=0;i<TOTAL_INSTRUCTION;i++)

    {       if(_vDiscPages[_vPage[i]].m_nPageFaceNumber==INVALID)

 {

   _nDiseffect++;    if(_pFreepf_head==NULL)

     {        bCont_flag=true;        nOld_DiscPage=nDiscPage;        while(bCont_flag)

   {

     

if(_vDiscPages[nDiscPage].m_nCounter==0&&_vDiscPages[nDiscPage].m_nPage

FaceNumber!=INVALID)        bCont_flag=false;      else        {          nDiscPage++;          if(nDiscPage==TOTAL_VP) nDiscPage=0;          if(nDiscPage==nOld_DiscPage)      for(j=0;j<TOTAL_VP;j++)        _vDiscPages[j].m_nCounter=0;

       }

   }

       

_pFreepf_head=&_vMemoryPages[_vDiscPages[nDiscPage].m_nPageFaceNumber

];

       _vDiscPages[nDiscPage].m_nPageFaceNumber=INVALID;

       _pFreepf_head->m_pNext=NULL;

     }

   

_vDiscPages[_vPage[i]].m_nPageFaceNumber=_pFreepf_head->m_nPageFaceNu mber;

   _pFreepf_head=_pFreepf_head->m_pNext;

 }       else

 _vDiscPages[_vPage[i]].m_nCounter=1;       if(i%CLEAR_PERIOD==0)  for(j=0;j<TOTAL_VP;j++)    _vDiscPages[j].m_nCounter=0;

    }   cout<<"NUR:"<<1-(float)_nDiseffect/320; }

 

void CMemory::OPT(const int nTotal_pf)

{   int i,j,max,maxpage,nDistance,vDistance[TOTAL_VP];

     initialize(nTotal_pf);

  for(i=0;i<TOTAL_INSTRUCTION;i++)

    {       if(_vDiscPages[_vPage[i]].m_nPageFaceNumber==INVALID)

 {

   _nDiseffect++;    if(_pFreepf_head==NULL)

     {        for(j=0;j<TOTAL_VP;j++)    if(_vDiscPages[j].m_nPageFaceNumber!=INVALID)      vDistance[j]=32767;    else      vDistance[j]=0;        nDistance=1;        for(j=i+1;j<TOTAL_INSTRUCTION;j++)

   {

     

if((_vDiscPages[_vPage[j]].m_nPageFaceNumber!=INVALID)&&(vDistance[_vP age[j]]==32767))        vDistance[_vPage[j]]=nDistance;      nDistance++;

   }

       max=-1;        for(j=0;j<TOTAL_VP;j++)    if(max<vDistance[j])

     {

       max=vDistance[j];        maxpage=j;

     }

       

_pFreepf_head=&_vMemoryPages[_vDiscPages[maxpage].m_nPageFaceNumber];

       _pFreepf_head->m_pNext=NULL;        _vDiscPages[maxpage].m_nPageFaceNumber=INVALID;

     }

   

_vDiscPages[_vPage[i]].m_nPageFaceNumber=_pFreepf_head->m_nPageFaceNu mber;

   _pFreepf_head=_pFreepf_head->m_pNext;

 }     }   cout<<"OPT:"<<1-(float)_nDiseffect/320;

}

#endif

 

3.main.cpp文件 

#include <iostream>

#include <string>

#include <vector>

#include <cstdlib>

#include <cstdio>

#include <unistd.h>

 

using namespace std;

 

#define INVALID -1

 

const int TOTAL_INSTRUCTION(320); const int TOTAL_VP(32); const int CLEAR_PERIOD(50);

 

#include "Page.h"

#include "PageControl.h"

#include "Memory.h"

 

int main() {

 int i;

 CMemory a;  for(i=4;i<=32;i++)

 {

   a.FIFO(i);

        a.LRU(i);

   a.NUR(i);

   a.OPT(i);    cout<<"\n";

 }  return 0;

}

编译并运行程序:g++ -o main main.cpp

./main    

 

实验八 Linux 文件系统

 

在本实验中,我们将使用不同的信息命令,来调查和发现文件的信息;使用命令来帮助确定文件类型和创建文件的应用;也可以使用几个命令来查看文本文件的内容和对它们进行比较。 

作为基础,使用命令来分析和管理文件及目录的能力对于进一步学习 Linux 操作系统是非常重要的。Linux 特权用户和系统管理员必须有使用命令行的工作背景,例如创建可执行的脚本文件等(Linux 脚本文件包含着一系列的 Linux 命令)。许多操作系统管理和设备配置作业需要理解 Linux 命令,并且在某些情况下,命令行是仅有的操作方式。 

在本实验中,我们将在命令行下使用文件管理命令工作,介绍文件和目录命名规则。我们将创建一个简单的目录结构,然后在那些目录中创建一个文件,并练习创建和删除文件和目录。 

实验目的

通过本实验:(1)使用控制字符执行特殊功能; 使用 file strings 命令确定文件类型; 使用 cat more 命令显示文本文件的内容;使用 head tail 命令显示文本文件的部分内容;使用 wc 命令确定单词、行和字符数;使用 diff 命令比较 2 个文件。回顾文件和目录命名约定;使用 touch 命令创建新文件;使用 mkdir 命令创建新目录;使用 rm 命令删除文件;使用 rm -r 命令删除目录。(2)显示文件系统权限。确定文件的用户权限、属组权限或其他 (公共) 权限。确定对于一个可执行文件的文件权限。使用默认权限创建一个文件或目录。从命令行中修改权限: 使用文件系统来控制安全访问。使用符号模式修改文件或目录的权限。使用八进制模式修改文件或目录的权限。使用 vi 编辑器创建一个脚本文件,并且使它可执行。访问文件管理器,使用文件和目录权限工作;使用文件管理器确定文件或文件夹的权限;使用文件管理器修改文件或文件夹的权限。 

实验内容与步骤

1. 文件信息命令 

步骤 1:开机,登录进入 GNOME 

GNOME 登录框中填写指导老师分配的用户名和口令并登录。 

步骤 2:访问命令行。 

单击系统,在菜单中单击系统工具”-“终端命令,打开终端窗口。 

步骤 3:使用控制字符执行特殊功能。 

控制字符用来执行特殊的作业,如中止和启动屏幕输出。 

大多数 PC 键盘有两个控制键。它们通常标记为 Ctr1,可以在键盘的左右下角找到。为了输入一个控制字符序列,可按住 Ctrl 键不放,然后按下键盘上相应的字符。 

Ctrl + C:中断当前活动。当你在 csh 中键入一个不可识别的命令行 (例如,

$ls “) ,收到第 2 个提示符的时候,Ctrl + C 也可以用于恢复 shell 提示符。 

Ctrl + Z:终止当前活动。显示 ls 命令的手册页面 (man ls) ,然后使用 Ctrl -z 中止输出。 

当你接下 Ctrl + Z 的时候,发生了什么事情? 

    

____________________________________________________________________

Ctrl + D:表示文件的末尾或者退出。 Ctrl + D 用于退出一些 Linux 工具程序 (bcwrite ),退出一个终端窗口,注销一个终端会话或者命令行登录会话。作为一个一般的规则,当您出现死机时,或者如果 Ctrl + C 不起作用,可试试

Ctrl + D。例如: 

  1. shell 提示符下键入 bc,启动基本的计算器工具程序。 
  2. 把两个数字相乘 (键入:458*594,然后按回车键)  
  3. Ctrl + D 退出计算器。 

当使用计算器的时候,你的提示符是什么? 

    

____________________________________________________________________

Ctrl + U :擦除整个命令行。Ctrl + U 常用在: 

· 一个擦除决定不执行的命令行的快捷方式。 

· 如果登录到一个远程系统,退格键不起作用。 

· 它可以确保在登录的时候,从一个的用户帐号和口令输入开始。 

· 因为在口令输入的时候看不见它们,当直到自己键入了错误字符的时候,使用 Ctrl + U 擦除密码,重新开始输入。 

如果输入一个命令,如 ls –R/,有时候,会在按下回车键之前想擦除命令行。输入一个命令,在接下回车键执行命令之前按下 Ctrl + U。结果是什么? 

_________________________________________________________________ _________________________________________________________________ 步骤 4:使用 file 命令确定文件类型。 

Linux 系统中可以找到许多类型的文件。文件类型可以通过使用 file 命令来确定。当一个用户试图打开或阅读一个文件的时候,这个信息很重要。确定文件类型可以帮助一个用户决定使用哪个程序或命令来打开这个文件。这个命令的输出 常见的是如下几种:文本文件、可执行文件或数据文件。 

  1. 文本文件:包括 ASCII 或英语文本、命令文本和可执行的 shell 脚本。这种类型的文件可以使用 cat more 命令读取,可以使用 vi 或其他文本编辑器编辑。单击“应用程序”菜单,在附件菜单中单击文本编辑器,在文本编辑中键入适当内容并保存为 test 文件。 

使用 file 命令来确定 test 文件的文件类型。它是哪种类型的文件? 

    

____________________________________________________________________

  1. 可执行 (或二进制) 文件:包括 32 位的可执行文件和可扩展链接格式 (ELF) 编码文件,和其他动态链接的可执行文件。这种文件类型表示这个文件是一个命令或程序。 

单击应用程序菜单中单击办公”-“OpenOffice.org Writer”命令,建立一个文档如 ww.sxw 

使用 file 命令确定你所建立的文件类型。它是哪种类型的文件?(注意文件名部分必须包括扩展名,如 file ww.sxw)

_________________________________________________________________ 步骤 5:使用 strings 命令。 

strings 命令可以用于打印可执行文件或者二进制文件中的可读字符。 

一些有编程背景的人,可以解释 strings 产生的输出。这个命令在这里只是作为一个展示可执行文件中可打印字符的方法来介绍。strings 命令必须用于读取可执行文件,如 /usr/bin/cat。在大多数情况下,strings 命令也可以给出命令的使用语法。 

使用 strings 命令查看 /usr/bin/cat 文件的可读字符。列出 strings 命令中的一些输出。 

_________________________________________________________________ 步骤 6:使用 cat 命令显示文件的内容。 

cat 命令在屏幕上显示一个文本文件的内容。它常用于显示如脚本文件 (类似批处理文件) 这样的短文本文件。如果文件超过一屏的话,必须使用一个屏幕可以滚动的窗口,如 GNOME 环境中的终端窗口。 

键入 ls /dev > dev1

使用 cat 命令显示主目录中 dev1 文件的内容。文本的显示出现了什么情况? 

    

____________________________________________________________________ 步骤 7:使用 more 命令显示文件的内容。 

more 命令是一个用于显示文本文件首选的方法,因为它会自动的一次显示一屏文件内容。如果文件的信息比一屏更长,屏幕的底部显示如下的信息: --More-- (n) (文件的 n%已经显示) 。按下回车键,继续一次显示一行信息。空格键将继续一次显示一屏内容。 

使用 more 命令显示主目录中 dev1 文件的内容。文本的显示出现了什么情况? 

_________________________________________________________________

步骤 8:使用 head 命令显示文件的一部分。 

head 命令用于显示一个或多个文本文件的前 n 行。在默认情况下,如果没有给出 -n 选项,将显示前 10 行。当您只想查看文件的开始的几行,而不管文件的大小的时候,head 命令是很有用的。 

  1. 单独使用 head 命令,显示主目录中 dev1 文件的开始部分。显示了多少行? 

_________________________________________________________________

  1. 使用带 -n 选项的 head 命令,显示主目录中 dante 文件的前 20 行。您输入什么命令? 

_________________________________________________________________ 步骤 9:使用 tail 命令显示文件的一部分。 

使用 tail 命令,显示文件的 后几行。在默认情况下,如果没有指定 -n 选项,将显示 后 10 行。当检查大型日志文件 近输入内容的时候,tail 命令是很有用的。备份工具程序常把备份哪个文件和什么时候做的备份,写到日志文件中去。一个备份日志文件中 后的输入通常是备份文件的总数和备份是否成功完成的信息。-n 选项显示了文件的 后 n 行。 

单独使用 tail 命令,显示主目录中 dante 文件的末端。显示了多少行? 

_________________________________________________________________ 步骤 10:通过使用 wc 命令,确定行数、单词数和字符数。 

wc (单词计数) 命令可以用于显示文本文件的行数、单词数、字节数或者字符数。当确定文件特征或者当比较两个文件的时候,这个命令是很有用的。使用不带选项的 wc 将给出文件的行数、字节数。使用带一个选项的 wc,可以确定想查看的哪一项内容。 

使用 wc 命令确定主目录中 dev1 文件的行数、单词数和字符数。有多少行、多少个单词和多少个字符? 

_________________________________________________________________ 步骤 11:使用 wc 计算目录条目的数目。 

使用 wc ls 命令确定主目录中条目 (文件和目录) 的数目。为此,必须把 ls 命令的输出导入到 wc 命令中。 

更多符号是竖线,和后斜线 (\) 在同一个键上。在命令行提示行下,输入命令 ls l wc -w。有多少个文件和目录名 (单词)  

_________________________________________________________________ 步骤 12:使用 diff 命令确定文件之间的不同之处。 

diff (不同) 命令用于比较 2 个文本文件,找出在它们之间的不同之处。wc 命令可以比较文件,因为它计算行数、单词数和字符数。有可能 2 个文件有相同的行数、单词数和字符数,但是字符和单词不同。diff 命令可以从实际上找出文件之间的不同。 

这个命令的输出把 2 个文本文件之间的不同一行一行的显示出来。diff 命令

2 个选项:-i  -c-i 选项忽略字母的大小写,例如 A a 相等。-c 选项执行细致的比较。 

单击应用程序菜单中单击附件”-“文本编辑器命令,创建两个文件 fruit1 fruit2,并键入适当内容。 

使用 diff 命令执行细节比较,确定 fruit1 文件和 fruit2 文件之间的区别。 

fruit1 文件和在 fruit2 文件中,哪几行是不同的? 

_________________________________________________________________ _________________________________________________________________

· fruit1 文件使用 cat 命令。 

· fruit2 文件使用 cat 命令。 

· 键入命令行 cat fruit1 fruit2 > filex 

· filex 文件使用 cat 命令。上面的命令行做了什么工作? 

_________________________________________________________________

2) 可以使用哪 2 个命令来确定 2 个文件是否相同? 

_________________________________________________________________ 步骤 13:关闭终端窗口,注销。 

    2. 基本的命令行文件管理 

步骤 14:回顾 Linux 的文件和目录命名规则。 

在本实验中,我们将创建文件和目录,因此,在开始之前,先来回顾一下

Linux 文件和目录的命名规则和指导方针。 

  1. 大长度:组成文件和目录名 大长度为 255 个数字字母字符。一般来说,应该尽可能的保持文件名短但是仍然有意义。 
  2. 非数字字母字符:一些非数字字母字符或者元字符是可用的:下划线 

(_) 、连字符号 (-) 和句点 (.) 。这些元字符可以在文件或目录名中使用多次 (Feb.Reports.Sales 是一个有效的文件或目录名) 。尽管 shell 允许把星号 (*) 、问号(?) 和发音符号 (~) 、方话号 ([ ]) &、管道 [ | ] 、引号 (“”) 和美元符号 ($) 在文件名中使用,但这不是推荐的,因为这些字符对于 shell 有特殊的意义。

分号 (;) 、小于号 (<) 和大于号 (>) 是不允许作为文件名的。 

  1. 文件名扩展:文件名可以包含一个或多个扩展名。扩展名常被一个应用追加到文件的末端。扩展名通常是 1 个到 3 个字符,追加到文件名的末端,之前有一个句点 (.) 。当命名文件的时候,您可以选择使用这个规则。 
  2. 目录名的扩展名:目录名一般不包含扩展名,但是也没有规则反对这一点。 
  3. 大小写敏感:Linux 文件和目录名是大小写敏感的。Project1 projectl 不是同一个文件。在一个目录中,不能够有两个文件有着同样的名字。一般规则都是使用小写字母。 

检查表 8-1 中的文件名,指出它们是否是有效或者推荐的 Linux 文件或目录名,为什么是或为什么不是。 

8-1  实验记录 

文件名 

是否为 Linux 文件或目录名

为什么是或为什么不是 

12345abcde678

  

  

Hobbies: 2

  

  

Adcd-123

  

  

Sales*repts*2001

  

  

D.projects.bj.2001

  

  

Projects>1.bj-2001

  

  

    步骤 15:使用 touch 命令创建文件。 

每次创建一个新的字处理文档或者电子数据表,就是正在创建一个新文件,

应该符合之前提到的文件命名规则。也必须拥有创建文件的目录的足够权限。 

使用 touch 命令,可以同时创建一个或多个文件。一些应用要求在写文件之前,文件必须存在。touch 命令对于快速创建需要处理的文件很有用。也可以使用 touch 命令更新文件被访问的时间和日期,使文件可以再次被备份。当创建文件或目录的时候,可以指定绝对和相对的路径名。 

命令格式: 

touch filename (s)

1)在主目录中使用 touch 命令创建一个名为 newfile 的文件,应该使用什么命令? 

_________________________________________________________________

2)使用 touch 命令在这个目录中创建另一个叫做 filenew 的新文件,应该使用什么命令? 

_________________________________________________________________

3)输入命令显示 practice 目录中的文件的长列表。创建的文件列出来了吗? 

_________________________________________________________________

4)谁是文件的所有者? 

_________________________________________________________________

5)和文件关联的组是什么? 

_________________________________________________________________

6)创建的日期和时间是什么? 

________________________________________________________________

7)文件的大小是多少? 

_________________________________________________________________

8) 使用 file 命令确定 newfile 的文件类型。它是哪一类的文件? 

_________________________________________________________________

9)使用 touch 命令同时创建 3 个文件:new1new2 new3,应该使用什么命令? 

_________________________________________________________________ 10)输入命令显示 practice 目录中文件的长列表。创建的 3 个新文件列出来了吗? 

_________________________________________________________________ 步骤 16:使用 mkdir 命令创建新目录。 

mkdir (创建目录) 命令用于创建目录或文件夹。目录可以包含其他目录,称

为子目录,它们可以包含文件。 

    目录可以使用或者绝对路径名或者相对路径名创建。可以在同一行中指

定多个目录名,创建多个新目录。必须有创建目录的足够权限。 

mkdir directory_name (s)

  1. 从主目录中,使用相对路径名改变到 practice 目录中。使用什么命令? 

_________________________________________________________________

  1. 使用 mkdir 命令,在这个目录中创建一个叫做 newdir 的子目录。使用什么命令? 

_________________________________________________________________

  1. 输入命令,显示 practice 目录中文件和目录的长列表。创建的目录列出来了吗? 

_________________________________________________________________

  1. 目录的所有者是? 

_________________________________________________________________

  1. 文件的大小是多少? 

_________________________________________________________________

  1. 使用 file 命令确定 newdir 文件的类型。它是哪一类的文件? 

_________________________________________________________________

  1. 如果名字中没有字符 dir,采取别的什么方法来识别出它是一个目录? 

_________________________________________________________________

  1. mkdir 命令创建 3 个目录,目录名分别为 highmedium low,应该使用什么命令? 

________________________________________________________________

  1. ls 命令检查创建是否成功?步骤 17:使用 rm 命令删除文件。 

rm 目录可以删除单个文件或多个文件。可以通过在 rm 命令之后指定文件的名字,或者使用星号 (*) 和问号 (?) 元字符,同时删除几个文件。在 Linux 系统中删除的文件是永远被删除了,除非使用图形界面删除文件,它们才能够被恢复。rm 命令可以带 –i (交互) 选项使用,它在删除文件之前会提示用户。使用 rm -i 命令作为防范,避免误删文件: rm [-i] filename (s)

  1. 使用 rm 命令删除早先在 practice 目录中创建的 newfile 文件,应该使用什么命令? _________________________________________________________________
  2. 输入命令显示 practice 目录中文件的长列表。创建的文件还在吗? 

_________________________________________________________________

  1. 使用带 -i 选项的 rm 命令,删除早先在 practice 目录中创建的 filenew

件。交互式选项起到什么作用? 

_________________________________________________________________

  1. 删除早先创建的三个名为 new1new2 new3 的文件。使用问号 (?) 通配符使用一个命令删除所有三个文件。使用什么命令? 

_________________________________________________________________

  1. 输入命令,显示 practice 目录中文件的长列表。三个文件还在吗? 

_________________________________________________________________

  1. 还有其他的什么方法来删除 new1new2 new3 文件? 

_________________________________________________________________ 步骤 18:使用 rm -r 命令删除目录。 

rm -r 目录用于删除目录。它将删除从目标目录开始的目录,包括所有的子

目录和文件。当 rm 命令带 -r 信息使用的时候,它可以删除单个目录 (空或不空) 或目录树的整节。rm 命令可以带 -i 选项使用,它在删除目录之前会提醒用户: 

rm –r [i] directory_name (s)

  1. 删除早先创建的 newdir 子目录,使用什么命令? 

_________________________________________________________________

  1. 输入命令显示 practice 目录中文件的长列表,创建的子目录还在吗? 

_________________________________________________________________

  1. 改变到早先创建的 mediurn 子目录中,输入什么命令? 

_________________________________________________________________

  1. 删除早先创建的 low 子目录,使用什么命令? 

________________________________________________________________

  1. 用相对路径名和快捷方式,改变回到 practice 子目录中,应使用什么命令? 

_________________________________________________________________

  1. 使用一个命令删除 high medium 子目录,应使用什么命令? 

________________________________________________________________ 步骤 19:练习所学习到的内容。 

通过在 practice 目录中创建一个三级的目录树,练习使用 touchmkdir rm 命令。试着使用有意义的目录名。记住可以使用一个命令创建整个目录结构。在每个目录中创建多个文件。记住可以使用一个命令创建多个文件。结束的时候,请删除实验时创建的文件和目录。步骤 20:关闭终端窗口,注销。 

作为全面安全策略的一个主要组成部分,文件系统安全决定了谁可以访问什么数据,以及他们可以如何处理数据。系统管理员基于用户、属组和权限建立其文件系统安全。目录和文件权限可以使用带 –l (长列表) 选项的 ls (列表) 命令来确定。可以使用 ls -l 命令确定文件类型、权限、所有者和属组。系统将显示文件和目录的权限,解释结果,以评价不同用户类别上权限的作用。 

接着,将使用命令行工具程序分析和修改 Linux 文件系统安全权限。 

文件和目录权限可以使用 chmod (修改模式) 命令修改。在正常情况下,文件或目录的默认权限可以满足大多数安全的需要。可能有很多次想要修改文件或目录的权限。在默认情况下,所有创建的文件设定了允许其他类别的用户可以读取这个文件的权限。这意味着所有拥有登录 id 的人都可以查看和拷贝文件的内容。对于机密文件和私人信息,可以修改文件的权限,防止其他人访问它。 

shell 脚本是想要修改其权限的另一个例子。当创建一个 shell 脚本文件 (或任何文件) 的时候,即使是对文件的所有者/创建者,默认的权限不包括执行权限。为了运行 shell 脚本,必须通过给用户 (所有者) 类别添加执行权限来修改权限。 

后,使用 GNOME (公共桌面环境) 文件管理器来分析和修改文件系统权限。GNOME 文件管理器工具程序提供了一个文件系统的图形化界面,可以用来查看和修改文件和文件夹的权限。 

 3. 确定文件系统权限及在命令行中修改权限步骤 1:开机,登录进入 GNOME 

GNOME 登录框中填写指导老师分配的用户名和口令并登录。 

步骤 2:访问命令行。 

单击应用程序菜单中单击系统工具”-“终端命令,打开终端窗口。 

步骤 3:显示权限。 

权限控制着谁能够对文件系统中的文件和目录做什么。目录和文件权限可以使用带 –l (长列表) 选项的 ls (列表) 命令来确定。ls -l 命令显示目录内容的长列表。如果同时给出 -a 选项,则所有文件,包括隐藏文件和目录 (那些以圆点打头的) 都被显示出来。 

8-2 提供了检查使用 ls -l 目录列出的信息。 

8-2

文件类型 

第一个位置的短横线表示一个普通文件。一个 d 表示目录 

权限 

3 组权限:用户,属组,其他 

链接 

链接到其他文件和目录 

所有者 

创建文件或目录的用户的用户 (登录 ID) ,除非指派了所有权 

属组 

所有者属于的属组名,由系统管理员建立 

大小 

文件按字节计算的大小 

修改日期/时间 

文件创建或 后修改的月、日、年 (如果不是当年) 和时间 

文件名 

文件或目录名 

     意:当使用权限工作的时候,文件类型、权限、所有者、属组和文件/

目录名是列表中 重要的信息片断。 

 

  1. ls 的第一个位置______________对文件或目录列表______________表示文件类型。使用 ls –l 命令列出主文件夹中的文件。/bin/cat 的文件类型是什么 (文件或目录) ?如何知道的? 

_________________________________________________________________

  1. 接下来的 ls 中的 9 个位置______________表示文件的权限。所有文件或目录可能的权限是:(小写) rwx 或短横线 (-) cat 的权限是什么? 

_________________________________________________________________ 步骤 4:解释权限。 

请参阅表 8-3,回答下面的问题。注意到权限的解释对于文件和目录是不同的。 

  1. 文件的 r 权限的意义是什么? 

______________________________________________________________

_________________________________________________________________

  1. 目录的 r 权限的意义是什么? 

_________________________________________________________________ _________________________________________________________________

 

8-3  普通文件目录的权限符号 

权限 

符号 

普通文件 

目录 

 

r

文件可以被显示或者拷贝 (拷贝的文件属于新的所有者) 。只有读权限不能够删除或移动文件 

可以使用 ls 命令列出内容。必须也有使用 ls 命令选项的执行权限 

 

w

文件可以被修改、移动和删除 (只有当目录给出写权限的时候)

文件可以被添加或删除。目录必须也有执行权限 

执行 

x

文件可以被执行 (shell 脚本或者可执行文件)

控制对目录的访问。用户可以 cd 到目录中,使用读访问列出其中的内容。文件可以使用写访问被移动或者拷贝到目录中 

无权限 

-

短横线表示权限被拒绝 

短横线表示权限被拒绝 

 

  1. 文件的 x 权限的意义是什么? 

_________________________________________________________________

  1. 文件的 w 权限的意义是什么? _________________________________________________________________
  2. 目录的 w 权限的意义是什么? 

_________________________________________________________________

  1. 权限位置的短横线 (-) 的意义是什么? 

_________________________________________________________________ 步骤 5:确定文件的用户权限。 

九个权限被分成三个权限一组。每组的三个权限总是按照 r ()w()x(执行)的顺序排列。如果权限不允许,短横线 (-) 将在它的位置。第 1 组三个权限是用户的权限组。这些权限决定其所有者能够做什么。在主目录中创建一文件 dante。在应用程序菜单中单击附件”-“文本编辑

命令,在文本编辑中键入适当内容并保存为 dante 

  1. 谁是 dante 文件的所有者? 

_________________________________________________________________

  1. 用户权限的头 2 个字符是什么? 

_________________________________________________________________

  1. 在用户的权限组中,第 3 个位置是什么? 

_________________________________________________________________

  1. 列出根据所给的文件权限,用户 (所有者) 能够做的 4 件事情。 

_________________________________________________________________ 步骤 6:确定文件的属组权限。 

系统管理员给每一个用户分配了一个主属组。这个文件所有者所在的属组是一个当该文件创建时,随所有者一起分配的成员。第 2 组的 3 个权限决定了主属组的成员能够做什么。 

  1. dante 文件的所有者是什么主属组的成员? 

_________________________________________________________________

  1. 属组权限的第一个字符是什么? 

_________________________________________________________________

  1. 这允许属组的其他成员对文件进行什么操作? 

_________________________________________________________________

  1. 为什么在接下来的第二和第三个位置不是 w x,而是短横线? 

_________________________________________________________________ 步骤 7:确定文件的其他 (公共) 权限。 

后一组字符,叫做其他权限,是其他每一个人都有的权限。其他指既不是

文件所有者也不是文件所有者所在属组的成员,但是有权访问系统的所有人。不是所有者和属组的其他人对 dante 文件有什么权限? 

_________________________________________________________________ 步骤 8:确定可执行文件的文件权限。 

可执行文件,例如 Linux 工具程序和脚本文件,对于想执行命令或者脚本的所有人要求 (执行) 权限。 

  1. 从主目录中显示在 /usr/bin 目录中的 bc 文件的长目录列表。使用什么命令? 

_________________________________________________________________

  1. 文件的权限是什么? 

________________________________________________________________

  1. 用户权限是什么? 

_________________________________________________________________

  1. 属组权限是什么? 

_________________________________________________________________

  1. 其他权限是什么? 

_________________________________________________________________

  1. 为什么组属用户和其他用户类别没有 w () 权限? 

_________________________________________________________________ 步骤 9:使用默认权限创建一个新文件。 

使用默认权限创建新文件。使用 touch 命令在主目录中创建一个新文件。 

  1. 在主目录中创建一个叫做 newfileperms 的新文件。使用什么命令和路径名? 

_________________________________________________________________

  1. 查看 newfileperms 的权限。使用什么命令和路径名? 

_________________________________________________________________

  1. 分配给这个文件的默认权限是什么? 

_________________________________________________________________

  1. 谁是所有者? 

_________________________________________________________________

  1. 谁是主属组? 

_________________________________________________________________

  1. 主属组的成员能够重新命名这个文件吗? 

_________________________________________________________________ 步骤 10:使用默认权限创建一个新目录。 

也可以使用不同的默认权限创建新目录。使用 mkdir 命令在主目录中创建一

个新目录。 

  1. 在主目录的 practice 目录中创建一个叫做 newdirperms 的新目录。使用什么命令和路径名? 

_________________________________________________________________

  1. 列出主目录中的内容,查看 newdirperms 的权限。使用什么命令和路径名?(注:先退回到主目录的上级目录中。)

_________________________________________________________________

  1. 分配给这个目录的默认权限是什么? 

_________________________________________________________________

  1. 谁是所有者? 

_________________________________________________________________

  1. 谁是主属组? 

_________________________________________________________________

  1. 一个主属组的成员能够在这个目录中添加文件吗? 

_________________________________________________________________ 步骤 11:回顾 chmod 命令模式。 

chmod (修改模式) 命令被文件所有者 (或超级用户) 用来修改文件权限。 chmod 命令的 2 种操作模式是符号 (或相对) 和八进制 (或绝对) chmod 命令通用的格式是: 

  chmod mode filename 模式部分会根据是使用符号模式还是使用八进制模式而做修改。 

· 符号模式:使用字母与符号的组合,为不同类别的用户添加或删除权限。符号模式也叫做相对模式。 

· 八进制模式:使用数字表示文件权限。八进制模式也叫做绝对或者数字模式。 

  1. 哪种 chmod 模式使用数字表示文件权限? 

_________________________________________________________________

  1. 哪种 chmod 模式使用字母 (或符号) 表示权限? 

_________________________________________________________________ 步骤 12:使用符号模式修改文件权限。 

当使用符号模式设定权限的时候,尽管会同时给所有类别的用户同样的权限,一般只对一种类别的用户工作。这个模式叫做相对模式,因为正在相对已经存在的权限进行分配或删除权限。可以给一个特定类别的用户添加或删除一个或多个权限,或者把权限都取走。符号模式的命令格式使用字母和符号。命令格式的模式部分由 3 个部分组成: 

· 谁:要处理的用户的类别:u = 用户,g = 属组, o = 其他,还有 a = 所有。 

· 操作:操作或者想做的事情:设定 (=) ,删除 (-) ,给予 (+)  

· 权限:分配的权限:r = 读,w = 写,还有 x = 执行。下面的例子删除 (-) 了文件 dante 中其他 (o) 类别用户的读 (r) 权限。 

意:在 o 短横线 (-) r 之间没有空格。   chmod o-r dante

下面的例子给文件dante添加 (+) 了属组 (g) 和其他 (o) 类别用户的写 (w)

权限: 

  chmod go+w dante

  1. 在主目录中使用相对路径名创建一个叫做 chmoddir 的新目录。使用什么命令创建目录? 

    

____________________________________________________________________

  1. 改变到 chmoddir 目录中,创建一个叫做 symfile 的新文件。使用什么命令创建文件? 

    

____________________________________________________________________

  1. 使用 ls -l 命令来确定 symfle 的权限。这些是文件的默认权限。用户、属组和其他的权限是什么? 

    

____________________________________________________________________

  1. 决定不想让其他用户 (除了自己和属组成员之外) 能够查看 symfle 的内容和拷贝 symfle。使用 chmod 命令,在符号模式下,删除其他用户对于文件 symfle 的读权限。使用什么命令? 

    

____________________________________________________________________

  1. 再次列出文件的权限。其他类别用户的权限现在是什么? 

    

____________________________________________________________________

  1. 如果想只使用一个命令删除属组和其他类别的读权限,使用什么命令? 

    

____________________________________________________________________ 步骤 13:使用符号模式修改目录权限。 

  1. 改变回到主目录,使用什么命令? 

    

____________________________________________________________________

  1. 从主目录中列出早先创建的新的 chmoddir 目录的权限。这些是目录的默认权限。用户、属组和其他的权限是什么? 

    

____________________________________________________________________

  1. 除了自己或者属组成员以外的其他用户能够从 chmoddir 目录中拷贝文件吗? 

    

____________________________________________________________________ 为什么能或者为什么不能? 

    

____________________________________________________________________ 4) 不想让其他用户从 chmoddir 目录中拷贝文件。改变到主目录中,使用 chmod 命令,在符号模式下,删除其他类别用户对于目录 chmoddir 的读和执行权限。使用什么命令? 

    

____________________________________________________________________

  1. 再次列出目录的权限。其他类别用户的权限是什么? 

    

____________________________________________________________________

  1. 主属组成员能够在你的 chmoddir 目录中创建或者拷贝文件吗?为什么能或者为什么不能? 

    

____________________________________________________________________

  1. 改变到主目录,使用 chmod 命令,在符号模式下,给主属组去除对目录 chmoddir 的读权限。使用什么命令? 

    

____________________________________________________________________

  1. 通过使用符号模式把权限改回默认的权限。使用什么命令?(提示:属组和权限可以使用一个命令组合,或者可以使用 2 个分离的命令。)

    

____________________________________________________________________ 步骤 14:确定八进制权限。 

八进制模式为同时修改所有类别用户的权限提供了一个快捷的数字方式,而且仍然允许每组权限可以是不同的。有 3 组可能的权限 (rw x) ,对应于每种用户类别 (用户、属组或其他) 。每组权限可以根据允许的权限使用数字 (0 7) 来分配。 

r () 权限指派为数字 4w () 权限指派为数字 2x (执行) 权限指派为

数字 1。通过添加数字,我们得到一个总的对应于用户类别 (用户、属组或其他) 3 个权限。例如,如果用户 (所有者) 对于文件的权限是 rwx,我们把 4 () 加上 2 ()加上 1 (执行) 得到 7。如果属组有 rw 权限,它们得到 4+2+0 (没有执行权限) 的总数 6。如果其他只有 r 权限,它们得到 4+0+0 (没有写和执行权限) 的总数 4。这个文件或目录的 octal_mode 764 

1) 通过把字符权限转换成等效的八进制,填入表 8-4。先转换每组权限 (用户、属组或其他) ,然后在八进制模式权限下输入 octal_mode (3 个数字)  

8-4  实验记录 

用户权限 

八进制数 

属组权限

八进制数

其他权限

八进制数

八进制模式权限 

r w x

  

r w -

  

r w -

  

  

r w -

  

r - -

  

r - -

  

  

r - -

  

r - -

  

r - -

  

  

r w x

  

r – x

  

r – x

  

  

     步骤 15:使用八进制模式修改文件权限。 

使用八进制模式,没有必要指定用户的类别,因为每个数字的位置表示了三种用户类别中的一种。Octal_mode 由三个数字组成,每个都是一种用户类别 (用户、属组或其他) 的总和。八位数值组合在一起标识了用于 chmod 命令的 octal_mode 

  chmod octal_mode filename

  1. 改变到 chmoddir 目录中,创建一个叫做 octfile 的新文件。使用什么命令创建文件? 

    

____________________________________________________________________

  1. 使用 ls -l 命令来确定 octfile 的权限。这些是文件的默认权限。什么是用户、属组和其他的数字权限? 

    

____________________________________________________________________

  1. 与这个文件的用户、属组和其他权限等同的八进制模式是什么? 

    

____________________________________________________________________

  1. 决定不想让其他用户能够查看或者拷贝 octfile 文件的内容。使用 chmod 命令,在八进制模式下,删除其他用户对于 octfile r () 权限。使用什么命令? 

    

____________________________________________________________________

  1. 再次列出文件的权限。其他用户类别现在的权限是什么? 

    

____________________________________________________________________

  1. 如果想只使用一个命令,删除属组和其他类别的所有权限,使用什么命令? 

    

____________________________________________________________________ 步骤 16:使用八进制模式修改目录权限。 

下面的格式用于修改目录的权限。-R (递归) 选项修改指定目录的权限,包

括其中的所有子目录和文件。 

  chmod [-R] octal_mode directoryname

  1. 改变到主目录。使用什么命令? 

    

____________________________________________________________________

  1. 从主目录中列出 chmoddir 目录的权限。这些是目录的默认权限。对于用户、属组和其他,数字权限是什么? 

    

____________________________________________________________________

  1. 什么是和用户、属组和其他对于这个目录等同的八进制模式? 

    

____________________________________________________________________

  1. 使用 chmod 命令,在八进制模式下,删除其他类别用户对于目录 chmoddir 的读和写权限。使用什么命令?(记住:在八进制模式下,必须总是指定所有的 3 组权限,即使它们可能没有被修改。)

    

____________________________________________________________________

  1. 再次列出目录的权限。其他用户类别的权限现在是什么? 

    

____________________________________________________________________ 用户和属组的权限保持不变吗? 

    

____________________________________________________________________

  1. 全属组 (核心组) 成员能够在 chmoddir 目录中创建新文件或者拷贝文件吗?为什么能或者为什么不能? 

    

____________________________________________________________________

  1. 决定让属组成员能够从你的目录中拷贝文件。改变到主目录,使用 chmod 命令,在八进制模式下,给主属组 (核心组) 添加对目录 chmoddir 的写权限。用户有权限 rwx,属组应该有权限 rw,其他应该对目录没有权限。使用什么命令? 

    

____________________________________________________________________

  1. 使用八进制模式把权限改回默认权限 (rwxr-xr-x)  

    

____________________________________________________________________ 步骤 17:创建一个脚本文件,使它能够执行。 

在本步骤中,将使用 vi 编辑器创建一个简单的文本脚本文件。为了能够运行脚本文件,然后需要使它可以执行。脚本文件在帮助自动重复作业的时候非常有用。 

  1. 改变到 chmoddir 目录,使用 vedit 启动 vi 编辑器。当启动编辑器的时候,指定或者打开一个新文件叫做 myscript。按下 i 进入插入输入模式,以小写文本键入下面的命令。在每行后按回车键。 

  echo  “hello!”

  1. 按下 ESC 键,返回命令模式,然后输入一个冒号,进入末行模式。按下

wq 来写入 (保存) 文件,然后退出 vi 

    

____________________________________________________________________

  1. 列出文件,确定它的权限。它们是什么? 

    

____________________________________________________________________

  1. 键入 ./myscript,就像它是一个命令,然后按下回车键。命令的响应是什么? 

_________________________________________________________________

    

  1. 修改 myscript 的权限,这样用户权限包括 x (执行) ,作为所有这可以执行或运行文件。可以使用符号模式或者八进制模式。使用什么命令修改权限? 

    

____________________________________________________________________

  1. 列出文件,检查修改的权限。用户 (所有者) 现在的权限是什么? 

    

____________________________________________________________________

  1. 再次把 myscript 作为一个命令键入,然后按下回车键。命令的响应是什么? 

_________________________________________________________________ 步骤 18:删除在本实验中创建的文件和目录。删除在本实验过程中在你的主目录中创建的所有文件和目录。 

步骤 19:关闭终端窗口,注销。 

 

实验九 用户接口实验

实验目的

1.理解面向操作命令的接口 Shell

2.学会简单的 Shell 编程 

3.理解操作系统调用的运行机制 

4.掌握创建系统调用的方法 

操作系统给用户提供了命令接口和程序接口(系统调用)两种操作方式,用户接口实验也因此而分为两大部分。首先要熟悉 Linux 基本命令,并在此基础上学会简单的 shell 编程方法。然后通过向 Linux 内核添加一个自己设计的系统调用,来理解系统调用的实现方法和运行机制。在本次实验中, 具吸引力的地方是:通过内核编译,将一组源代码编程操作系统内核,并由此重新引导系统,这对我们初步了解操作系统的生成过程极为有利。 

准备知识

为了使用户通过操作系统完成各项管理工作,操作系统必须为用户提供各种接口来实现人机交互。经典的操作系统理论将操作系统的接口分为控制台命令和系统调用两种。前者主要提供给计算机的操作人员对计算机进行各种控制;儿后者则提供给程序员,使他们可以方便地使用计算机中的各种资源。 

9.1 控制台命令接口 

操作系统向用户提供一组控制台命令,用户可以通过终端输入命令的方式获得操作系统的服务,并以此来控制自己作业的运行。一般来讲,控制台命令应该包含:一组命令、终端处理程序以及命令解释程序。 

1.bash 的由来 

当登录 Linux 或者打开一个 xterm 时,当前默认 shell 就是 bashBash GNU Project shellGNU Project 是自由软件基金会(Free Software Foundation)的一部分,它对 Linux 下的许多编程工具负责。bashBourne Again Shell)是自由软件基金会发布的 Bourne shell 的兼容程序。它包含了其它优秀 shell 的许多良好特性,功能非常全面。很多 Linux 版本都提供 bash 

2.bash 的大致原理 

bash 在处理自己的脚本时,先找到需要处理的命令名称,进而在当前用户的默认命令目录中找到对应的命令,这些默认目录一般是/usr/bin/bin /sbin。在执行这些命令时,先使用进城创建系统调用 fork(),再使用 exec()来执行这些命令。 

3.建立 bash 脚本 

  1. 编辑文件 

可以用 熟悉的编辑器来编辑这个文本文件,比如文件名为 script,在 shell 下输入: 

$vi script

#! /bin/bash

echo Hello world!

然后保存退出。 

  1. 测试脚本使用指令: 

$source script

  1. 更改脚本属性。 

使用指令: 

$chmod a+x script

将脚本设置为可执行。 

  1. 执行脚本。 

$./script

 

关键字参考 

echo     在终端上显示 

bash     特殊变量 1~9,保存当前进程或脚本的钱 9 个参数 ls       列举文件 wc      统计数量 

function  定义函数 

9.2 添加系统调用 

系统调用是操作系统为程序设计人员提供的接口服务。使用系统调用,程序员可以更充分地利用计算机资源,使编写的程序更加灵活,功能更加强大。程序员在对系统充分了解的情况下甚至可以自己订做系统调用,实现那些非专业程序员所难以实现的功能。 

1.添加源代码 

第一个任务是编写添加到内核中的源程序,即添加到内核文件中的一个函数。该函数的名称应该是在新的系统调用名称之前加上 sys_标志。假设新加的系统调用为 foo(),功能为原值返回输入的整型数。格式为 int foo(int iNumber),返回的值就是输入的参数。在/usr/src/linux/kernel/sys.c 文件中添加源代码,如下图所示: 

asmlinkage int sys_foo(int x)

{

printf(“%d\n”,x);

}

注意:目录“/usr/src/linux”是 Linux 各个版本的统称,它因系统内核的版本不同而名称不同。例如,当前操作系统是 Linux 7.1 其内核是 Linux-2.4.2,所以在“usr/src”目录下有两个文件:Linux-2.4 Linux-2.4.2,其中 Linux-2.4 Linux-2.4.2 的连接文件,程序员可以进入任何一个目录,它对内核的修改都是一样的。 

2.连接新的系统调用 

添加新的系统调用之后,下一个任务是让 Linux 内核的其余部分知道该程序

的存在。为了从已有的内核程序中添加新的函数的链接,需要进行下面的操作: 

  1. 进入目录/usr/src/linux/include/asm-i386/,打开文件 unistd.h。这个文件包含了系统调用清单,用来给每个系统调用分配一个惟一的编号。系统调用的定义格式如下: 

#define _NR _name NNN

其中,name 以系统调用名称代替,而 NNN 是该系统调用对应的号码。应该将新的系统调用名称放到清单 后,并给它分配已经用到的系统调用号后面的一个号码,比如: 

#define _NR _foo 222

以上的系统调用号便是 222.Linux 内核自身的系统调用号已经用到 221

了。如果读者还要自行增加系统调用,就必须从 223 开始。 

  1. 进入目录/usr/src/linux/arch/i386/kernel/,打开文件 entry.S。该文件中有类似下面的清单: 

ENTRY(sys_call_table)

.long SYSMBOL_NAME(sys_ni_syscall)

.long SYSMBOL_NAME(sys_exit)

.long SYSMBOL_NAME(sys_fork)

在该表的 后加上: 

.long SYSMBOL_NAME(sys_foo)

3.重新编译内核 

为了使新的系统调用生效,需要重建 Linux 的内核。首先必须以 root 身份登录。进入目录/usr/src/linux。重建内核: 

[root@Linuxserver root]#make menuconfig       //配置新的内核 

[root@Linuxserver root]#make dep             //创建新内核 

[root@Linuxserver root]#make modules_install   //加入模块 

[root@Linuxserver root]#make clean           //清除多余创建的文件 

[root@Linuxserver root]#make bzImage        //生产可执行内核引导文件 

4.使用新编译的内核 

cp –a /usr/src/linux-2.4.2/arch/i386/boot/bzImage /boot

5.重新配置/etc/lilo.conf 文件使用 vi 编辑器编辑/etc/lilo.conf 文件: vi /etc/lilo.conf

在其中加入如下几行: 

image=/boot/bzImage  #启动内核的位置,即自己新配置的内核所在目录 

label=xhlinux       #给内核起一个名称,配置完成,重新启动的时候,显示这个名称 

                  #用户可以选择该项,重新启动后,系统将进入你新配置的内核进行引导 

read_only         #定义硬盘的启动位置是/dev/hda5,在该设计中没有变 

root=/dev/hda5    #仿照以前的内核引到位置,不用修改,用以前的就可以。 

6.重启系统 

完成以上配置后,重新启动系统进入自己的新系统。 

9.3 实验内容 

9.3.1 控制台命令接口实验 

该实验是通过“几种操作系统的控制台命令”,“终端处理程序”、“命令解释程序”和“Linux 操作系统的 bash”来让实验者理解面向操作命令的接口 SHELL 和进行简单的 shell 编程。 

  1. 查看 bash 版本。 
  2. 编写 bash 脚本,统计/my 目录下 C 语言文件的个数。 

9.3.2 系统调用实验 

该实验是通过实验者对“Linux 操作系统的系统调用机制”的进一步了解来理解操作系统调用的运行机制:同时通过“自己创建一个系统调用 mycall()”和

“变成调用自己创建的系统调用”进一步掌握创建和调用系统调用的方法。 

  1. 编程调用一个系统调用 fork(),观察结果 
  2. 编程调用创建的系统调用 foo(),观察结果 
  3. 自己创建一个系统调用 mycall(),实现功能:显示字符串到屏幕上 
  4. 编程调用自己创建的系统调用 

9.4 控制台命令接口实验指导 

1.查看 bash 版本 

shell 提示符输入: 

$echo $BASH_VERSION

2.编写 bash 脚本:统计/my 目录下 c 语言文件的个数 

通过 bash 脚本,可以有多种方式实现这个功能,而使用函数是其中一个选择。在使用函数之前,必须先定义函数。 

1) 进入自己的工作目录,用 vi 编写名为 count 的文件: cd /home/student   #home/student 目录下编程 

vi count

下面是脚本程序: 

#! /bin/bash

function count

{

echo –n “Number of matches for $1: ”  #接收程序的第一个参数 ls $1|wc –l      #对子程序的第一个参数所在的目录进行操作 

}

2) 执行。 

count 文件复制到当前目录下,然后在当前目录下建立文件夹 my 

mkdir my cd my

vi 1.c      #my 目录下建立几个 c 文件,以便用来程序测试 

cd … chmod +x count count ./my/*.c

9.5 系统调用实验指导 

1.编程调用一个系统调用 fork()

在应用程序中调用系统调用 fork()非常简单,下面的程序可以很清楚地显示出由 fork()系统调用生成子程序,而产生的分叉作用: 

#include<stdio.h>    int main( )

{    int iUid;    iUid=fork( );    if(iUid==0)       for(;;) {           printf(“This is parent.\n);           sleep(1);             } if(iUid>0)     for(;;){        printf(“This is child.\n”);         sleep(1);

} if(iUid<0)

   printf(“Can not use system call.\n”); return 0;

}

下面是可能得到的一种结果: 

this is child. this is parent. this is child. this is parent. this is parent. this is child. this is child. this is parent. this is parent. this is child. this is child this is parent. this is parent. this is child

2.编程调用创建的系统调用 foo()

foo()是本实验系统设计者自行添加的一个很简单的系统调用,其实现过程在

前面已介绍了,它的功能很简单,就是向标准输出一个特定的证书。程序 test.c 如下: 

#include<stdio.h>

#include<linux/unistd.h> _syscall1(char*,foo,int,ret) main( ) {   int I,J;   I=100;

  J=0;   J=foo(I);   printf(“This is the result of new kernel\n”);   printf(“%d”,j);

}

程序编译: 

gcc –o –I /usr/src/linux-2.4.2 /include test.c

注意:由于需要引入内核头文件 unistd.h,不能简单地用普通的编译命令,

而应该这样设置参数。 

3) 运行测试程序 

     ./test

解释:当函数还没有定义在内核中时,如果运行以上程序,系统将显示一个未定义的值-1;而在内核中定义了以后,运行该程序,系统将显示 100,说明我们的内核添加系统调用已经成功。 

4.创建系统调用 mycall(),实现功能:显示字符串到屏幕上(1)编写系统调用相应函数。 

/usr/src/linux/kernel/sys.c 文件中添加如下代码: 

asmlinkage void mycall(char *str)

{  printk(“%s\n”,str);

}

注意: 上面的例子中,有一个很少见的特殊函数 printk(),它的功能与 printf() 类似。之所以不用 printf()的原因在于,它不是内核的函数。要在内核实现在控制台显示字符串的功能,就必须以内核所提供的 printk()函数来代替。printk()printf()的地方是:printk()会把输出的结果送到内核的缓冲区里面, 终调用控制台的写函数将其打印出来。(2)添加系统调用号。 

打开文件/usr/src/linux/include/asm-i386/unistd.h,添加如下一行: 

#include _NR_mycall 223  //因为_NR_foo 222,所以这个只能用 223  

3) 改动系统调用表。 

打 开 文 件 /usr/src/linux/arch/i376/kernel/entry.s , 在 “ .long

SYMBOL_NAME(sys_foo)”下面添加一行: 

.long SYMBOL_NAME(sys_mycall)

(4) 重新编译内核。 

对重新编译内核和使用新内核引导熟悉的同学,可以不再阅读第(4)步和第(5)步。以 root 身份登录。进入目录/usr/src/linux,重建内核: 

[root@linuxserver root]# make menuconfig     //配置新内核 

[root@linuxserver root]# make dep           //创建新内核 

[root@linuxserver root]# make modules_install  //加入模块 

[root@linuxserver root]# make clean          //清除多余创建的文件 

[root@linuxserver root]# make bzImage        //生成可执行内核引导文件 

编译完毕之后,新的内核引导文件在目录/usr/src/linux/arch/i386/boot/

中,叫 bzImage。把它复制到/boot/下面: 

[root@linuxserver root]# cp /usr/src/linux/arch/i386/boot/bzImage /boot/

(5)用新的内核引导。 

这需要修改文件/etc/lilo.conf,先打开该文件,参见 5.2.5 节进行修改。修改完成后,存盘退出,运行命令: 

[root@linuxserver root]# /sbin/lilo

(6)重新启动系统。 

4.编程调用自己创建的系统调用 

系统调用 mycall()是读者自己创建的,自己应该对系统调用的参数表非常熟悉。然而使用这个系统调用还需要注意:系统调用转换宏。这个转换宏是将系统调用命令转换为对应参数的 INT80 中断请求,一般的格式是 syscallN(),这个 N 可以为 0~6,对应于系统调用参数的个数,此处 N=1.下面是一个简单的源代码 test1.c:

#include <linux/unistd.h> _syscall1(char*, mycall, int,ret) int main()   {       char *str;       char string[50];       str=string;       str=”This string will be displayed.”;       mycall(str);       return 0;

}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
西北工业大学计算机操作系统实验geekos是一项重要的实践性课程,旨在帮助学生深入学习和了解计算机操作系统的原理和实践应用。该实验基于geekos操作系统,通过一系列实验项目,让学生亲自动手完成操作系统的设计与实现。 在这个实验中,我们首先学习了操作系统的概念和基本原理,包括进程管理、内存管理、文件系统等。然后,我们使用geekos操作系统作为实验平台,通过编码实现一些基本功能。这些功能可能包括进程调度算法的实现、内存分配策略的设计、I/O设备的驱动程序编写等。 在完成这些实验项目时,我们将理论知识与实践相结合,通过编码实现来加深对操作系统的理解和掌握。通过与同学们的讨论和指导,我们能够更好地解决实际问题,提高编程能力和操作系统设计的能力。 实验中,我们还会面临一些挑战和困难。有时候,我们可能会遇到一些奇怪的错误,需要通过调试来找到问题的根源。有时候,我们也需要针对一些限制条件进行设计,如资源的有限性、多进程之间的并发问题等。但是,通过这些挑战和困难,我们能够更加深入地理解操作系统的工作原理。 总的来说,西北工业大学计算机操作系统实验geekos是一门非常有价值和意义的课程。通过在实验中亲自动手设计和实现操作系统的功能,我们不仅能够加深对操作系统的理解,也能够提高我们的编程能力和问题解决能力。这对于我们未来的学习和职业发展都有着重要的意义。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值