정글사관학교 개발일지/운영체제-PintOS

PintOS Project 4 - File Systems (2) Extensible Files(91일차 TIL)

Woonys 2022. 2. 3. 01:03
반응형

간만에 잘 쉬다 왔다! 가서 텅 비우고 왔으니 다시 꽉꽉 채워보자!

 

 

0. 과제 관련 개념 정리

저번 글에서 첫번째 과제를 다 구현한 것처럼 적어놨지만, 사실은 반쪽만 구현해놓은 상태이다. 저번 과제에서는 기존에 연속 할당으로 디스크에 저장하는 방식을, FAT 테이블을 구현하고 디스크 내 클러스터 위치를 테이블에 저장하는 방식으로 변경하는 작업(Indexed Files)을 수행했다. 과제에서는 이어서 File growth를 구현하라고 했다. 이는 무엇인가?

 

확장 가능한 파일을 구현해라. 기본 파일 시스템에서, 파일 크기는 파일이 생성될 때 정해진다. 하지만 대부분 현대 파일 시스템에서는 파일은 처음에 사이즈 0으로 생성되고 매 시간 write가 완료될 때마다 크기가 확장된다. 우리의 파일 시스템은 반드시 이것을 따라야 한다. - Gitbook

위의 말처럼, 파일 크기는 유동적이기 때문에 처음에 크기를 정해놓으면 파일을 수정하는 과정에서 크기 한도를 넘어서는 경우가 자주 발생할 수 있다. 이를 해결하기 위해 파일 크기를 유동적으로 만들어주도록 시스템을 변경해야 하는 게 나머지 반쪽 부분.

 

이쯤에서 다시 FAT 구조와 UNIX 파일 시스템 구조를 비교해보자.

 

(좌)FAT 파일 시스템 (우) UNIX 파일 시스템

FAT 파일 시스템에서는 파일의 위치(데이터 블록 내 클러스터 번호)를 FAT 테이블에 저장해놓고 이를 메모리에 올려 빠르게 찾아가도록 하는 방식이다. 반면 UNIX에서는 위치를 포함한 파일의 다양한 메타데이터를 Inode 리스트에 저장해놓는다.

 

그렇다면 핀토스는 어떨까? 둘을 반쯤 합쳐놓은 방식인데, 바로 FAT 파일 시스템에서 root 디렉토리 대신 inode list가 들어있는 구조이다. 따라서 파일 위치를 제외한 나머지 메타데이터는 각 파일 별로 Inode 구조체를 만들고 여기에 넣은 뒤, 이를 갖고 있는 리스트에서 관리한다.

 

그러면 핀토스 시스템에서 어디를 수정하면 좋을까? 가장 직접적으로 연계된 부분을 찾아보자. 바로 inode_write_at()이다.

 

inode_write_at()

 

인자로 받은 inode와 버퍼에 대해, 버퍼에서 offset부터 시작해 SIZE 바이트만큼 읽어들여 inode에 저장해주는 함수이다. 주석 내용처럼, 현재는 file growth가 구현되어있지 않은 상황이라고 나와있다.

 

아래 코드를 큰 흐름에서 살펴보면, while 문을 돌면서 버퍼 내용을 size만큼 읽어서 디스크에 써준다(disk_write()). 이후에 써준 크기만큼 size 값을 줄이다가(size -= chunk_size) size값이 0보다 작거나 같아지면 while 문을 종료하고 write가 종료된 offset 위치를 반환한다.

 

/* Writes SIZE bytes from BUFFER into INODE, starting at OFFSET.
 * Returns the number of bytes actually written, which may be
 * less than SIZE if end of file is reached or an error occurs.
 * (Normally a write at end of file would extend the inode, but
 * growth is not yet implemented.) */
