操作系统实验报告
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 = ¶ms[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, ¶m1);
pthread_create(&workerTid2, NULL, &work, ¶m2);
- 进行选择排序
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);
来源:oschina
链接:https://my.oschina.net/u/4406280/blog/4326378