CS330 Assignment 2 Solved

35.00 $

Category:

Description

Rate this product

As part of this assignment, you will be implementing system calls in a teaching OS (gemOS). We will be using a minimal OS called gemOS to implement these system calls and provide system call APIs to the user space. The gemOS source can be found in the src directory. This source provides the OS source code and the user space process (i.e., init process) code (in src/user directory).

1 Basic File Operations(40 marks)

We will be implementing some file related system calls in gemOS. gemOS pro- vides a very basic file system layer (See fs.h, fs.c) which supports simple file operations. Note that directories are not supported by this file system as of now.

List of system calls to implement

• int open(const char *pathname, int flags, int mode); • int read(int fd, void *buf, int count);
• int write(int fd, const void *buf, int count);
• int dup2(int oldfd, int newfd);

• long lseek(int fd, long offset, int whence);
• int close(int fd);
• int sendfile(int outfd, int infd, long *offset, int count);

1.1 open

To implement open system call, you are required to provide implementation for the template function do regular file open (in file.c) which takes the current context, filename, flags and mode as arguments. The argument flags

1

must include one of the following access modes O RDONLY, O WRONLY, or O RDWR. Open call can be used to open an existing file or create a new one by passing the O CREAT flag. If O CREAT flag is specified and the file already exists then the O CREAT flag has no effect, open the file after doing permission checks. If the O CREAT flag is specified then the mode argument must be passed. For regular files, an underlying inode is provided through the FS APIs which you are required to invoke. While creating a file, the first step is to get an inode from the underlying FS (File System) layer by invoking create inode (implemented in fs.c). The signature of create inode is as follows,

struct inode *create_inode(char *filename, u64 mode);

where filename and mode should be same as it is passed to the
do regular file open function. The mode can take O READ,O WRITE, O EXEC values which corresponds to Read, Write and Execute permissions (passed by the user). Permission check is performed on read/write access based on mode value, eg., write call on a file which is created with O READ mode should return an EACCES error.
Now let us look at the second scenario of opening an existing file. The first step here is look up the inode corresponding to the filename from the underlying FS layer by invoking lookup inode (in fs.c). The signature of this function is

    struct inode* lookup_inode(char *filename).

A valid inode is returned on success (NULL on error) and you need to ensure that the access flags mentioned in open are compatible with the mode in which file was created. After getting the inode from the FS layer, you need to find a free file descriptor, allocate a file object (using alloc file method in file.c) and fill-in the fields of corresponding struct file object which is pointed to by the entry in files (in context.h) field of current execution context. Here you need to look for a free position in files array starting from index 3. Index positions 0, 1, 2 cor- responds to stdin, stdout, stderr. You need to implement do read regular, do write regular, do lseek regular and do file close functions and assign them to read, write, lseek and close function pointers of struct fileops by accessing fops field in the struct file. As last step of open call, you need to return the file descriptor which is returned back to the user and used for sub- sequent file operations. The implementation of file objects and operations for stdin, stdout and stderr are already provided to help you with the under- standing of the task.

1.2 read

You need to implement the do read regular function (in file.c). This function is to be assigned as the read handler in the file object while opening the file. The inode provides a read method (flat read) with the following signature

2

int flat_read(struct inode *, char *buf, int count, int *offset);

where, buf and count are the user buffer and count, respectively, passed to do read regular from the read system call handler. The above function returns the number of bytes read from the underlying file. Read implementation for stdin, do read kbd, is provided in file.c as an illustration.

1.3 write

You need to implement the do write regular function (in file.c). This func- tion is to be assigned as the write handler in the file object while opening the file. The inode provides a write method (flat write) with the following signature

int flat_write(struct inode *, char *buf, int count, int *offset).

where, buf and count are the user buffer and count,respectively, passed to do write regular from the write system call handler. The above function returns the number of bytes written to the underlying file. Write implementation for stdout/stderr, do write console, is provided in file.c as an illustration.

1.4 dup2

You have to implement fd dup2 function (in file.c). It takes current execution context, oldfd and newfd as arguments. Before making newfd as a copy of oldfd, you need to close newfd if it is open. If the oldfd is not open, you have to return -EINVAL.

1.5 close

