操作系统实验报告

瘦欲@ 提交于 2020-08-05 04:34:20

操作系统实验报告

1. myecho.c

1.1. 实验内容

  • myecho.c的功能与系统echo程序相同
  • 接受命令行参数,并将参数打印出来

1.2. 效果展示

myecho$ ./myecho a b c
a b c

1.3. 实验思路和关键代码

读取输入的参数,按顺序输出

int main(int argc, char *argv[])
{
    for (int i = 1; i < argc; i++)
    {
        printf("%s ", argv[i]);
    }
    printf("\n");
    return 0;
}

2. mycat.c

2.1. 实验内容

  • mycat.c的功能与系统cat程序相同
  • mycat将指定的文件内容输出到屏幕
  • 要求使用系统调用open/read/write/close实现

2.2. 效果展示

mycat$ ./mycat mycat.c

//以下显示mycat.c文件的内容

2.3. 实验思路和关键代码

  • 读取参数, 根据参数打开相应文件
    FILE *fp;
    int fsize;
    int fr;
    char *buffer;

    fp = fopen(argv[1], "r");
    if (!fp)
    {
        printf("不能打开该文件\n");
        exit(0);
    }
  • 通过设置文件指针读取文件大小,分配缓冲区
    fseek(fp, 0, SEEK_END);
    fsize = ftell(fp);
    rewind(fp);

    buffer = (char *)malloc((1 + fsize) * sizeof(char));
    if (!buffer)
    {
        printf("分配空间失败\n");
        exit(0);
    }
  • 将文件读取到缓冲区并输出
    fr = fread(buffer, 1, fsize, fp);
    if (!fr)
    {
        printf("读文件失败\n");
        exit(0);
    }

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

3. mycp.c

3.1. 实验内容

  • mycp.c的功能与系统cp程序相同
  • 将源文件复制到目标文件
  • 要求使用系统调用open/read/write/close实现

3.2. 效果演示

mycp$ ./mycp mycp.c mycp1.c

//将mycp.c的内容复制到了mycp1.c

3.3. 实验思路和关键代码

  • 与2.3.中相同, 不过在将源文件写到缓冲区后, 需要打开/创建目的文件, 将缓冲区内容写到目的文件
    fp = fopen(argv[2], "w");
    if (!fp)
    {
        printf("打开目的文件失败\n");
        exit(0);
    }

    fwrite(buffer, 1, fsize, fp);

4. mysys.c

4.1. 实验内容

实现函数mysys,用于执行一个系统命令,要求如下:

  • mysys的功能与系统函数system相同,要求用进程管理相关系统调用自己实现一遍
  • 使用fork/exec/wait系统调用实现mysys
  • 不能通过调用系统函数system实现mysys

4.2. 效果演示

测试程序

int main()
{
    printf("---------------------------------\n");
    mysys("echo a b c d");
    printf("---------------------------------\n");
    mysys("ls /");
    printf("---------------------------------\n");
    return 0;
}

输出结果

---------------------------------
a b c d
---------------------------------
bin  boot  dev  etc  home  init  lib  lib32  lib64  libx32  media  mnt  opt  proc  root  run  sbin  snap  srv  sys  tmp  usr  var
---------------------------------

4.3. 实验思路和关键代码

通过在子进程中使用execl()函数调用sh命令实现简单的系统命令调用

    pid = fork();
    if (pid == 0)
        execl("/bin/sh", "sh", "-c", str, NULL);
    wait(NULL);

sh命令是shell命令语言解释器,执行命令从标准输入读取或从一个文件中读取。通过用户输入命令,和内核进行沟通


5. sh3.c

5.1. 实验内容

  • 该程序读取用户输入的命令,调用函数mysys执行用户的命令
    • 考虑如何实现内置命令cd、pwd、exit
  • 实现文件重定向
  • 实现管道
    • 只要求连接两个命令,不要求连接多个命令
    • 不要求同时处理管道和重定向

