实验四 文件IO编程

实验要求

  1. 撰写实验报告;
  2. 给出关键步骤的实现和效果截屏;
  3. 分析实验过程中出现的问题;
  4. 实验总结。

实验内容

  1. 编写程序实现文件写入锁和读取锁的设置和运行;
  2. 编写程序使用文件操作,仿真FIFO(先进先出)结构以及生产者-消费者运行模型;
  3. 编写程序实现文件多路复用操作。

实验步骤

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

首先创建两个管道文件in1in2.

在这里插入图片描述

依次输入命令

在这里插入图片描述
逐句测试
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

实验总结

本次实验帮助我深入理解了操作系统的核心概念和机制,例如文件系统,进程管理,线程管理等。通过编写各种实验程序,我学会了如何使用各种系统调用和库函数,例如fcntl,mkfifo和poll等,来管理和操作文件系统和进程/线程。此外,我还学习了如何使用并发编程技术来提高程序的性能和可靠性。总之,这是一次非常有用和有趣的实验,让我对操作系统和编程有了更深入的认识。

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