You have to implement do file close function (in file.c). You need to ensure that the reference count in the file object associated with the file is maintained correctly.When the last reference to the file object is dropped, you need to invoke the given free file object function.

1.5.1 Handler for process exit

As a program may exit without closing the files, you need to perform file close on exit system call by appropriately implementing do file exit in file.c. This function takes the execution context of the exiting process as argument.

void do_file_exit(struct exec context *ctx);

You will have update the ref count field of the file struct and call free file object function if no process is using this file.

3

1.6 lseek

You need to implement do lseek regular (in file.c). It takes pointer to struct file, offset and whence as arguments. You need to implement the functionality for three whence options SEEK SET, SEEK CUR, SEEK END (in file.h). You need to return error codes (in entry.h) based on the error conditions. Note that, if lseek results in taking the file offset beyond the file end, you need to return error code EINVAL.

1.7 sendfile

The sendfile system call transfers data between file descriptors.infd should be a file descriptor opened for reading and outfd should be a descriptor opened for writing.
If offset is not NULL, then it points to a variable holding the file offset from which sendfile() will start reading data from infd. When sendfile() returns, this variable will be set to the offset of the byte following the last byte that was read. If offset is not NULL, then sendfile() does not modify the file offset of infd, otherwise the file offset is adjusted to reflect the number of bytes read from infd. If offset is NULL, then data will be read from infd starting at the file offset, and the file offset will be updated by the call.

count is the number of bytes to copy between the file descriptors.
You need to provide the implementation of sendfile in the do sendfile func- tion in file.c. To allocate any memory buffers needed for the implementation, use the alloc memory buffer function provided, it allocates a 4KB buffer. To free the allocated buffers use free memory buffer function. The do sendfile function returns number of bytes written to outfd on success. If infd or outfd is not opened return -EINVAL. If infd is not opened for reading or outfd is

not opened for writing, return -EACCESS. ERROR CODES

You should only use following error codes on errors. All these error codes should be negated before returning (Example: EINVAL should be returned as -EINVAL).

  • EINVAL – (Invalid Argument) It should be used in-case of invalid ar- gument such as filename does not exist, invalid file descriptor, accessing closed file etc.
  • EACCES – (Invalid Access) It should be used in-case of invalid access such as writing to read-only file etc
  • ENOMEM – (No Memory) It should be used if any memory allocation functions fails.
  • EOTHERS – (Others) In case of any other errors which is not specified above use EOTHERS.

    4

NOTES

  • Don’t try to create or allocate memory by yourself. Try to use the specified functions.In case of any issues reach out to us.
  • Do not modify any files other than file.c for this part of the assignment. ASSUMPTIONS

    • There can be at-most 16 files, each having a maximum size of 4KB at any point of time.

    TESTING

    In the GemOS terminal (accessed using the telnet command ), you can type init to execute the user space process. The user space code is available in src/user/init.c. Three user space files are used to implement the user space logic. They are

  • init.c : Implements the first user space process which can invoke fork() to create more processes. Note that, there is no exec system call yet in the version provided to you. For changing the user space logic, you are required to modify only init.c.
  • ulib.h : Provides declarations of macros and functions. Note that you do not modify this file.
  • lib.c : Implements system call wrappers and provide different user space libraries (e.g., printf).Note that you do not modify this file.

    You need to write your test cases in init.c to validate your implementation. The sample test-cases (in src/user/test cases part1) can be copied into init.c to make use of them. If your implementation is correct, the output of executing test cases should match the expected output provided in src/user/test cases part1. The user and kernel code are compiled into a single binary file, i.e., gemOS.kernel when built using make from the src directory.

5

2 Message Queues

The message queue is a mechanism that facilitates inter-process communication. The members of the message queue are processes and they can be identified by their pids. A message queue can be considered as a special kind of file, so it is represented by an entry in the files table in the exec context. A file struct represents a message queue if its msg queue field has a NON NULL value. This field is a pointer to a struct of type msg queue info, this structure holds all the necessary information for the operation of the message queue.

The structure of a message queue

All the data structures needed to manage the message queue should be declared within the msg queue info struct (see msg queue.h). You are free to de- clare all the data structures that you need to manage the message queue in
the msg queue info struct. Ensure that your design will be able to man-
age all the functionality specified. You can allocate space to store a struct msg queue info using the alloc msg queue info function given in msg queue.c. You will need a buffer to hold all the messages in the message queue, to allocate this buffer use the alloc buffer function provided in msg queue.c, this buffer