5.2. 效果演示

  • 打开后自动显示工作路径(pwd)
sh$ ./sh3

当前工作目录是: /计算机操作系统/实验/homework/sh
>
  • 读取指令并执行
当前工作目录是: /计算机操作系统/实验/homework/sh
> echo a b c
a b c

当前工作目录是: /计算机操作系统/实验/homework/sh
>
  • cd指令
当前工作目录是: /计算机操作系统/实验/homework/sh
> cd /

当前工作目录是: /
> ls
bin  boot  dev  etc  home  init  lib  lib32  lib64  libx32  media  mnt  opt  proc  root  run  sbin  snap  srv  sys  tmp  usr  var
  • exit指令
当前工作目录是: /
> exit
已经退出shell
  • 重定向
当前工作目录是: /计算机操作系统/实验/homework/sh
> echo a b c >out.txt

out.txt文件内容

a b c

  • 管道
当前工作目录是: /计算机操作系统/实验/homework/sh
> cat /etc/passwd | wc -l
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
// 内容过多以下省略

5.3. 实验思路和关键代码

  • 实验的要求是从sh1到sh3逐渐增加的,其代码也是从简单到复杂逐步构建

字符串分割

  • 由于要处理输入的命令,所以我们要进行字符串的分割
    在实验过程中分析需求,决定构建以下数据结构,储存命令的缓冲区和需要进行分割得到的结构体
#define BUF_LEN 1024
char buffer[BUF_LEN];

#define ARGV_LEN 32
struct Command
{
    int argc;       //分割串的数量
    int redirectF;  //重定向标记
    int pipLineF;   //管道标记
    char *argv[ARGV_LEN];
};
struct Command command;
  • 对命令进行分割, 使用strtok函数

C 库函数 char *strtok(char *str, const char *delim) 分解字符串 str 为一组字符串,delim 为分隔符。

  • 对命令进行分割时,需要判断是否有’>‘重定向和’|‘管道命令。由于实验中并未同时支持重定向和管道命令,所以判断到’>‘或’|'时直接将它们对应的标记位置为其在char* argv[]中的位置
  • 分割部分代码
    //进行分割
    char *buf;
    buf = strtok(buffer, " ");
    while (buf != NULL)
    {
        if (buf[0] == '>')
        {
            command.argv[command.argc] = NULL;
            command.redirectF = command.argc;
            command.argc++;
            command.argv[command.argc] = (char *)malloc(sizeof(char) * (strlen(buf) + 1));
            memcpy(command.argv[command.argc], buf + 1, strlen(buf));
            command.argc++;
        }
        else
        {
            command.argv[command.argc] = (char *)malloc(sizeof(char) * (strlen(buf) + 1));
            memcpy(command.argv[command.argc], buf, strlen(buf) + 1);
            if (!strcmp("|", buf))
            {
                command.pipLineF = command.argc;
            }
            command.argc++;
        }
        buf = strtok(NULL, " ");
        if (command.argc >= ARGV_LEN)
        {
            perror("指令分割过多\n");
            exit(-1);
        }
    }
    command.argv[command.argc - 1][strlen(command.argv[command.argc - 1]) - 1] = 0; //去掉换行
    command.argv[command.argc] = NULL;

sh1.c

  • 在sh1.c中,不要求实现重定向和管道,但使用上个实验中的mysys并不合适,这里做了重新的实现,将execl函数换成exevp函数

int execvp(const char* file, const char* argv[])

  • 第一个参数是要运行的文件,会在环境变量PATH中查找file,并执行
  • 第二个参数,是一个参数列表,如同在shell中调用程序一样,参数列表为0,1,2,3……因此第0个参数,需要重复一遍
  • argv列表最后一个必须是 NULL.
  • 失败会返回-1, 成功无返回值,但是,失败会在当前进程运行,执行成功后,直接结束当前进程,可以在子进程中运行
  • 实现代码