off_t
inode_write_at (struct inode *inode, const void *buffer_, off_t size,
		off_t offset) {
	const uint8_t *buffer = buffer_;
	off_t bytes_written = 0;
	uint8_t *bounce = NULL;

	if (inode->deny_write_cnt)
		return 0;

	while (size > 0) {
		/* Sector to write, starting byte offset within sector. */
		disk_sector_t sector_idx = byte_to_sector (inode, offset);
		int sector_ofs = offset % DISK_SECTOR_SIZE;

		/* Bytes left in inode, bytes left in sector, lesser of the two. */
		off_t inode_left = inode_length (inode) - offset;
		int sector_left = DISK_SECTOR_SIZE - sector_ofs;
		int min_left = inode_left < sector_left ? inode_left : sector_left;

		/* Number of bytes to actually write into this sector. */
		int chunk_size = size < min_left ? size : min_left;
		if (chunk_size <= 0)
			break;

		if (sector_ofs == 0 && chunk_size == DISK_SECTOR_SIZE) {
			/* Write full sector directly to disk. */
			disk_write (filesys_disk, sector_idx, buffer + bytes_written); 
		} else {
			/* We need a bounce buffer. */
			if (bounce == NULL) {
				bounce = malloc (DISK_SECTOR_SIZE);
				if (bounce == NULL)
					break;
			}

			/* If the sector contains data before or after the chunk
			   we're writing, then we need to read in the sector
			   first.  Otherwise we start with a sector of all zeros. */
			if (sector_ofs > 0 || chunk_size < sector_left) 
				disk_read (filesys_disk, sector_idx, bounce);
			else
				memset (bounce, 0, DISK_SECTOR_SIZE);
			memcpy (bounce + sector_ofs, buffer + bytes_written, chunk_size);
			disk_write (filesys_disk, sector_idx, bounce); 
		}

		/* Advance. */
		size -= chunk_size;
		offset += chunk_size;
		bytes_written += chunk_size;
	}
	free (bounce);

	return bytes_written;
}

 

이 Inode에 대해 좀 더 알아보자. (글은 여기를 참고)

 

Inode는 파일(디렉토리 포함)에 대한 메타데이터를 갖고 있는 고유 식별자이다. 사실 이 inode는 컴퓨터가 인식하는 파일이라고 보면 되는데, 우리는 파일을 이름으로 인식하는 반면, 컴퓨터는 inode로 이를 인식한다. 따라서 우리가 인식하는 파일을 갖고 작업을 하기 위해서는 이 파일을 inode로 바꿔 컴퓨터가 알아들을 수 있게 해줘야 한다. 따라서 파일은 모두 자신에게 부여된 inode를 가리키는 inode 포인터를 갖고 있다.

 

inode 구조체는 파일이나 디렉토리를 열 때 호출되는 inode_open() 함수에 의해서 생성되어 메모리로 올라오며, 그렇지 않을 때는 inode_disk 구조체 형태로 디스크에 저장된다. inode에 inode_disk 구조체가 멤버로 들어있으며, 실질적인 파일 위치에 대한 메타데이터는 Inode_disk가 갖고 있다. 아래 그림처럼 inode_open()을 통해 디스크에서 메모리로 올라오는 형식이다.

 

inode, inode_disk 구조체에 대한 디테일한 설명은 참조글에 있으니 글을 참고하도록 하고, 여기서는 바로 구현에 들어간다.

 

1. 과제 구현

 

inode_disk 구조체 수정

 

이 inode가 파일인지 디렉토리인지를 체크하기 위한 bool type 구조체를 하나 추가해준다. inode_disk는 항상 512바이트로 꽉차있어야 하기에 unused array로 나머지 빈 칸을 채운다. 이때, 나머지 네 개의 멤버 용량 총합이 13바이트이기에 512-13 = 499바이트이고 이를 uint8_t(1비트짜리) 499개 배열로 채운다.

/* Project 4-1: Extensible Files*/
/* On-disk inode.
 * Must be exactly DISK_SECTOR_SIZE bytes long. */