has a size of 4KB and would be enough to store all the messages.

Messages

The members of the message queue can use it to exchange messages having the following structure:

    struct msg{
        u32 from_pid;

u32 to_pid;

        char msg_txt[MAX_MSG_SIZE];
    };

The msg txt is a null terminated string. The from pid field contains the process id of the process which sent the message. The to pid field contains the process id of the process to which the message is addressed, it can also contain a special value to represent broadcast messages (see BROADCAST PID in message queue.h).

Functionality
2.1 Creating a message queue (5 marks) A message queue is created using the

6

    int create_msg_queue();

system call, it returns a file descriptor for the created message queue. Note that when a process calls this system call a new message queue is created and a file descriptor which can be used to access the message queue in future is returned. You have to fill the following function in msg queue.c with the logic to create a message queue.

    int do_create_msg_queue(struct exec_context *ctx);

Inorder to create a message queue, you first have to find a free file descriptor from the files table in ctx structure. Then you have to allocate a file structure using the alloc file function provided in fs.c.You can initialize the fields of the fops structure in the file with NULL, as we won’t be using them for file operations in the case of a message queue. Now you can allocate a msg queue info structure, initialize it and make the msg queue field in allocated file structure point to it. To allocate the msg queue info structure use the alloc msg queue info function provided in msg queue.c. Return the file descriptor on success. If you were unable to allocate any structure during the creation, return -ENOMEM.

2.2 Adding members to the message queue (5 marks)

New members are added to the message queue whenever a process which is a member of the message queue forks. When a process forks, its child is added as a member of all the message queues the parent was a member of. You have to add the logic to implement this functionality in the following function in msg queue.c

    void do_add_child_to_msg_queue(struct exec_context *child_ctx);

This function takes the exec context struct of the newly created child process as argument. Note that the child has inherited the file descriptors from its par- ent. You will have to traverse through all the file descriptors in the child ctx, find descriptors representing message queues and add the child process to all of them by modifying the appropriate data structures that you declared to manage the message queue. You can assume that you will always be able to add the child to the message queues. Also note that the child does not get the messages in the message queue that were addressed to the parent.

2.3 Recieving messages (10 marks)

Message queues provide the following system call to read messages from them.

    int msg_queue_rcv(int fd, struct message *msg);

This system call takes a file descriptor corresponding to a message queue and a pointer to a struct message, and fills the struct message with the earliest message in the message queue addressed to this process (i.e., messages to a

7

process are delivered in a FIFO manner).
You have to implement the functionality for this system call in the following function in msg queue.c.

    int do_msg_queue_rcv(
        struct exec_context *ctx,
        struct file *filep,
        struct message *msg );

The ctx argument is the execution context of the calling process, filep argument points to the file struct corresponding to the message queue and msg points to the message struct to be filled.You have to find the earliest message addressed to the calling process in the message queue and fill the message structure with it and then return 1. If there are no messages to the calling process in the message queue return 0. If the filep argument does not point to a message queue, then return -EINVAL.

2.4 Sending messages (10 marks)

Message queue provides the following system call to send messages to other members of the message queue.

    int msg_queue_send(int fd, struct message *msg);

This system call takes a file descriptor corresponding to a message queue and a pointer to a message struct that contains the message.

You have to implement the logic for this system call in the following function in msg queue.c.

    int do_msg_queue_send(
        struct exec_context *ctx,
        struct file *filep,
        struct message *msg );

The ctx argument is the execution context of the calling process, filep argument points to the file struct corresponding to the message queue and msg points to the message struct which contains the message. Based on the value of the to pid field in the message struct the message can be a unicast message or a broadcast message. If the value of the to pid field in the message structure is set to BROADCAST PID (see msg queue.h) then it is a broadcast mes- sage and should be to all the members of the message queue, except the sender, ie all the members will recieve this message on calling msg queue rcv in the future. If the to pid field contains the pid of a process which is a member of the message queue then the message is delivered to that process. On success return the number of processes to which the message was delivered (e.g., 1 in the case of unicast). If the filep argument does not point to a message queue or the message is addressed to a process that is not a member of the message

8

