To create an Ext2 filesystem on a floppy disk we will use the
fdformat
and
mkfs.ext2
commands.
fdformat
is used to format the disk, while
mkfs.ext2
creates the filesystem. The commands
work as following:
[ealtieri@italia os]$ fdformat /dev/fd0 Double-sided, 80 tracks, 18 sec/track. Total capacity 1440 kB. Formatting ... done Verifying ... done [ealtieri@italia os]$ /sbin/mkfs.ext2 /dev/fd0 mke2fs 1.26 (3-Feb-2002) Filesystem label= OS type: Linux Block size=1024 (log=0) Fragment size=1024 (log=0) 184 inodes, 1440 blocks 72 blocks (5.00%) reserved for the super user First data block=1 1 block group 8192 blocks per group, 8192 fragments per group 184 inodes per group Writing inode tables: done Writing superblocks and filesystem accounting information: done This filesystem will be automatically checked every 33 mounts or 180 days, whichever comes first. Use tune2fs -c or -i to override.
In the example above, /dev/fd0
is the
device in charge of raw access to 1.44 MB floppy disks. We
will use the same device in our examples later.
Once the two commands terminate, you may test the floppy:
[ealtieri@italia os]$ mount /mnt/floppy/ [ealtieri@italia os]$ ls -sil /mnt/floppy/ total 12 11 12 drwx------ 2 root root 12288 Jun 24 12:14 lost+found [ealtieri@italia os]$ umount /mnt/floppy/
Notice the -sil
options in the ls
command. The meaning of these options will be explained later.
On disk, the Ext2 filesystem is organized as shown in the picture below:
The first 1024 bytes of the disk, the "boot block", are reserved for the partition boot sectors and are unused by the Ext2 filesystem. The rest of the partition is split into block groups, each of which has the layout shown in the figure above. On a 1.44 MB floppy disk, there is only one block group.
The Ext2 data structures above are defined in include/linux/ext2_fs.h
. In
the following sections we will go through each of these data
structures.
The superblock is defined in struct
ext2_super_block
, line 339 of include/linux/ext2_fs.h
.
It contains information such as the total number of blocks on
disk, the size of a block (usually 1024 bytes), the number of free
blocks, etc. The meaning of each field in the
ext2_super_block
structure is explained in
ext2_fs.h
. Part of this structure has been
reported below:
struct ext2_super_block { __u32 s_inodes_count; /* Inodes count */ __u32 s_blocks_count; /* Blocks count */ ... __u32 s_free_blocks_count; /* Free blocks count */ __u32 s_free_inodes_count; /* Free inodes count */ __u32 s_first_data_block; /* First Data Block */ __u32 s_log_block_size; /* Block size */ ... __u32 s_blocks_per_group; /* # Blocks per group */ ... __u16 s_magic; /* Magic signature */ ...
The __u32
, __u16
and
__u8
data types denote unsigned 32-, 16- and
8-bit integer numbers.
s_inodes_count
and s_blocks_count
store the number of inodes and blocks on disk. If you look
back at the output of mkfs.ext2
, you will see
that the total number of blocks on a floppy disk is 1440 and
the number of inodes is 184. Also notice from the same output
that the size of a block is 1024 bytes, therefore 1440 blocks
* 1024 bytes = 1440 KB, the total size of a floppy disk.
The size of a block is given by
s_log_block_size
. This value expresses the size
of a block as a power of 2, using 1024 bytes as the
unit. Thus, 0 denotes 1024-byte blocks, 1 denotes 2048-byte
blocks, and so on. To calculate the size in bytes of a block:
unsigned int block_size = 1024 << super.s_log_block_size; /* block size in bytes */
The super block also tells us the number of blocks per group
with s_blocks_per_group
. Using
mkfs.ext2
, we can see that this value is 8192 on
a floppy disk. Because there are 1440 blocks on a floppy,
there can only be one group.
The superblock is located at offset 1024 of a floppy. The code
to read the superblock from a floppy is shown below. This code
also checks the magic number of the super block
(EXT2_SUPER_MAGIC
) to see if we are reading from
an Ext2 filesystem. For simplicity, error checking has been
omitted.
#include <linux/ext2_fs.h> ... int main() { int fd; struct ext2_super_block super; fd = open("/dev/fd0", O_RDONLY); /* open floppy device */ lseek(fd, 1024, SEEK_SET); /* position head above super-block */ read(fd, &super, sizeof(super)); /* read super-block */ if (super.s_magic != EXT2_SUPER_MAGIC) exit(1); /* bad file system */ block_size = 1024 << super.s_log_block_size; /* calculate block size in bytes */ ...
ext2super.c
reads the superblock from a floppy disk and prints it
to screen. |
In the blocks immediately following the super-block reside the
list of block-group descriptors. This list contains a
descriptor for each block group on the disk. In the case of a
floppy, there is only one block group and therefore one group
descriptor. For a bigger disk, we would have to calculate the
size of this list by using s_blocks_count
and
s_blocks_per_group
in the superblock:
/* calculate number of block groups on the disk */ unsigned int group_count = 1 + (super.s_blocks_count-1) / super.s_blocks_per_group; /* calculate size of the group descriptor list in bytes */ unsigned int descr_list_size = group_count * sizeof(struct ext2_group_descr);
A group descriptor is defined by the ext2_group_descr
structure, line 148
of ext2_fs.h
. This structure is reported below:
struct ext2_group_desc { __u32 bg_block_bitmap; /* Blocks bitmap block */ __u32 bg_inode_bitmap; /* Inodes bitmap block */ __u32 bg_inode_table; /* Inodes table block */ __u16 bg_free_blocks_count; /* Free blocks count */ __u16 bg_free_inodes_count; /* Free inodes count */ __u16 bg_used_dirs_count; /* Directories count */ __u16 bg_pad; __u32 bg_reserved[3]; };
A 1.44 MB floppy has one group descriptor only, which can be read using the following code:
struct ext2_group_descr group_descr; ... lseek(fd, 1024 + block_size, SEEK_SET); /* position head above the group descriptor block */ read(fd, &group_descr, sizeof(group_descr));
ext2group.c
reads the first (and only) group descriptor of a
floppy disk and prints it to screen. |
The group descriptor tells us the location of the block/inode
bitmaps and of the inode table (described later) through the
bg_block_bitmap
, bg_inode_bitmap
and
bg_inode_table
fields. These values indicate the
blocks where the bitmaps and the table are located. It is
handy to have a function to convert a block number to an
offset on disk, which can be easily done by knowing that all
blocks on disk have the same size of block_size
bytes (calculated earlier from the super-block):
#define BASE_OFFSET 1024 /* location of the super-block in the first group */ #define BLOCK_OFFSET(block) (BASE_OFFSET + (block-1)*block_size)
Blocks are numbered starting from 1. Block 1 is the superblock of the first group, block 2 contains the group descriptors, and so on. Block 0 is the NULL block and does not correspond to any disk offset.
A bitmap is a sequence of bits. Each bit represents a specific block (blocks bitmap) or inode (inode bitmap) in the block group. A bit value of 0 indicates that the block/inode is free, while a value of 1 indicates that the block/inode is being used. A bitmap always refers to the block-group it belongs to, and its size must fit in one block.
Limiting the size of a bitmap to one block also limits the
size of a block-group, because a bitmap always refers to the
blocks/inodes in the group it belongs to. Consider the blocks
bitmap: given a block size of 1024 bytes, and knowing that
each byte is made of 8 bits, we can calculate the maximum
number of blocks that the blocks bitmap can represent: 8 *
1024 = 8192 blocks. Therefore, 8192 blocks is the size of a
block-group using a 1024-byte block size, as we also see from
the output of mkfs.ext2
in the first section.
Q3. Calculate the number of blocks in a block-group given a block size of 4096 bytes. |
The following code fragment reads the block bitmap from disk:
struct ext2_super_block super; /* the super block */ struct ext2_group_desc group; /* the group descritopr */ unsigned char *bitmap; /* ... [read superblock and group descriptor] ... */ bitmap = malloc(block_size); /* allocate memory for the bitmap */ lseek(fd, BLOCK_OFFSET(group->bg_block_bitmap), SEEK_SET); read(fd, bitmap, block_size); /* read bitmap from disk */ ... free(bitmap);
The inode table consists of a series of consecutive blocks,
each of which contains a predefined number of inodes. The
block number of the first block of the inode table is stored
in the bg_inode_table
field of the group
descriptor. To figure out how many blocks are occupied by the
inode table, divide the total number of inodes in a group
(stored in the s_inodes_per_group
field of the
superblock) by the number of inodes per block:
/* number of inodes per block */ unsigned int inodes_per_block = block_size / sizeof(struct ext2_inode); /* size in blocks of the inode table */ unsigned int itable_blocks = super.s_inodes_per_group / inodes_per_block;
In the case of our floppy disk, we can see from the output of
mkfs.ext2
that we have 184 inodes per group and a
block size of 1024 bytes. The size of an inode is 128 bytes,
therefore the inode table will take 184 / (1024/128) = 23
blocks.
Q4. Assume a block size of 2048 bytes. What is the offset of the inode table if
bg_inode_table=5 in the group descritor? |
The inode table contains everything the operating system needs to know about a file, including the type of file, permissions, owner, and, most important, where its data blocks are located on disk. It is no surprise therefore that this table needs to be accessed very frequently and its read access time should be minimized as much as possible. Reading an inode from disk every time it is needed is usually a very bad idea. However, in this context we will adopt this method to keep the example code as simple as possible. We provide a general function to read an inode from the inode table:
static void read_inode(fd, inode_no, group, inode) int fd; /* the floppy disk file descriptor */ int inode_no; /* the inode number to read */ const struct ext2_group_desc *group; /* the block group to which the inode belongs */ struct ext2_inode *inode; /* where to put the inode */ { lseek(fd, BLOCK_OFFSET(group->bg_inode_table)+(inode_no-1)*sizeof(struct ext2_inode), SEEK_SET); read(fd, inode, sizeof(struct ext2_inode)); }
The offset of the inode to read is calculated by adding together the absolute offset of the inode table and the distance of the desired inode from the beginning of the inode table.
Q5. Calculate the offset of inode 93 on disk. Assume a
block size of 1024 bytes and
bg_inode_table=5 . To which block does the inode
belong to? (The size of an inode structure is 128 bytes) |
Inodes are numbered starting from 1. An inode is defined as a
struct ext2_inode
, in
ext2_fs.h
. The most important fields of this
structure have been reported below:
struct ext2_inode { __u16 i_mode; /* File type and access rights */ __u16 i_uid; /* Low 16 bits of Owner Uid */ __u32 i_size; /* Size in bytes */ __u32 i_atime; /* Access time */ __u32 i_ctime; /* Creation time */ __u32 i_mtime; /* Modification time */ __u32 i_dtime; /* Deletion Time */ __u16 i_gid; /* Low 16 bits of Group Id */ __u16 i_links_count; /* Links count */ __u32 i_blocks; /* Blocks count */ __u32 i_flags; /* File flags */ ... __u32 i_block[EXT2_N_BLOCKS]; /* Pointers to blocks */ ... };
Most of the fields in this structure are self-explanatory. Particular atttention should be paid to the fields in bold, explained in the following paragraphs.
i_mode
determines the type and access
rights of a file. Possible file types are listed below. For
each file type is defined a macro (sys/stat.h
)
that can be used to test for that specific file type.
Type Macro Regular file S_ISREG(m)
Directory S_ISDIR(m)
Character Device S_ISCHR(m)
Block Device S_ISBLK(m)
Fifo S_ISIFO(m)
Socket S_ISSOCK(m)
Symbolic Link S_ISLNK(m)
The file permissions are also stored in
i_mode
. These permissions can be tested by ANDing
i_mode
with a set of symbols defined in
sys/stat.h
:
Domain Read Write Exec All User S_IRUSR S_IWUSR S_IXUSR S_IRWXU Group S_IRGRP S_IWGRP S_IXGRP S_IRWXG All S_IROTH S_IWOTH S_IXOTH S_IRWXO
For example, to test if a file has user-execute permissions:
if (inode.i_mode & S_IXUSR) ...
The i_blocks
field of the inode structure
counts the number of blocks used by the file. Pointers to the
actual data blocks of the file are stored in the
i_block[EXT2_N_BLOCKS]
array. The
EXT2_N_BLOCKS
symbol is defined in
ext2_fs.h
(line 177) as following:
#define EXT2_NDIR_BLOCKS 12 /* number of direct blocks */ #define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS /* single indirect block */ #define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1) /* double indirect block */ #define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1) /* trible indirect block */ #define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1) /* total number of blocks */
In total there are 15 pointers in the
i_block[]
array. The meaning of each of these
pointers is explained below:
i_block[0..11]
point directly to the first
12 data blocks of the file.i_block[12]
points to a single indirect blocki_block[13]
points to a double indirect blocki_block[14]
points to a triple indirect blockThe following example prints the blocks used by the root directory of the floppy disk, which is always on inode 2 of the inode table:
struct ext2_inode inode; struct ext2_group_desc group; /* ... [ read superblock and group descriptor ] ... */ read_inode(fd, 2, &group, &inode); /* read root inode (#2) from the floppy disk */ for(i=0; i<EXT2_N_BLOCKS; i++) if (i < EXT2_NDIR_BLOCKS) /* direct blocks */ printf("Block %2u : %u\n", i, inode.i_block[i]); else if (i == EXT2_IND_BLOCK) /* single indirect block */ printf("Single : %u\n", inode.i_block[i]); else if (i == EXT2_DIND_BLOCK) /* double indirect block */ printf("Double : %u\n", inode.i_block[i]); else if (i == EXT2_TIND_BLOCK) /* triple indirect block */ printf("Triple : %u\n", inode.i_block[i]);
ext2root.c
reads the root inode (#2) from the inode table of a floppy disk. |
Directory entries in the inode table require special
attention. To test if an inode refers to a directory file we
can use the S_ISDIR(mode)
macro:
if (S_ISDIR(inode.i_mode)) ...
In the case of directory entries, the data blocks pointed by
i_block[]
contain a list of the files in the
directory and their respective inode numbers.
The list is composed of ext2_dir_entry_2
structures:
struct ext2_dir_entry_2 { __u32 inode; /* Inode number */ __u16 rec_len; /* Directory entry length */ __u8 name_len; /* Name length */ __u8 file_type; char name[EXT2_NAME_LEN]; /* File name */ };
The file_type
field indicates what kind of file
the entry is referring to. Possible values are:
file_type Description 0 Unknown 1 Regular File 2 Directory 3 Character Device 4 Block Device 5 Named pipe 6 Socket 7 Symbolic Link
Each entry has a variable size depending on the length of the
file name. The maximum length of a file name is
EXT2_NAME_LEN
, which is usually 255. The
name_len
field stores the length of the file name,
while rec_len
stores the total size of the entry
and is used to locate the next entry in the list.
Q7. Can you explain when and why NULL characters ("\0") are appended to the end of the file name? |
The following code reads the entries of a directory. Assume
that the inode of the directory is stored in
inode
:
struct ext2_dir_entry_2 *entry; unsigned int size; unsigned char block[BLOCK_SIZE]; ... lseek(fd, BLOCK_OFFSET(inode->i_block[0]), SEEK_SET); read(fd, block, block_size); /* read block from disk*/ size = 0; /* keep track of the bytes read */ entry = (struct ext2_dir_entry_2 *) block; /* first entry in the directory */ while(size < inode->i_size) { char file_name[EXT2_NAME_LEN+1]; memcpy(file_name, entry->name, entry->name_len); file_name[entry->name_len] = 0; /* append null char to the file name */ printf("%10u %s\n", entry->inode, file_name); entry = (void*) entry + entry->rec_len; /* move to the next entry */ size += entry->rec_len; }
The code above reads the first data block of a directory
from disk. The block is stored in the block
array. As explained earlier, this block contains a list of the
directory's entries. The problem is that entries in this list
have a variable size. This is the reason why we cannot just
read the data block into an array of struct
ext2_dir_entry_2
elements.
The entry
pointer points to the current entry in
the directory. The file name of the entry is copied from
entry->name
into file_name
so that a
null character can be appended to it. The inode and name of
the file is then displayed.
Finally, the position of the following entry in the list is
given by entry->rec_len
. This field indicates the
length in bytes of the current entry record. Therefore, the next
entry is located at address (void*) entry +
entry->rec_len
.
Notice that the code above only works if the size of the directory is less than a block. If the entries list takes more than one block, the program will crash.
ext2list.c
lists the contents of the root directory of the floppy
disk. |
Locating the data blocks belonging to a file implies locating its inode in the inode table first. The inode of the desired file is generally not known at the time the open operation is issued. What we know is the path of the file. For example:
int fd = open("/home/ealtieri/hello.txt", O_RDONLY);
The desired file is hello.txt
, while its path is
/home/ealtieri/hello.txt
.
To find out the inode belonging to the file we first need to
descend through its path, starting from the root directory,
until we reach the file's parent directory. At this point we
can locate the ext2_dir_entry_2
entry
corresponding to hello.txt
and then its inode
number.
Once the inode of the file is known, the data blocks belonging
to the hello.txt
are specified by the
inode.block[]
array.
The root directory is always located at inode 2.