void mysys()
{
    pid_t pid;

    pid = fork();
    if (pid == 0)
    {
        execvp(command.argv[0], command.argv);
    }
    wait(NULL);
}
  • sh1需要实现内置的pwd,cd和exit命令
  • 通过getcwd函数获取工作路径

char *getcwd( char *buffer, int maxlen );
getcwd()会将当前工作目录的绝对路径复制到参数buffer所指的内存空间中,参数maxlen为buffer的空间大小。成功则返回当前工作目录,失败返回 FALSE

实现代码

    memset(buffer, 0, BUF_LEN);                 //清空缓冲区
    char *cwd = getcwd(buffer, sizeof(buffer)); //获取工作路径 实现pwd
    if (NULL == cwd)
    {
        perror("获取工作目录失败\n");
        exit(-1);
    }
    else
    {
        printf("当前工作目录是: %s\n", cwd);
    }
  • cd命令的实现, 通过chdir函数

int chdir(const char * path);
chdir()用来将当前的工作目录改变成以参数path 所指的目录
行成功则返回0, 失败返回-1, errno 为错误代码

    //实现cd
    else if (command.argv[0] != NULL && !strcmp("cd", command.argv[0]))
    {
        if (command.argv[1] == NULL)
        {
            perror("参数错误\n");
        }
        else
        {
            chdir(command.argv[1]);
        }
    }
  • 内置的exit命令
    if (command.argv[0] != NULL && !strcmp("exit", command.argv[0]))
    {
        printf("已经退出shell\n");
        exit(0);
    }

sh2.c

  • 在sh2.c中添加重定向的功能,其中要使用到dup2()函数

int dup2(int oldfd, int newfd);
通过复制文件描述符oldfd,创建一个新的文件描述符newfd, newfd和oldfd指向相同的文件
如果成功,返回新复制的文件描述符; 如果失败,返回非0

  • 先使用close(1)关闭标准输出,再使用dup2使得fd=1重定向到目标文件。这部分代码为
    int copyFd;
    if (command.redirectF != 0)
    {
        //需要重定向
        command.argv[command.redirectF] = NULL; //按>分割
        if (command.argv[command.redirectF + 1] == NULL)
        {
            perror("参数错误\n");
            exit(-1);
        }
        close(1);
        int fd = open(command.argv[command.redirectF + 1], O_WRONLY | O_CREAT, 0777);
        copyFd = dup2(1, fd);
    }
    execvp(command.argv[0], command.argv);
    if (command.redirectF != 0)
    {
        close(1);
        dup2(copyFd, 1);
    }

sh3.c

  • sh3要求增加管道功能

  • 需要开启两个子进程,分别设置管道的读端和写端,其代码实现

    int fds[2];
    pid_t pid1, pid2;
    command.argv[command.pipLineF] = NULL; //按|分割
    pid1 = fork();
    if (pid1 == 0)
    {
        dup2(fds[1], STDOUT_FILENO);
        dup2(fds[1], STDERR_FILENO);
        close(fds[0]);
        close(fds[1]);
        execvp(command.argv[0], command.argv);
        _exit(127);
    }
    pid2 = fork();
    if (pid2 == 0)
    {
        dup2(fds[0], STDIN_FILENO);
        close(fds[0]);
        close(fds[1]);
        execvp(command.argv[command.redirectF + 1], &command.argv[command.redirectF + 1]);
        _exit(127);
    }
    wait(&pid1);
    wait(&pid2);

6. pi1.c

6.1. 实验内容

使用2个线程根据莱布尼兹级数计算PI

  • 莱布尼兹级数公式: 1 - 1/3 + 1/5 - 1/7 + 1/9 - … = PI/4
  • 主线程创建1个辅助线程
  • 主线程计算级数的前半部分
  • 辅助线程计算级数的后半部分
  • 主线程等待辅助线程运行結束后,将前半部分和后半部分相加