queue, then return -EINVAL. You can assume that at any moment there will be no more than 32 messages addressed to a member process in the message queue. Refer to section 2.5 to know what happens on msg queue send when a member of the message queue blocks another member.

2.5 Getting information about message queue (5 marks)

Message queue provides the following system call to get the information about members of the message queue.

    int get_member_info(int fd, struct msg_queue_member_info *info);

This system call takes a file descriptor corresponding to a message queue and a pointer to struct msg queue member info which will be filled with information on completion of the system call.

    struct msg_queue_member_info{
        u32 member_count;
        u32 member_pid[MAX_MEMBERS];

};

The member count field holds the number of members in the message queue including the calling process.The member pid array holds the process ids of the members of the message queue. You must do the implementation of this system call in the following function in msg queue.c.

    int do_msg_queue_get_member_info(
        struct exec_context *ctx,
        struct file *filep,
        struct msg_queue_member_info *info );

You have to populate the member count field of the struct with the number of processes which are members of the message queue.The member pid array must contain the process ids of the members of the message queue. On success this system call returns 0. If the filep argument is pointing to a file struct which is not a message queue return -EINVAL.

2.6 Getting the number of pending messages (5 marks)

Message queue provides the following system call to get the number of messages in the queue, addressed to a member.

    int get_msg_count(int fd);

The system call takes a file descriptor corresponding to a message queue as the argument and returns the number of messages in the queue addressed to the calling process. You have to implement the functionality for this system call in the following function in msg queue.c.

9

    int do_get_msg_count(
        struct exec_context *ctx,
        struct file *filep );

Return the number of messages in the queue addressed to the calling process. Return -EINVAL if the fd does not represent a message queue.

2.7 Blocking messages from another process (5 marks)

Message queue provides the following system call to prevent one process from sending a message to another process.

    int msg_queue_block(int fd, int pid);

The system call takes a file descriptor fd corresponding to a message queue, and a process id pid. After this system call is made the process with pid can’t send any messages to the process that made this system call, i.e., msg queue send call by a blocked process will receive an -EINVAL error on trying to send a message to the process that blocked it and broadcast messages send by a blocked process will not be delivered to the blocking process. You have to implement the functionality for this system call in the following function in msg queue.c.

    int do_msg_queue_block(
        struct exec_context *ctx,
        struct file *filep,
        int pid);

On success return 0. If the filep argument does not represent a message queue or if the pid argument is not valid (i.e., not a member of the message queue), then return -EINVAL.

2.8 Closing the message queue (10 marks)

Message queue provides the following system call to allow a process to leave the message queue.

    int msg_queue_close(int fd);

The system call takes a file descriptor corresponding to a message queue as argument. After this system call the calling process is not a member of the message queue, and the file descriptor corresponding to the message queue is closed. You have to implement the functionality for this system call in the following function in msg queue.c.

    int do_msg_queue_close(struct exec_context *ctx, int fd);

You have to modify the data structures associated with the queue so that the calling process is not a member of the message queue and if the calling process was the last member of the message queue, then the structures associated with

10

the message queue has to be deallocated. Return 0 on success. If the fd argu- ment represents a file which is not a message queue return -EINVAL.

Handling process exit

When a process exits, it has to be removed from all the message queues it was a member of. You have to implement this functionality in the following function given in msg queue.c

    void do_msg_queue_cleanup(struct exec_context *ctx);

This function is called on process exit and has the execution context of the exiting process as argument. You have to remove the membership of the ex- iting process from all the message queues it was a member of. If the exiting process was the last member of some message queue, then deallocate the struc- tures associated with the message queue using the free msg queue info and free msg queue buffer functions provided.

ASSUMPTIONS

  • There will be at most 4 members in a message queue at a given point in time (see MAX MEMBERS in msg queue.h).
  • There will be at most four processes that will be running at any point in time. No need to fork more than 4 processes.
  • The pid of all the processes will be less than 8. TESTING

    The test procedure is similar to that mentioned in Part 1. Some sample testcases and their expected outputs are given in the user/test cases part2 folder. You can write your own test cases in init.c.

    Submission

    You have to submit three files file.c, msg queue.c, msg queue.h. Do not modify any other files other than these three files and init.c.

    11

  • Assignment2-pufce8.zip