实验四 文件IO编程
实验要求
- 撰写实验报告;
- 给出关键步骤的实现和效果截屏;
- 分析实验过程中出现的问题;
- 实验总结。
实验内容
- 编写程序实现文件写入锁和读取锁的设置和运行;
- 编写程序使用文件操作,仿真FIFO(先进先出)结构以及生产者-消费者运行模型;
- 编写程序实现文件多路复用操作。
实验步骤
1.文件写入锁和读取锁
1)编程lock_set.c实现文件记录锁功能
/* lock_set.c */
int lock_set(int fd, int type)
{
struct flock old_lock, lock;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
lock.l_type = type;
lock.l_pid = -1;
/* 判断文件是否可以上锁 */
fcntl(fd, F_GETLK, &lock);
if (lock.l_type != F_UNLCK)
{
/* 判断文件不能上锁的原因 */
if (lock.l_type == F_RDLCK) /* 该文件已有读取锁 */
{
printf("Read lock already set by %d\n", lock.l_pid);
}
else if (lock.l_type == F_WRLCK) /* 该文件已有写入锁 */
{
printf("Write lock already set by %d\n", lock.l_pid);
}
}
/* l_type 可能已被F_GETLK修改过 */
lock.l_type = type;
/* 根据不同的type值进行阻塞式上锁或解锁 */
if ((fcntl(fd, F_SETLKW, &lock)) < 0)
{
printf("Lock failed:type = %d\n", lock.l_type);
return 1;
}
switch(lock.l_type)
{
case F_RDLCK:
{
printf("Read lock set by %d\n", getpid());
}
break;
case F_WRLCK:
{
printf("Write lock set by %d\n", getpid());
}
break;
case F_UNLCK:
{
printf("Release lock by %d\n", getpid());
return 1;
}
break;
default:
break;
}/* end of switch */
return 0;
}
2)编写文件写入锁的测试用例write_lock.c:创建一个hello文件,之后对其上写入锁,键盘输入任意一个字符后解除写入锁。
/* write_lock.c */
#include <unistd.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include "lock_set.c"
int main(void)
{
int fd;
/* 首先打开文件 */
fd = open("hello",O_RDWR | O_CREAT, 0644);
if(fd < 0)
{
printf("Open file error\n");
exit(1);
}
/* 给文件上写入锁 */
lock_set(fd, F_WRLCK);
printf("File hello has been locked!!!\n");
char ch = getchar();
/* 给文件解锁 */
lock_set(fd, F_UNLCK);
printf("File hello has been unlocked!!!\n");
close(fd);
exit(0);
}
3)在两个终端上运行./write_lock,查看运行结果
如图所示,首先新建一个终端,运行write_lock文件,终端输出上锁成功的提示信息,此时再新建一个终端,运行write_lock文件,终端输出write lock already set by 5023
,说明写入锁被占用。
随后输入任意一个字符,解除写入锁,终端输出信息表明锁已被释放,右边write_lock程序随即申请写入锁成功!
输入任意字符,写入锁再次释放成功,结束运行。
4)编写文件读取锁的测试用例read_lock.c:创建一个hello文件,之后对其上读取锁,键盘输入任意一个字符后解除读取锁。
/* read_lock.c */
#include <unistd.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include "lock_set.c"
int main(void)
{
int fd;
fd = open("hello",O_RDWR | O_CREAT, 0644);
if(fd < 0)
{
printf("Open file error\n");
exit(1);
}
/* 给文件上读取锁 */
lock_set(fd, F_RDLCK);
printf("FILE hello has been locked!!!\n");
char ch = getchar();
/* 给文件解锁 */
lock_set(fd, F_UNLCK);
printf("FILE hello has been unlocked!!!\n");
close(fd);
exit(0);
}
5)在两个终端上运行./read_lock,查看运行结果。
新建两个终端,运行read_lock程序,可以发现,两个进程均申请读取锁成功。
这说明读取锁是共享的,可以允许多个进程申请使用。
输入任意字符,释放读取锁,程序运行完毕。
6)如果在一个终端上运行读取锁程序,在另一个终端上运行写入锁程序,会有什么结果?
新建两个终端,分别运行read_lock和write_lock,运行结果如下图:
可以看出,读取锁申请成功后,写锁申请失败,原因是读取锁占用。这说明读写锁是互斥的,两者不能同时申请并使用。
2.文件操作仿真FIFO,实现生产者-消费者运行模型
1)编程实现生产者程序producer.c,创建仿真FIFO结构文件(普通文件),按照给定的时间间隔向FIFO文件写入自动生成的字符(自定义),生产周期及生产的资源数通过参数传递给进程。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<string.h>
#include<time.h>
#include "lock_set.c"
const char* FIFO_FILE = "./luffyFIFO"; //仿真FIFO文件名
#define BUFFER_SIZE 10 //缓冲区大小
char buff[BUFFER_SIZE]; //缓冲区
//生产一个字符并写入到仿真FIFO文件中
char product()
{
int fd;
time_t t;
//打开仿真FIFO文件
if((fd = open(FIFO_FILE, O_CREAT|O_RDWR|O_APPEND, 0666)) < 0)
{
perror("open");
exit(1);
}
//使用随机数生成随机一个英文字符
char ch = (char)(rand() % 26 + 'A');
sprintf(buff, "%c", ch);
/* 给文件上写入锁 */
lock_set(fd, F_WRLCK);
if(write(fd, buff, strlen(buff)) < 0)
{
//写入错误
perror("Producer");
exit(1);
}
/* 给文件解锁 */
lock_set(fd, F_UNLCK);
close(fd);
return ch;
}
int main(int argc, char* argv[])
{
int count, interval;
char c;
// ./producer [生产资源数量] [生产间隔]
if (argc != 3)
{
printf("Usage: %s [count] [interval] \n", argv[0]);
exit(1);
}
count = atoi(argv[1]);
interval = atoi(argv[2]);
srand((unsigned int)time(NULL));
// 检查FIFO文件是否存在
if (access(FIFO_FILE, F_OK) != -1) {
// 创建FIFO文件
if(mkfifo(FIFO_FILE, 0666) < 0)
{
perror("mkfifo");
exit(1);
}
}
for(int i=0; i<count; i++)
{
c = product();
printf("第 %d 轮生产:%c\n",(i+1), c);
sleep(interval);
}
unlink(FIFO_FILE);
return 0;
}
2)编程实现消费者程序customer.c,从文件中读取相应数目的字符并在屏幕上显示,然后从文件中删除刚才消费过的数据,可通过两次幅值来实现文件内容的偏移,每次消费的资源通过参数传递给进程。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<string.h>
#include<time.h>
#include "lock_set.c"
const char* FIFO_FILE = "./luffyFIFO"; //仿真FIFO文件名
const char* TMP_FILE = "./luffytmp"; //临时tmp文件名
/*资源消费函数*/
int consume(int need)
{
int fd;
char buff;
int counter = 0;
if((fd = open(FIFO_FILE, O_RDONLY)) < 0)
{
printf("Consuming Error!\n");
return -1;
}
printf("Consuming....................\n");
lseek(fd, SEEK_SET, 0);
while(1)
{
while(read(fd, &buff, 1) == 1)
{
/*消费*/
printf("CONSUME: %c\n", buff);
counter ++;
if(counter >= need)
{
break;
}
}
if(counter >= need)
{
break;
}
}
printf("FULL!\n");
close(fd);
return 0;
}
/*文件拷贝,src_file -> dst_file 偏移量:offset copy字节大小:cnt*/
int FileCopy(const char * src_file, const char * dst_file, int offset, int cnt)
{
int input_file, output_file;
int counter = 0;
char buff;
if((input_file = open(src_file, O_RDONLY|O_NONBLOCK)) < 0)
{
printf("%s FILE OPEN ERROR\n", src_file);
return -1;
}
if((output_file = open(dst_file, O_CREAT|O_RDWR|O_TRUNC|O_NONBLOCK, 0644)) < 0)
{
printf("%s FILE CREATE OR OPEN ERROR\n", dst_file);
return -1;
}
lseek(input_file, offset, SEEK_SET);
while((read(input_file, &buff, 1) == 1) && (counter < cnt))
{
write(output_file, &buff, 1);
counter++;
}
close(input_file);
close(output_file);
return 0;
}
int main(int argc, char* argv[])
{
int need;
int fd;
char c;
// ./producer [需求量]
if (argc != 2)
{
printf("Usage: %s [need]\n", argv[0]);
exit(1);
}
need = atoi(argv[1]);
while(1)
{
/*消费*/
consume(need);
if((fd = open(FIFO_FILE, O_RDWR)) < 0)
{
printf("COPY ERROR IN FIFO_FILE\n");
exit(1);
}
/* 给文件上写入锁 */
lock_set(fd, F_WRLCK);
/*剩下的数据copy到临时文件*/
FileCopy(FIFO_FILE, TMP_FILE, need, 1024);
/*临时文件的数据覆盖FIFOfile*/
FileCopy(TMP_FILE, FIFO_FILE, 0, 1024);
/* 给文件解锁 */
lock_set(fd, F_UNLCK);
unlink(TMP_FILE);
printf("Input : [need]\n");
scanf("%d", &need);
}
return 0;
}
3)在两个终端上分别运行生产者程序producer和消费者程序customer
mkfifo
函数用于创建命名管道(FIFO)文件,是POSIX标准提供的一种IPC机制,通过它可以实现两个或多个进程之间的通信,实现数据的读写.
int mkfifo(const char *pathname, mode_t mode);
其中,
pathname
是命名管道的路径名,mode
是文件的权限位,通常使用0666
. mkfifo函数会创建一个指定路径名的文件,文件类型为FIFO,其特点是以先进先出的方式读写数据,即读进程从文件开头处读取数据,写进程向文件末尾写入数据。在成功创建FIFO文件之后,进程就可以使用open函数打开文件,读写其中的数据.
分别运行生产者和消费者,其中生产者设置为生产20个物品,生产间隔设置为5秒,消费者初始消费需求为5个物品,运行结果如下图:
由上图可以看出,生产者生产了两个字符V A
,消费者随即消费资源V A
,等待生产者生产资源。
等待一段时间后,生产者经历了六轮生产,其中V A K G X
被消费者消费完毕,消费者申请写锁,将剩下的数据拷贝到临时文件然后再覆盖原数据文件。
消费者输入新的消费需求10,可以看出紧接着消费的第一个物品对应第6轮生产字符D
消费者消费完成,生产者进入第18轮生产。
消费者输入新的消费需求3,紧接着第15轮的产物,消费物品依次为A N T
。
3.多路复用——I/O操作及阻塞
编程实现文件描述集合的监听.
/* multiplex_poll.c */
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <poll.h>
#define MAX_BUFFER_SIZE 1024 /* 缓冲区大小*/
#define IN_FILES 3 /* 多路复用输入文件数目*/
#define TIME_DELAY 60 /* 超时时间秒数 */
#define MAX(a, b) ((a > b)?(a):(b))
int main(void)
{
struct pollfd fds[IN_FILES];
char buf[MAX_BUFFER_SIZE];
int i, res, real_read, maxfd;
/*首先按一定的权限打开两个源文件*/
fds[0].fd = 0;
if((fds[1].fd = open ("in1", O_RDONLY|O_NONBLOCK)) < 0)
{
printf("Open in1 error\n");
return 1;
}
if((fds[2].fd = open ("in2", O_RDONLY|O_NONBLOCK)) < 0)
{
printf("Open in2 error\n");
return 1;
}
for (i = 0; i < IN_FILES; i++)
{
fds[i].events = POLLIN;
}
/*循环测试该文件描述符是否准备就绪,并调用poll函数对相关文件描述符做对应操作*/
while(fds[0].events || fds[1].events || fds[2].events)
{
if (poll(fds, IN_FILES, 0) < 0)
{
printf("Poll error\n");
return 1;
}
for (i = 0; i< IN_FILES; i++)
{
if (fds[i].revents)
{
memset(buf, 0, MAX_BUFFER_SIZE);
real_read = read(fds[i].fd, buf, MAX_BUFFER_SIZE);
if (real_read < 0)
{
if (errno != EAGAIN)
{
return 1;
}
}
else if (!real_read)
{
close(fds[i].fd);
fds[i].events = 0;
}
else
{
if (i == 0)
{
if ((buf[0] == 'q') || (buf[0] == 'Q'))
{
return 1;
}
}
else
{
buf[real_read] = '\0';
printf("%s", buf);
}
} /* end of if real_read*/
} /* end of if revents */
} /* end of for */
} /*end of while */
exit(0);
}
运行时,需要打开3个虚拟终端,分别创建两个管道文件in1和in2,运行主程序
mknod in1 p
:该命令是用于在Linux系统中创建一个命名管道(Named Pipe)的命令。该命令使用的选项是mknod,后面的参数in1表示要创建的命名管道的名称,p则表示该命名管道是一个管道类型(pipe)。 命名管道是一种特殊的文件类型,它允许进程之间进行双向通信。进程可以像读写文件一样读写命名管道,但是它们不会像普通文件一样存储数据。命名管道中的数据是在读取和写入过程中直接传递的,因此它可以用于实现进程间通信(IPC)。
当使用mknod命令创建命名管道时,如果该命名管道不存在,则会在指定的路径下创建一个新的命名管道文件。如果已经存在同名的命名管道,则mknod命令将返回一个错误。创建命名管道后,可以使用标准的文件读写操作进行读写。
# 终端1
mknod in1 p
cat > in1
MULTIPLEX CALL
TEST IN1
END
# 终端2
mknod in2 p
cat > in2
MULTIPLEX CALL
TEST IN2
END
# 终端3
./multiplex_poll
首先创建两个管道文件in1
和in2
.
依次输入命令
逐句测试
实验总结
本次实验帮助我深入理解了操作系统的核心概念和机制,例如文件系统,进程管理,线程管理等。通过编写各种实验程序,我学会了如何使用各种系统调用和库函数,例如fcntl,mkfifo和poll等,来管理和操作文件系统和进程/线程。此外,我还学习了如何使用并发编程技术来提高程序的性能和可靠性。总之,这是一次非常有用和有趣的实验,让我对操作系统和编程有了更深入的认识。