struct inode_disk {
	disk_sector_t start;                /* First data sector.  -> 4바이트 */
	off_t length;                       /* File size in bytes. -> 4바이트 */
	unsigned magic;                     /* Magic number. -> 4바이트 */
	bool isdir;							/* 파일인지 디렉토리인지 체크하는 멤버 -> 1바이트 */
	uint8_t unused[499];               /* Not used. */
};

 

byte_to_sector() 수정

 

byte_to_sector()는 인자로 받은 inode와 offset에 대해 해당 inode를 갖고 있는 섹터를 반환하는 함수다. 파일은 하나 이상의 섹터에 쪼개져서 저장될 것이다. 수정 전 함수를 보면, 파일이 디스크 내에서 연속적으로 섹터에 저장되는 형식이었다. 이제 FAT 테이블을 이용해 index 방식으로 여기저기 흩어진 섹터에 파일을 저장하는 방식으로 바뀌었으니 여기서도 수정해준다.

 

해당 inode가 들어있는 섹터를 sector_to_cluster()로 가져온다. for문을 돌면서 FAT 테이블에서 해당 섹터에 도달할 때까지 i값을 올린다. 굳이 for문을 도는 이유는, FAT 테이블에 들어있는 value는 다음 섹터 위치이기 때문에 반드시 해당 value를 방문해야 다음 섹터 위치를 알 수 있기 때문이다. i가 해당 위치(pos/DISK_SECTOR_SIZE)에 도달하면 그때의 클러스터 번호를 받아서 cluster_to_sector()로 섹터 값을 반환한다.

static disk_sector_t
byte_to_sector (const struct inode *inode, off_t pos) {
	ASSERT (inode != NULL);
	if (pos < inode->data.length){
		#ifdef EFILESYS
			cluster_t clst = sector_to_cluster(inode->data.start);
			for (unsigned i = 0; i < (pos / DISK_SECTOR_SIZE); i++) {
					clst = fat_get(clst);
					if (clst == 0)
						return -1;
			}
			return cluster_to_sector(clst);
		#else
			return inode->data.start + pos / DISK_SECTOR_SIZE;
		#endif
	}
	else
		return -1;
}


inode_create() 수정

 

inode를 생성할 때 이전 과제에서 구현했던 fat_create_chain()을 호출한다. 파일이 섹터에 다 들어갈 때까지 for문을 돌면서 FAT 테이블 내에 빈 클러스터를 불러와 파일 크기에 맞는 클러스터 체인을 만들어준다. 이때는 값을 써주는 게 아니라 여기저기 흩어져 있는 섹터를 하나씩 불러 파일 크기에 맞게 chain으로 연결해주는 작업만 해준다.

 

for문으로 여기저기 흩어져있는 섹터들을 하나의 체인으로 연결하고 나면, 이제 FAT 테이블에는 다음 클러스터 번호가 입력되어 있을 것이다. 이제 한 번 더 for문을 돌며 disk_write()를 호출해 클러스터에 해당 파일을 써준다. 이후는 원래 코드와 동일하다.