6.2. 效果演示

pi$ gcc -o pi1 pi1.c -lpthread
pi$ ./pi1
PI = 3.141543

6.3. 实验思路关键代码

  • 计算级数第n项的宏,此处为了减少重复代码
#define F(i, j) (i % 2 == 0 ? (1 / (2 * j + 1)) : (-1 / (2 * j + 1)))
  • 创建辅助线程
    pthread_t workerTid;
    pthread_create(&workerTid, NULL, &work, NULL);
  • 辅助线程中的计算
void *work(void *arg)
{
    int i;
    double j;
    workerOutput = 0;           //全局变量
    for (i = 0; i < NUM; i++)   //NUM是宏
    {
        j = i;
        workerOutput += F(i, j);
    }
    return NULL;
}
  • 主线程中的计算类似
  • 等待两边计算完成,开始计算PI

7. pi2.c

7.1. 实验内容

使用N个线程根据莱布尼兹级数计算PI

  • 主线程创建N个辅助线程
  • 每个辅助线程计算一部分任务,并将结果返回
  • 主线程等待N个辅助线程运行结束,将所有辅助线程的结果累加
  • 使用线程参数,消除程序中的代码重复
  • 不能使用全局变量存储线程返回值

7.2. 效果演示

pi$ gcc -o pi2 pi2.c -lpthread
pi$ ./pi2
PI = 3.141543

7.3. 实验思路和关键代码

  • 由于要求使用线程参数且不能使用全局变量存储线程返回值。定义了两个结构体,分别用于传递参数和返回结果
struct param
{
    int start;
    int end;
};

struct result
{
    double workerOutput;
};
  • 分别启动THREAD_NUM个线程计算
    for (i = 0; i < THREAD_NUM; i++)
    {
        struct param *param;
        param = &params[i];
        param->start = i * NUM;
        param->end = (i + 1) * NUM;
        pthread_create(&workerTids[i], NULL, worker, param);
    }
  • 等待进程结束并输出结果,释放空间
    for (i = 0; i < THREAD_NUM; i++)
    {
        struct result *result;
        pthread_join(workerTids[i], (void **)&result);
        sum += result->workerOutput;
        free(result);
    }

这里free的空间在子线程中分配


8. sort.c

8.1. 实验内容

多线程排序

  • 主线程创建两个辅助线程
  • 辅助线程1使用选择排序算法对数组的前半部分排序
  • 辅助线程2使用选择排序算法对数组的后半部分排序
  • 主线程等待辅助线程运行結束后,使用归并排序算法归并子线程的计算结果
  • 使用线程参数,消除程序中的代码重复

8.2. 效果演示

sort$ gcc -o sort sort.c -lpthread
sort$ ./sort
排序前:2 4 3 5 1 6 7 9 0 8
前半段:5 4 3 2 1
后半段:9 8 7 6 0
排序后:9 8 7 6 5 4 3 2 1 0

8.3. 实验思路和关键代码

  • 创建两个线程并传递参数
    pthread_t workerTid1;
    pthread_t workerTid2;

    struct param param1;
    param1.start = 0;
    param1.end = LEN / 2;
    struct param param2;
    param2.start = LEN / 2;
    param2.end = LEN;

    pthread_create(&workerTid1, NULL, &work, &param1);
    pthread_create(&workerTid2, NULL, &work, &param2);
  • 进行选择排序
