System-calls and system-defined structures
Most system-calls accept integers and
pointers to characters as parameters,
and typically return integer values indicating their success.
When more information must be passed to a system-call,
or the call needs to return multiple values,
we employ system-defined (C11) structures
defined in operating system provided header files.
Accessing structures using pointers
We've seen that we can access fields of a structure
using a single dot ('.' or fullstop).
What if, instead of accessing the structure directly,
we only have a pointer to a structure?
We've seen "one side" of this situation, already -
when we passed the address of a structure to a function:
struct timeval start_time;
gettimeofday( &start_time, NULL );
The function gettimeofday(), must have been declared to receive a pointer:
extern int gettimeofday( struct timeval *time, ......);
Consider the following example,
in which a pointer to a structure
is returned from a function.
We now use the → operator
(pronounced the 'arrow', or 'points-to' operator)
to access the fields via the pointer:
#include <stdio.h>
#include <time.h>
void greeting(void)
{
time_t NOW = time(NULL);
struct tm *tm = localtime(&NOW);
printf("Today's date is %i/%i/%i\n",
tm->tm_mday, tm->tm_mon + 1, tm->tm_year + 1900);
if(tm->tm_hour < 12) {
printf("Good morning\n");
}
else if(tm->tm_hour < 17) {
printf("Good afternoon\n");
}
else {
printf("Good evening\n");
}
}
|
CITS2002 Systems Programming, Lecture 16, p1, 19th September 2023.
Another example - accessing a system's password entries
On a stand-alone Linux system,
one not dependent on a network-based server to provide its user information,
some local user information is (historically) stored in the textfile /etc/passwd,
with each user's information stored one-per-line with fields separated by colons.
Rather than expecting every user program to parse this information correctly,
Linux systems host standard XOPEN header files and libraries
to conveniently provide the information.
We can iterate through this information with the help of the
getpwent() function,
which returns a pointer to a 'struct passwd' structure:
#include <stdio.h>
#define __USE_XOPEN_EXTENDED
#include <sys/types.h>
#include <pwd.h>
#if 0
The header file <pwd.h> declares:
struct passwd {
char *pw_name; /* username */
char *pw_passwd; /* user password */
uid_t pw_uid; /* user ID */
gid_t pw_gid; /* group ID */
char *pw_gecos; /* user information */
char *pw_dir; /* home directory */
char *pw_shell; /* shell program */
};
struct passwd *getpwent(void);
#endif
int main(int argc, char *argv[])
{
struct passwd *pwd;
while((pwd = getpwent()) != NULL) {
printf("%20s\t%-30s\t%s\n",
pwd->pw_name, pwd->pw_dir, pwd->pw_shell);
}
return 0;
}
|
CITS2002 Systems Programming, Lecture 16, p2, 19th September 2023.
Defining our own datatypes
We can further simplify our code,
and more clearly identify related data by defining our own datatypes.
Recall material from
Lecture-6
where structures, and arrays of structures were introduced.
We preceded each structure's name with the struct keyword,
and accessed structure elements using the 'dot' operator.
Instead,
we can use the typedef keyword to define our own
new datatype in terms of an old (existing) datatype,
and then not have to always provide the struct keyword:
// DEFINE THE LIMITS ON PROGRAM'S DATA-STRUCTURES
#define MAX_TEAMS 24
#define MAX_TEAMNAME_LEN 30
....
typedef struct {
char teamname[MAX_TEAMNAME_LEN+1]; // +1 for null-byte
....
int played;
....
} TEAM;
TEAM team[MAX_TEAMS];
|
As a convention (but not a C11 requirement),
we'll define our user-defined types using uppercase names:
// PRINT EACH TEAM'S RESULTS, ONE-PER-LINE, IN NO SPECIFIC ORDER
for(int t=0 ; t<nteams ; ++t) {
TEAM *tp = &team[t];
printf("%s %i %i %i %i %i %i %.2f %i\n", // %age to 2 decimal-places
tp->teamname,
tp->played, tp->won, tp->lost, tp->drawn,
tp->bfor, tp->bagainst,
(100.0 * tp->bfor / tp->bagainst), // calculate percentage
tp->points);
}
|
CITS2002 Systems Programming, Lecture 16, p3, 19th September 2023.
Defining our own datatypes, continued
Let's consider another example -
the starting (home) and ending (destination) bustops from the
CITS2002 1st project of 2015.
We starting with some of its definitions:
// GLOBAL CONSTANTS, BEST DEFINED ONCE NEAR THE TOP OF FILE
#define MAX_FIELD_LEN 100
#define MAX_STOPS_NEAR_ANYWHERE 200 // in Transperth: 184
....
// 2-D ARRAY OF VIABLE STOPS FOR COMMENCEMENT OF JOURNEY
char viable_home_stopid [MAX_STOPS_NEAR_ANYWHERE][MAX_FIELD_LEN];
char viable_home_name [MAX_STOPS_NEAR_ANYWHERE][MAX_FIELD_LEN];
int viable_home_metres [MAX_STOPS_NEAR_ANYWHERE];
int n_viable_homes = 0;
// 2-D ARRAY OF VIABLE STOPS FOR END OF JOURNEY
char viable_dest_stopid [MAX_STOPS_NEAR_ANYWHERE][MAX_FIELD_LEN];
char viable_dest_name [MAX_STOPS_NEAR_ANYWHERE][MAX_FIELD_LEN];
int viable_dest_metres [MAX_STOPS_NEAR_ANYWHERE];
int n_viable_dests = 0;
|
(After a post-project workshop)
we later modified the 2-dimensional arrays to use
dynamically-allocated memory and 'pointers-to-pointers':
// 2-D ARRAY OF VIABLE STOPS FOR COMMENCEMENT OF JOURNEY
char **viable_home_stopid = NULL;
char **viable_home_name = NULL;
int *viable_home_metres = NULL;
int n_viable_homes = 0;
// 2-D ARRAY OF VIABLE STOPS FOR END OF JOURNEY
char **viable_dest_stopid = NULL;
char **viable_dest_name = NULL;
int *viable_dest_metres = NULL;
int n_viable_dests = 0;
|
We can now gather the (many) related global variables into a single structure,
and use typedef to define our own datatype:
// A NEW DATATYPE TO STORE 1 VIABLE STOP
typedef struct {
char *stopid;
char *name;
int metres;
} VIABLE;
// A VECTOR FOR EACH OF THE VIABLE home AND dest STOPS
VIABLE *home_stops = NULL;
VIABLE *dest_stops = NULL;
int n_home_stops = 0;
int n_dest_stops = 0;
|
CITS2002 Systems Programming, Lecture 16, p4, 19th September 2023.
Finding the attributes of a file
As seen in Lecture-15,
many operating systems manage their data in a file system,
in particular maintaining files in a hierarchical directory structure -
directories contain files and other (sub)directories.
As we saw with time-based information,
we may request file- and directory information from the operating system
by calling system-calls.
We may employ another POSIX† function,
stat(),
and the system-provided structure struct stat,
to determine the attributes of each file:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
char *progname;
void file_attributes(char *filename)
{
struct stat stat_buffer;
if(stat(filename, &stat_buffer) != 0) { // can we 'stat' the file's attributes?
perror( progname );
exit(EXIT_FAILURE);
}
else if( S_ISREG( stat_buffer.st_mode ) ) {
printf( "%s is a regular file\n", filename );
printf( "is %i bytes long\n", (int)stat_buffer.st_size );
printf( "and was last modified on %i\n", (int)stat_buffer.st_mtime);
printf( "which was %s", ctime( &stat_buffer.st_mtime) );
}
}
|
†POSIX
is an acronym for "Portable Operating System Interface", a family of
standards specified by the IEEE for maintaining compatibility between
operating systems.
POSIX defines the application programming interface
(API), along with command line shells and utility interfaces, for software
compatibility with variants of Unix (such as macOS and Linux)
and other operating systems (e.g. Windows has a POSIX emulation layer).
CITS2002 Systems Programming, Lecture 16, p5, 19th September 2023.
Reading the contents of a directory
Most modern operating systems store their data in
hierarchical file systems,
consisting of directories which hold items that,
themselves,
may either be files or directories.
The formats used to store information
in directories in different file-systems are different(!),
and so when writing portable C programs,
we prefer to use functions that work portably.
Consider the strong similarities between opening and reading a (text) file,
and opening and reading a directory:
#include <stdio.h>
void print_file(char *filename)
{
FILE *fp;
char line[BUFSIZ];
fp = fopen(filename, "r");
if(fp == NULL) {
perror( progname );
exit(EXIT_FAILURE);
}
while(fgets(line, sizeof(buf), fp) != NULL) {
printf( "%s", line);
}
fclose(fp);
}
|
|
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
void list_directory(char *dirname)
{
DIR *dirp;
struct dirent *dp;
dirp = opendir(dirname);
if(dirp == NULL) {
perror( progname );
exit(EXIT_FAILURE);
}
while((dp = readdir(dirp)) != NULL) {
printf( "%s\n", dp->d_name );
}
closedir(dirp);
}
|
|
With directories,
we're again discussing functions that are not part of the C11 standard,
but are defined by POSIX standards.
The inconsistent naming of system-defined datatypes -
for example, struct dirent versus DIR -
can be confusing (annoying) but,
over time,
renaming datatypes across billions of lines of open-source code becomes impossible.
CITS2002 Systems Programming, Lecture 16, p6, 19th September 2023.
Investigating the contents of a directory
We now know how to open a directory for reading,
and to determine the names of all items in that directory.
What is each "thing" found in the directory -
is it a directory, is it a file...?
To answer those questions,
we need to employ the POSIX function,
stat(),
to determine the attributes
of the items we find in directories:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/param.h>
#include <dirent.h>
#include <unistd.h>
void list_directory(char *dirname)
{
char fullpath[MAXPATHLEN];
.....
while((dp = readdir(dirp)) != NULL) {
struct stat stat_buffer;
sprintf(fullpath, "%s/%s", dirname, dp->d_name );
if(stat(fullpath, &stat_buffer) != 0) {
perror( progname );
}
else if( S_ISDIR( stat_buffer.st_mode )) {
printf( "%s is a directory\n", fullpath );
}
else if( S_ISREG( stat_buffer.st_mode )) {
printf( "%s is a regular file\n", fullpath );
}
else {
printf( "%s is unknown!\n", fullpath );
}
}
closedir(dirp);
}
|
|
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/param.h>
#include <dirent.h>
#include <unistd.h>
void list_directory(char *dirname)
{
char fullpath[MAXPATHLEN];
.....
while((dp = readdir(dirp)) != NULL) {
struct stat stat_buffer;
struct stat *pointer = &stat_buffer;
sprintf(fullpath, "%s/%s", dirname, dp->d_name );
if(stat(fullpath, pointer) != 0) {
perror( progname );
}
else if( S_ISDIR( pointer->st_mode )) {
printf( "%s is a directory\n", fullpath );
}
else if( S_ISREG( pointer->st_mode )) {
printf( "%s is a regular file\n", fullpath );
}
else {
printf( "%s is unknown!\n", fullpath );
}
}
closedir(dirp);
}
|
|
CITS2002 Systems Programming, Lecture 16, p7, 19th September 2023.
File and Directory Permissions
Recall that:
- Like most modern systems,
Linux arranges its file system in a hierarchical structure of
files and directories.
Directories may contain entries for files and other directories.
- File entries contain the file's name,
together with a pointer to a structure termed the inode
(the information node?),
which represents the details of the file.
These are the familiar items shown by the standard ls -l command:
drwxr-xr-x 11 chris staff 1024 Jul 29 20:29 WWW
-rw------- 1 chris chris 53436 Aug 1 16:28 autonomous.pdf
-rw-r--r-- 1 chris chris 88 Dec 20 2016 scrolldown.gif
|
Multiple file entries, from possibly different directories, may point to
the same inode. Thus, it is possible to have a single file with multiple
names - we say that the names are links to the same file.
One unsigned 32-bit integer field in each inode contains the file's
permission (or protection) mode bits -
see /usr/include/bits/typesizes.h
From history,
the permission mode bits appear in the same integer defining
the file's type (regular, directory, block device, socket, ...) -
See man 2 stat for details.
CITS2002 Systems Programming, Lecture 16, p8, 19th September 2023.
File and Directory Permissions, continued
When access requests are made by a process on behalf of a
subject (a user) for an object (a file),
the Unix kernel compares the
effective user- and group-id attributes of the process
against the permission mode bits of the file.
Of note, if the owner's permission bits of a file or directory are not set,
then the owner cannot access the object by virtue of the 'group'
or 'other' bits (can you think why?).
The inode structure also contains indication of the object's
setuid and setgid status,
together with a sticky bit having an
overloaded meaning (historically, setting the sticky bit on an executable
file requested that it not be swapped out of memory - requiring privilege
to set the bit).
On different variants of Unix/Linux the permission mode bits,
in combination,
have some obscure meanings:
- having execute access, but not read access,
to a directory still permits an attacker to 'guess' filenames therein,
- having the sticky bit set on a directory permits only the owner of
a file, therein, to remove or modify the file,
- having the setgid bit set on a directory means that files created in
the directory receive the groupid of the directory, and not of their
creator (owner).
A system administrator managing different operating systems
(Unix/Linux, macOS, many flavours of Windows)
needs be aware of these subtle differences.
CITS2002 Systems Programming, Lecture 16, p9, 19th September 2023.
Describing permissions using octal values
While we know that all data within a computer is stored in binary,
we do not refer to or describe system-focused values with ones-and-zeroes.
In the case of file-system permissions,
3 bits (ranging over the integer values 0..7)
are used to store Boolean values for the
read,
write,
and
execute permissions.
Further,
3 sets of these 3 permissions bits,
one for each of the
user,
group,
and
'other' owners
are employed for each file-system entry.
So, while we don't describe these data values in binary,
it makes a lot of sense to describe them using octal values:
You may also like to try the
Unix Permissions Calculator.
CITS2002 Systems Programming, Lecture 16, p10, 19th September 2023.
Accessing file-system permission bits
The previous examples,
using RGB colour values,
only employed individual bytes of the integers being considered.
Here we need to access individual bits,
using each bit as if it were a Boolean value (which C11 doesn't directly support).
Recall that we have seen how we can determine if a directory entry
is another directory or a file:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
.....
struct stat stat_buffer;
if(stat(pathname, &stat_buffer) != 0) {
perror( progname );
}
else if( S_ISDIR( stat_buffer.st_mode )) {
printf( "%s is a directory\n", pathname );
}
else if( S_ISREG( stat_buffer.st_mode )) {
printf( "%s is a file\n", pathname );
}
else {
printf( "%s is unknown!\n", pathname );
}
|
|
// DEFINE A "NEW" TYPE TO REPRESENT A FILE'S MODE
typedef unsigned int mode_t;
struct stat {
....
mode_t st_mode;
....
}
#define S_IFMT 0170000 // BITS DETERMINING FILE TYPE
// File types
#define S_IFDIR 0040000 // DIRECTORY
....
#define S_IFREG 0100000 // REGULAR FILE
// MACROS FOR TESTING FOR DIFFERENT FILE TYPES
#define S_ISDIR(mode) (((mode) & S_IFMT) == (S_IFDIR))
....
#define S_ISREG(mode) (((mode) & S_IFMT) == (S_IFREG))
|
|
This C program better explains demonstrates these concepts:
showmode.c
CITS2002 Systems Programming, Lecture 16, p11, 19th September 2023.
|