bool
inode_create (disk_sector_t sector, off_t length, bool isdir) {
	struct inode_disk *disk_inode = NULL;
	bool success = false;

	ASSERT (length >= 0);

	/* If this assertion fails, the inode structure is not exactly
	 * one sector in size, and you should fix that. */
	ASSERT (sizeof *disk_inode == DISK_SECTOR_SIZE);

	disk_inode = calloc (1, sizeof *disk_inode);
	if (disk_inode != NULL) {
		size_t sectors = bytes_to_sectors (length);
		disk_inode->length = length;
		disk_inode->magic = INODE_MAGIC;
		disk_inode->isdir = isdir;
		#ifdef EFILESYS
		
		cluster_t clst = sector_to_cluster(sector); 
		cluster_t newclst = clst; // save clst in case of chaining failure

		if(sectors == 0) disk_inode->start = cluster_to_sector(fat_create_chain(newclst));

		for (int i = 0; i < sectors; i++){
			newclst = fat_create_chain(newclst);
			if (newclst == 0){ // chain 생성 실패 시 (fails to allocate a new cluster)
				fat_remove_chain(clst, 0);
				free(disk_inode);
				return false;
			}
			if (i == 0){
				clst = newclst;
				disk_inode->start = cluster_to_sector(newclst); // set start point of the file
			}
		}
		disk_write (filesys_disk, sector, disk_inode);
		if (sectors > 0) {
			static char zeros[DISK_SECTOR_SIZE];
			for (i = 0; i < sectors; i++){
				ASSERT(clst != 0 || clst != EOChain);
				disk_write (filesys_disk, cluster_to_sector(clst), zeros); // non-contiguous sectors 
				clst = fat_get(clst); // find next cluster(=sector) in FAT
			}
		}
		success = true;
		#else
		if (free_map_allocate (sectors, &disk_inode->start)) {
			disk_write (filesys_disk, sector, disk_inode);
			if (sectors > 0) {
				static char zeros[DISK_SECTOR_SIZE];
				size_t i;

				for (i = 0; i < sectors; i++)
					disk_write (filesys_disk, disk_inode->start + i, zeros);
			}
			success = true; 
		} 
		#endif
		free (disk_inode);
	}
	return success;
}

 

inode_write_at() 수정

 

위에서와 마찬가지로 FAT 테이블에 입력해주기 위해 chain을 먼저 만든다. 이 과정에서 디스크 내에 해당 size만큼 쓸 수 있는 여분의 공간이 디스크 내에 있는지 먼저 확인하는 작업을 거친다. 즉, 파일의 시작 클러스터부터 offset + size까지의 공간이 모두 파일 클러스터 체인에 있어야 한다.

 

만약 없다면, 디스크에서 빈 클러스터를 할당받아 파일의 클러스터 체인에 추가해 파일을 담을 공간을 확장시킨다. 이때, 파일의 끝, 그러니까 EOF부터 write 시작 위치까지의 공간을 모두 0으로 초기화해준다. 새로 할당받은 디스크 클러스터에도 역시 0으로 초기화한다.

 

그러고 나서 쓰기 작업을 수행한다. SECTOR_SIZE 단위로 메모리의 버퍼에서부터 디스크 파일 클러스터에 데이터를 복사한다.