void *work(void *arg)
{
    int maxNum;
    struct param *param;
    param = (struct param *)arg;
    for (int i = param->start; i < param->end; i++)
    {
        maxNum = i;
        for (int j = i; j < param->end; j++)
        {
            if (array[j] > array[maxNum])
            {
                maxNum = j;
            }
        }
        int c = array[maxNum];
        array[maxNum] = array[i];
        array[i] = c;
    }
    return NULL;
}
  • 等待两个线程排序完成,在主线程进行归并排序
    //归并排序
    int newArray[10];
    int p = 0;
    int p1 = 0;
    int p2 = LEN / 2;
    for (p; p < LEN; p++)
    {
        if (p1 >= LEN / 2)
        {
            newArray[p] = array[p2];
            p2++;
        }
        else if (p2 >= LEN)
        {
            newArray[p] = array[p1];
            p1++;
        }
        else
        {
            if(array[p1] > array[p2])
            {
                newArray[p] = array[p1];
                p1++;
            }
            else
            {
                newArray[p] = array[p2];
                p2++;
            }
        }
    }

9. pc1.c

9.1. 实验内容

使用条件变量解决生产者、计算者、消费者问题

  • 系统中有3个线程:生产者、计算者、消费者
  • 系统中有2个容量为4的缓冲区:buffer1、buffer2
  • 生产者生产’a’、‘b’、‘c’、‘d’、‘e’、‘f’、‘g’、'h’八个字符,放入到buffer1
  • 计算者从buffer1取出字符,将小写字符转换为大写字符,放入到buffer2
  • 消费者从buffer2取出字符,将其打印到屏幕上

9.2. 效果演示

pc$ gcc -o pc1 pc1.c -lpthread
pc$ ./pc1
生产者输出: a
生产者输出: b
生产者输出: c
    计算者得到: a
        计算者输出: A
    计算者得到: b
        计算者输出: B
    计算者得到: c
        计算者输出: C
            消费者得到: A
            消费者得到: B
            消费者得到: C
生产者输出: d
生产者输出: e
生产者输出: f
    计算者得到: d
        计算者输出: D
    计算者得到: e
        计算者输出: E
    计算者得到: f
        计算者输出: F
生产者输出: g
生产者输出: h
            消费者得到: D
            消费者得到: E
            消费者得到: F
    计算者得到: g
        计算者输出: G
    计算者得到: h
        计算者输出: H
            消费者得到: G
            消费者得到: H

9.3. 实验思路和关键代码

  • 定义缓冲区大小、数量、缓冲区结构,缓冲区为全局变量
#define CAPACITY 4      //缓冲区大小
#define BUFFER_NUM 2    //缓冲区数量

struct Buffer
{
    int buffer[CAPACITY];
    int in;
    int out;
};

struct Buffer buffer[BUFFER_NUM];
  • 对缓冲区的判断和操作代码与老师给出用例相似,这里不赘述
  • 互斥变量和条件变量, 数量上和缓冲区数量一致
//互斥和同步
pthread_mutex_t mutex[BUFFER_NUM];
pthread_cond_t waitEmptyBuffer[BUFFER_NUM]; //条件变量
pthread_cond_t waitFullBuffer[BUFFER_NUM];  //条件变量
  • 生产者、计算者、消费者的工作代码
    这里以计算者为例,同时涉及了两个缓冲区的操作,需要加两段锁
    for (i = 0; i < ITEM_COUNT; i++)
    {
        pthread_mutex_lock(&mutex[0]);
        //等待缓冲区填满
        while (BufferIsEmpty(0))
            pthread_cond_wait(&waitFullBuffer[0], &mutex[0]);

        //取值
        item = GetItem(0);
        printf("    计算者得到: %c\n", item);

        //唤醒等待缓冲区空的生产者
        pthread_cond_signal(&waitEmptyBuffer[0]);
        pthread_mutex_unlock(&mutex[0]);

        pthread_mutex_lock(&mutex[1]);
        //等待缓冲区为空
        while (BufferIsFull(1))
            pthread_cond_wait(&waitEmptyBuffer[1], &mutex[1]);

        //计算并将值放入缓冲区
        item -= 32;
        PutItem(1, item);
        printf("        计算者输出: %c\n", item);

        //唤醒等待缓冲区满的消费者
        pthread_cond_signal(&waitFullBuffer[1]);
        pthread_mutex_unlock(&mutex[1]);
    }
  • 主线程中需要对互斥变量和条件变量初始化
    pthread_mutex_init(&mutex[0], NULL);
    pthread_cond_init(&waitEmptyBuffer[0], NULL);
    pthread_cond_init(&waitFullBuffer[0], NULL);

    pthread_mutex_init(&mutex[1], NULL);
    pthread_cond_init(&waitEmptyBuffer[1], NULL);
    pthread_cond_init(&waitFullBuffer[1], NULL);