off_t
inode_write_at (struct inode *inode, const void *buffer_, off_t size,
		off_t offset) {
	const uint8_t *buffer = buffer_;
	off_t bytes_written = 0;
	uint8_t *bounce = NULL;
	
	bool grow = false; // 이 파일이 extend할 파일인지 아닌지를 나타내는 flag 
	uint8_t zero[DISK_SECTOR_SIZE]; // buffer for zero padding
	
    /* 해당 파일이 write 작업을 허용하지 않으면 0을 리턴*/
	if (inode->deny_write_cnt)
		return 0;

	/* inode의 데이터 영역에 충분한 공간이 있는지를 체크한다.
    write가 끝나는 지점인 offset+size까지 공간이 있는지를 체크한다. 
    공간이 없다면 -1을 리턴한다.
    */
	disk_sector_t sector_idx = byte_to_sector (inode, offset + size);

	// Project 4-1 : File growth
    /* 디스크에 충분한 공간이 없다면 파일을 늘린다(extend).
    확장할 때 EOF부터 write를 끝내는 지점까지의 모든 데이터를 0으로 초기화(memset)한다.*/
	#ifdef EFILESYS
	while (sector_idx == -1){
		grow = true; // flag 체크: 파일이 커진다는 것을 표시
		off_t inode_len = inode_length(inode); // 해당 inode 데이터 길이
		
		// endclst: 파일 데이터 영역의 가장 끝 섹터 번호를 불러온다.
		cluster_t endclst = sector_to_cluster(byte_to_sector(inode, inode_len - 1));
		// endclst 뒤에 새 클러스터를 만든다.
        cluster_t newclst = inode_len == 0 ? endclst : fat_create_chain(endclst);
		if (newclst == 0){
			break; //newclst가 0이면 여분 공간이 없다는 뜻.
		}

		// Zero padding
		memset (zero, 0, DISK_SECTOR_SIZE);

		off_t inode_ofs = inode_len % DISK_SECTOR_SIZE;
		if(inode_ofs != 0)
			inode->data.length += DISK_SECTOR_SIZE - inode_ofs; // round up to DISK_SECTOR_SIZE for convinience
		// #ifdef Q. What if inode_ofs == 0? Unnecessary sector added -> unnecessary가 아님. extend 중이니까! 

		disk_write (filesys_disk, cluster_to_sector(newclst), zero); // zero padding for new cluster
		if (inode_ofs != 0){
			disk_read (filesys_disk, cluster_to_sector(endclst), zero);
			memset (zero + inode_ofs + 1 , 0, DISK_SECTOR_SIZE - inode_ofs);
			disk_write (filesys_disk, cluster_to_sector(endclst), zero); // zero padding for current cluster
			/*
					endclst          newclst (extended)
				 ---------------     ------------
				| data  0 0 0 0 | - | 0 0 0 0 0 |
				 ---------------     -----------
						↑ zero padding here!
			*/
		}

		inode->data.length += DISK_SECTOR_SIZE; // update file length
		sector_idx = byte_to_sector (inode, offset + size);
	}		
	#endif

	sector_idx = byte_to_sector (inode, offset); // start writing from offset

	while (size > 0) {
		int sector_ofs = offset % DISK_SECTOR_SIZE;

		/* Bytes left in inode, bytes left in sector, lesser of the two. */
		off_t inode_left = inode_length (inode) - offset;
		int sector_left = DISK_SECTOR_SIZE - sector_ofs;
		int min_left = inode_left < sector_left ? inode_left : sector_left;

		/* Number of bytes to actually write into this sector. */
		int chunk_size = size < min_left ? size : min_left;
		if (chunk_size <= 0)
			break;

		if (sector_ofs == 0 && chunk_size == DISK_SECTOR_SIZE) {
			/* Write full sector directly to disk. */
			disk_write (filesys_disk, sector_idx, buffer + bytes_written); 
		} else {
			/* We need a bounce buffer. */
			if (bounce == NULL) {
				bounce = malloc (DISK_SECTOR_SIZE);
				if (bounce == NULL)
					break;
			}

			/* If the sector contains data before or after the chunk
			   we're writing, then we need to read in the sector
			   first.  Otherwise we start with a sector of all zeros. */
			if (sector_ofs > 0 || chunk_size < sector_left) 
				disk_read (filesys_disk, sector_idx, bounce);
			else
				memset (bounce, 0, DISK_SECTOR_SIZE);
			memcpy (bounce + sector_ofs, buffer + bytes_written, chunk_size);

			#ifdef EFILESYS
				// if (grow == true && size - chunk_size == 0) // last chunk
				// 	memset (bounce + sector_ofs + chunk_size + 1, 'EOF', 1);

				// #ifdef DBG
				/*
				inode_write_at에서, extend한 맨 마지막 chunk에 EOF 표시를 해줄 필요가 있나? 
				그리고 'EOF'는 character가 아니라 memset엔 못쓸텐데, 고쳐야 하는거 맞지?
				*/
			#endif
			
			disk_write (filesys_disk, sector_idx, bounce); 
		}

		/* Advance. */
		size -= chunk_size;
		offset += chunk_size;
		bytes_written += chunk_size;
	
		sector_idx = byte_to_sector (inode, offset);
	}
	#ifdef EFILESYS
		if (grow == true){
			inode->data.length = offset; // correct inode length
		}
		// #ifdef DBG Q. 이미 위 file growth 할때 inode->data.length 바꾸고 있잖아. 그리고 offset + size가 length?는 아니지 않나
	#endif
	free (bounce);
	// free (zero);

	disk_write (filesys_disk, inode->sector, &inode->data); 

	return bytes_written;
}

 

 

 

반응형