10. pc2.c

10.1. 实验内容

使用信号量解决生产者、计算者、消费者问题

  • 功能和实验9相同,使用信号量解决

10.2. 效果演示

pc$ gcc -o pc2 pc2.c -lpthread
pc$ ./pc2
生产者输出: a
生产者输出: b
生产者输出: c
        计算者得到: a
            计算者输出: A
生产者输出: d
                消费者得到: A
        计算者得到: b
            计算者输出: B
        计算者得到: c
                消费者得到: B
生产者输出: e
生产者输出: f
            计算者输出: C
        计算者得到: d
            计算者输出: D
        计算者得到: e
            计算者输出: E
        计算者得到: f
                消费者得到: C
                消费者得到: D
                消费者得到: E
            计算者输出: F
生产者输出: g
生产者输出: h
                消费者得到: F
        计算者得到: g
            计算者输出: G
        计算者得到: h
            计算者输出: H
                消费者得到: G
                消费者得到: H

10.3. 实验思路和关键代码

  • 本次实验与上次实验类似, 把其中的互斥量和条件变量封装成了信号量, 其结构体为
typedef struct
{
    int value;
    pthread_mutex_t mutex;
    pthread_cond_t cond;
} sema_t;
  • 将对信号量的操作封装成函数, 有初始化/等待/唤醒三种操作
void SemaInit(sema_t *sema, int value)
{
    sema->value = value;
    pthread_mutex_init(&sema->mutex, NULL);
    pthread_cond_init(&sema->cond, NULL);
}

void SemaWait(sema_t *sema)
{
    pthread_mutex_lock(&sema->mutex);
    while (sema->value <= 0)
        pthread_cond_wait(&sema->cond, &sema->mutex);
    sema->value--;
    pthread_mutex_unlock(&sema->mutex);
}

void SemaSignal(sema_t *sema)
{
    pthread_mutex_lock(&sema->mutex);
    ++sema->value;
    pthread_cond_signal(&sema->cond);
    pthread_mutex_unlock(&sema->mutex);
}
  • 每个缓冲区需要三个信号量
sema_t mutexSema[2];
sema_t emptyBufferSema[2];
sema_t fullBufferSema[2];
  • 不同线程工作函数与上题类似, 这里以计算者为例
    for (i = 0; i < ITEM_COUNT; i++)
    {
        SemaWait(&fullBufferSema[0]);
        SemaWait(&mutexSema[0]);

        item = GetItem(0);
        printf("        计算者得到: %c\n", item);

        SemaSignal(&mutexSema[0]);
        SemaSignal(&emptyBufferSema[0]);

        SemaWait(&emptyBufferSema[1]);
        SemaWait(&mutexSema[1]);

        item -= 32;
        PutItem(1,item);
        printf("            计算者输出: %c\n", item);

        SemaSignal(&mutexSema[1]);
        SemaSignal(&fullBufferSema[1]);
    }
  • 主线程需要对信号量初始化
    SemaInit(&mutexSema[0], 1);
    SemaInit(&emptyBufferSema[0], CAPACITY - 1);
    SemaInit(&fullBufferSema[0], 0);

    SemaInit(&mutexSema[1], 1);
    SemaInit(&emptyBufferSema[1], CAPACITY - 1);
    SemaInit(&fullBufferSema[1], 0);

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!