mkfs.mfs -I: insert fs at offset feature

. use it in x86_hdimage.sh:
	  avoid copying big FS images into iso & hd images each time

Change-Id: I0775f43cd1821ea7be2fec5b96d107a68afb96d6
This commit is contained in:
Ben Gras 2013-11-07 08:33:39 +01:00
parent 09143af258
commit 8dbe32610b
3 changed files with 121 additions and 104 deletions

View file

@ -28,12 +28,6 @@ fi
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
#
# Artifacts from this script are stored in the IMG_DIR
#
rm -rf $IMG_DIR $IMG
mkdir -p $IMG_DIR $CDFILES
while getopts "i" c while getopts "i" c
do do
case "$c" in case "$c" in
@ -45,6 +39,12 @@ done
: ${IMG=minix_x86.img} : ${IMG=minix_x86.img}
#
# Artifacts from this script are stored in the IMG_DIR
#
rm -rf $IMG_DIR $IMG
mkdir -p $IMG_DIR $CDFILES
# #
# Call build.sh using a sloppy file list so we don't need to remove the installed /etc/fstag # Call build.sh using a sloppy file list so we don't need to remove the installed /etc/fstag
# #
@ -56,24 +56,13 @@ then cp ${DESTDIR}/usr/mdec/boot_monitor $CDFILES/boot
cp ${MODDIR}/* $CDFILES/ cp ${MODDIR}/* $CDFILES/
. ./releasetools/release.functions . ./releasetools/release.functions
cd_root_changes # uses $CDFILES and writes $CDFILES/boot.cfg cd_root_changes # uses $CDFILES and writes $CDFILES/boot.cfg
${CROSS_TOOLS}/nbwriteisofs -s0x0 -l MINIX -B ${DESTDIR}/usr/mdec/bootxx_cd9660 -n $CDFILES ${IMG_DIR}/iso.img # start the image off with the iso image; reduce root size to reserve
ISO_SIZE=$((`stat -c %s ${IMG_DIR}/iso.img` / 512)) ${CROSS_TOOLS}/nbwriteisofs -s0x0 -l MINIX -B ${DESTDIR}/usr/mdec/bootxx_cd9660 -n $CDFILES $IMG
ISO_SIZE=$((`stat -c %s $IMG` / 512))
else # just make an empty iso partition else # just make an empty iso partition
ISO_SIZE=8 ISO_SIZE=8
fi fi
# This script creates a bootable image and should at some point in the future
# be replaced by makefs.
#
# All sized are written in 512 byte blocks
#
# we create a disk image of about 2 gig's
# for alignment reasons, prefer sizes which are multiples of 4096 bytes
#
: ${ROOT_SIZE=$(( 64*(2**20) / 512))}
: ${HOME_SIZE=$(( 128*(2**20) / 512))}
: ${USR_SIZE=$((1536*(2**20) / 512))}
# #
# create a fstab entry in /etc this is normally done during the # create a fstab entry in /etc this is normally done during the
# setup phase on x86 # setup phase on x86
@ -87,15 +76,6 @@ rm -f ${DESTDIR}/SETS.*
${CROSS_TOOLS}/nbpwd_mkdb -V 0 -p -d ${DESTDIR} ${DESTDIR}/etc/master.passwd ${CROSS_TOOLS}/nbpwd_mkdb -V 0 -p -d ${DESTDIR} ${DESTDIR}/etc/master.passwd
#
# Now given the sizes above use DD to create separate files representing
# the partitions we are going to use.
#
dd if=/dev/zero of=${IMG_DIR}/iso.img bs=512 count=1 seek=$(($ISO_SIZE -1)) 2>/dev/null
dd if=/dev/zero of=${IMG_DIR}/root.img bs=512 count=1 seek=$(($ROOT_SIZE -1)) 2>/dev/null
dd if=/dev/zero of=${IMG_DIR}/home.img bs=512 count=1 seek=$(($HOME_SIZE -1)) 2>/dev/null
dd if=/dev/zero of=${IMG_DIR}/usr.img bs=512 count=1 seek=$(($USR_SIZE -1)) 2>/dev/null
# make the different file system. this part is *also* hacky. We first convert # make the different file system. this part is *also* hacky. We first convert
# the METALOG.sanitised using mtree into a input METALOG containing uids and # the METALOG.sanitised using mtree into a input METALOG containing uids and
# gids. # gids.
@ -133,54 +113,48 @@ rm ${IMG_DIR}/root.in
cat ${IMG_DIR}/input | grep "^\./usr/\|^. " | sed "s,\./usr,\.,g" | ${CROSS_TOOLS}/nbtoproto -b ${DESTDIR}/usr -o ${IMG_DIR}/usr.proto cat ${IMG_DIR}/input | grep "^\./usr/\|^. " | sed "s,\./usr,\.,g" | ${CROSS_TOOLS}/nbtoproto -b ${DESTDIR}/usr -o ${IMG_DIR}/usr.proto
cat ${IMG_DIR}/input | grep "^\./home/\|^. " | sed "s,\./home,\.,g" | ${CROSS_TOOLS}/nbtoproto -b ${DESTDIR}/home -o ${IMG_DIR}/home.proto cat ${IMG_DIR}/input | grep "^\./home/\|^. " | sed "s,\./home,\.,g" | ${CROSS_TOOLS}/nbtoproto -b ${DESTDIR}/home -o ${IMG_DIR}/home.proto
# If in ISO mode, fit the FSes # This script creates a bootable image and should at some point in the future
# be replaced by makefs.
#
# All sized are written in 512 byte blocks
#
# we create a disk image of about 2 gig's
# for alignment reasons, prefer sizes which are multiples of 4096 bytes
#
: ${ROOT_SIZE=$(( 64*(2**20) / 512))}
: ${HOME_SIZE=$(( 128*(2**20) / 512))}
: ${USR_SIZE=$(( 1536*(2**20) / 512))}
if [ "$ISOMODE" ] if [ "$ISOMODE" ]
then ROOTSIZEARG="-x 5" # give root fs a little breathing room on the CD then
else # give args with the right sizes # In iso mode, make all FSes fit (i.e. as small as possible), but
# leave some space on /
ROOTSIZEARG="-x 5"
else
# In hd image mode, FSes have fixed sizes
ROOTSIZEARG="-b $((${ROOT_SIZE} / 8))" ROOTSIZEARG="-b $((${ROOT_SIZE} / 8))"
USRSIZEARG="-b $((${USR_SIZE} / 8))" USRSIZEARG="-b $((${USR_SIZE} / 8))"
HOMESIZEARG="-b $((${HOME_SIZE} / 8))" HOMESIZEARG="-b $((${HOME_SIZE} / 8))"
fi fi
#
# Generate /root, /usr and /home partition images.
#
echo "Writing Minix filesystem images" echo "Writing Minix filesystem images"
echo " - ROOT"
${CROSS_TOOLS}/nbmkfs.mfs $ROOTSIZEARG ${IMG_DIR}/root.img ${IMG_DIR}/root.proto
echo " - USR"
${CROSS_TOOLS}/nbmkfs.mfs $USRSIZEARG ${IMG_DIR}/usr.img ${IMG_DIR}/usr.proto
echo " - HOME"
${CROSS_TOOLS}/nbmkfs.mfs $HOMESIZEARG ${IMG_DIR}/home.img ${IMG_DIR}/home.proto
# Set the sizes based on what was just generated - should change nothing if sizes
# were specified
echo "$ROOT_SIZE $USR_SIZE $HOME_SIZE"
ROOT_SIZE=$((`stat -c %s ${IMG_DIR}/root.img` / 512))
USR_SIZE=$((`stat -c %s ${IMG_DIR}/usr.img` / 512))
HOME_SIZE=$((`stat -c %s ${IMG_DIR}/home.img` / 512))
echo "$ROOT_SIZE $USR_SIZE $HOME_SIZE"
# Do some math to determine the start addresses of the partitions. # Do some math to determine the start addresses of the partitions.
# Ensure the start of the partitions are always aligned, the end will # Ensure the start of the partitions are always aligned, the end will
# always be as we assume the sizes are multiples of 4096 bytes, which # always be as we assume the sizes are multiples of 4096 bytes, which
# is always true as soon as you have an integer multiple of 1MB. # is always true as soon as you have an integer multiple of 1MB.
# ROOT_START=$ISO_SIZE
ISO_START=0
ROOT_START=$(($ISO_START + $ISO_SIZE)) echo " - ROOT"
ROOT_SIZE=$((`${CROSS_TOOLS}/nbmkfs.mfs -d $ROOTSIZEARG -I $(($ROOT_START*512)) $IMG ${IMG_DIR}/root.proto`/512))
USR_START=$(($ROOT_START + $ROOT_SIZE)) USR_START=$(($ROOT_START + $ROOT_SIZE))
echo " - USR"
USR_SIZE=$((`${CROSS_TOOLS}/nbmkfs.mfs -d $USRSIZEARG -I $(($USR_START*512)) $IMG ${IMG_DIR}/usr.proto`/512))
HOME_START=$(($USR_START + $USR_SIZE)) HOME_START=$(($USR_START + $USR_SIZE))
echo " - HOME"
HOME_SIZE=$((`${CROSS_TOOLS}/nbmkfs.mfs -d $HOMESIZEARG -I $(($HOME_START*512)) $IMG ${IMG_DIR}/home.proto`/512))
# ${CROSS_TOOLS}/nbpartition -m ${IMG} 0 81:${ISO_SIZE} 81:${ROOT_SIZE} 81:${USR_SIZE} 81:${HOME_SIZE}
# Merge the partitions into a single image.
#
echo "Merging file systems"
dd if=${IMG_DIR}/iso.img of=${IMG} seek=$ISO_START conv=notrunc
dd if=${IMG_DIR}/root.img of=${IMG} seek=$ROOT_START conv=notrunc
dd if=${IMG_DIR}/usr.img of=${IMG} seek=$USR_START conv=notrunc
dd if=${IMG_DIR}/home.img of=${IMG} seek=$HOME_START conv=notrunc
${CROSS_TOOLS}/nbpartition -m ${IMG} ${ISO_START} 81:${ISO_SIZE} 81:${ROOT_SIZE} 81:${USR_SIZE} 81:${HOME_SIZE}
mods="`( cd $MODDIR; echo mod* | tr ' ' ',' )`" mods="`( cd $MODDIR; echo mod* | tr ' ' ',' )`"
if [ "$ISOMODE" ] if [ "$ISOMODE" ]

View file

@ -52,7 +52,6 @@
#if !defined(__minix) #if !defined(__minix)
#define mul64u(a,b) ((uint64_t)(a) * (b)) #define mul64u(a,b) ((uint64_t)(a) * (b))
#define lseek64(a,b,c,d) lseek(a,b,c)
#endif #endif
/* some Minix specific types that do not conflict with Posix */ /* some Minix specific types that do not conflict with Posix */
@ -88,6 +87,7 @@ int lct = 0, fd, print = 0;
int simple = 0, dflag = 0, verbose = 0; int simple = 0, dflag = 0, verbose = 0;
int donttest; /* skip test if it fits on medium */ int donttest; /* skip test if it fits on medium */
char *progname; char *progname;
uint64_t fs_offset_bytes, fs_offset_blocks, written_fs_size = 0;
time_t current_time; time_t current_time;
char *zero; char *zero;
@ -128,11 +128,13 @@ __dead void pexit(char const *s, ...) __printflike(1,2);
void *alloc_block(void); void *alloc_block(void);
void print_fs(void); void print_fs(void);
int read_and_set(block_t n); int read_and_set(block_t n);
void special(char *string); void special(char *string, int insertmode);
__dead void usage(void); __dead void usage(void);
void get_block(block_t n, void *buf); void get_block(block_t n, void *buf);
void get_super_block(void *buf); void get_super_block(void *buf);
void put_block(block_t n, void *buf); void put_block(block_t n, void *buf);
static uint64_t mkfs_seek(uint64_t pos, int whence);
static ssize_t mkfs_write(void * buf, size_t count);
/*================================================================ /*================================================================
* mkfs - make filesystem * mkfs - make filesystem
@ -145,6 +147,7 @@ main(int argc, char *argv[])
ino_t inodes, root_inum; ino_t inodes, root_inum;
char *token[MAX_TOKENS], line[LINE_LEN], *sfx; char *token[MAX_TOKENS], line[LINE_LEN], *sfx;
struct fs_size fssize; struct fs_size fssize;
int insertmode = 0;
progname = argv[0]; progname = argv[0];
@ -157,7 +160,7 @@ main(int argc, char *argv[])
#endif #endif
zone_shift = 0; zone_shift = 0;
extra_space_percent = 0; extra_space_percent = 0;
while ((ch = getopt(argc, argv, "B:b:di:ltvx:z:")) != EOF) while ((ch = getopt(argc, argv, "B:b:di:ltvx:z:I:")) != EOF)
switch (ch) { switch (ch) {
#ifndef MFS_STATIC_BLOCK_SIZE #ifndef MFS_STATIC_BLOCK_SIZE
case 'B': case 'B':
@ -179,6 +182,10 @@ main(int argc, char *argv[])
break; break;
(void)sfx; /* shut up warnings about unused variable...*/ (void)sfx; /* shut up warnings about unused variable...*/
#endif #endif
case 'I':
fs_offset_bytes = strtoul(optarg, (char **) NULL, 0);
insertmode = 1;
break;
case 'b': case 'b':
blocks = bblocks = strtoul(optarg, (char **) NULL, 0); blocks = bblocks = strtoul(optarg, (char **) NULL, 0);
break; break;
@ -253,9 +260,11 @@ main(int argc, char *argv[])
*/ */
zero = alloc_block(); zero = alloc_block();
fs_offset_blocks = roundup(fs_offset_bytes, block_size) / block_size;
/* Determine the size of the device if not specified as -b or proto. */ /* Determine the size of the device if not specified as -b or proto. */
maxblocks = sizeup(argv[optind]); maxblocks = sizeup(argv[optind]);
if (bblocks != 0 && bblocks > maxblocks){ if (bblocks != 0 && bblocks + fs_offset_blocks > maxblocks && !insertmode) {
errx(4, "Given size -b %d exeeds device capacity(%d)\n", bblocks, maxblocks); errx(4, "Given size -b %d exeeds device capacity(%d)\n", bblocks, maxblocks);
} }
@ -274,7 +283,7 @@ main(int argc, char *argv[])
*/ */
if (argc - optind != 2 && (argc - optind != 1 || blocks == 0)) usage(); if (argc - optind != 2 && (argc - optind != 1 || blocks == 0)) usage();
if (maxblocks && blocks > maxblocks) { if (maxblocks && blocks > maxblocks && !insertmode) {
errx(1, "%s: number of blocks too large for device.", argv[optind]); errx(1, "%s: number of blocks too large for device.", argv[optind]);
} }
@ -315,7 +324,7 @@ main(int argc, char *argv[])
blocks += blocks*extra_space_percent/100; blocks += blocks*extra_space_percent/100;
inodes += inodes*extra_space_percent/100; inodes += inodes*extra_space_percent/100;
/* XXX is it OK to write on stdout? Use warn() instead? Also consider using verbose */ /* XXX is it OK to write on stdout? Use warn() instead? Also consider using verbose */
printf("dynamically sized filesystem: %u blocks, %u inodes\n", fprintf(stderr, "dynamically sized filesystem: %u blocks, %u inodes\n",
(unsigned int) blocks, (unsigned int) inodes); (unsigned int) blocks, (unsigned int) inodes);
} }
} else { } else {
@ -362,7 +371,7 @@ main(int argc, char *argv[])
(unsigned)umap_array_elements); (unsigned)umap_array_elements);
/* Open special. */ /* Open special. */
special(argv[--optind]); special(argv[--optind], insertmode);
if (!donttest) { if (!donttest) {
uint16_t *testb; uint16_t *testb;
@ -371,19 +380,13 @@ main(int argc, char *argv[])
testb = alloc_block(); testb = alloc_block();
/* Try writing the last block of partition or diskette. */ /* Try writing the last block of partition or diskette. */
if(lseek64(fd, mul64u(blocks - 1, block_size), SEEK_SET, NULL) < 0) { mkfs_seek(mul64u(blocks - 1, block_size), SEEK_SET);
err(1, "couldn't seek to last block to test size (1)");
}
testb[0] = 0x3245; testb[0] = 0x3245;
testb[1] = 0x11FF; testb[1] = 0x11FF;
testb[block_size/2-1] = 0x1F2F; testb[block_size/2-1] = 0x1F2F;
if ((w=write(fd, testb, block_size)) != block_size) w=mkfs_write(testb, block_size);
err(1, "File system is too big for minor device (write1 %d/%u)",
w, block_size);
sync(); /* flush write, so if error next read fails */ sync(); /* flush write, so if error next read fails */
if(lseek64(fd, mul64u(blocks - 1, block_size), SEEK_SET, NULL) < 0) { mkfs_seek(mul64u(blocks - 1, block_size), SEEK_SET);
err(1, "couldn't seek to last block to test size (2)");
}
testb[0] = 0; testb[0] = 0;
testb[1] = 0; testb[1] = 0;
testb[block_size/2-1] = 0; testb[block_size/2-1] = 0;
@ -395,13 +398,12 @@ main(int argc, char *argv[])
testb[0], testb[1], testb[block_size-1]); testb[0], testb[1], testb[block_size-1]);
errx(1, "File system is too big for minor device (read)"); errx(1, "File system is too big for minor device (read)");
} }
lseek64(fd, mul64u(blocks - 1, block_size), SEEK_SET, NULL); mkfs_seek(mul64u(blocks - 1, block_size), SEEK_SET);
testb[0] = 0; testb[0] = 0;
testb[1] = 0; testb[1] = 0;
testb[block_size/2-1] = 0; testb[block_size/2-1] = 0;
if (write(fd, testb, block_size) != block_size) mkfs_write(testb, block_size);
err(1, "File system is too big for minor device (write2)"); mkfs_seek(0L, SEEK_SET);
lseek(fd, 0L, SEEK_SET);
free(testb); free(testb);
} }
@ -426,6 +428,8 @@ main(int argc, char *argv[])
(int)next_inode-1, next_zone); (int)next_inode-1, next_zone);
} }
if(insertmode) printf("%ld\n", written_fs_size);
return(0); return(0);
/* NOTREACHED */ /* NOTREACHED */
@ -560,9 +564,7 @@ sizeup(char * device)
progname, (unsigned long)d); progname, (unsigned long)d);
} }
#else #else
size = lseek(fd, 0, SEEK_END); size = mkfs_seek(0, SEEK_END);
if (size == (off_t) -1)
err(1, "cannot get device size fd=%d: %s", fd, device);
/* Assume block_t is unsigned */ /* Assume block_t is unsigned */
if (size / block_size > (block_t)(-1ul)) { if (size / block_size > (block_t)(-1ul)) {
d = (block_t)(-1ul); d = (block_t)(-1ul);
@ -691,10 +693,8 @@ super(zone_t zones, ino_t inodes)
#endif #endif
} }
if (lseek(fd, (off_t) SUPER_BLOCK_BYTES, SEEK_SET) == (off_t) -1) mkfs_seek((off_t) SUPER_BLOCK_BYTES, SEEK_SET);
err(1, "super() couldn't seek"); mkfs_write(buf, SUPER_BLOCK_BYTES);
if (write(fd, buf, SUPER_BLOCK_BYTES) != SUPER_BLOCK_BYTES)
err(1, "super() couldn't write");
/* Clear maps and inodes. */ /* Clear maps and inodes. */
for (i = START_BLOCK; i < initblks; i++) put_block((block_t) i, zero); for (i = START_BLOCK; i < initblks; i++) put_block((block_t) i, zero);
@ -1541,19 +1541,20 @@ read_and_set(block_t n)
__dead void __dead void
usage(void) usage(void)
{ {
fprintf(stderr, "Usage: %s [-dltv] [-b blocks] [-i inodes] [-z zone_shift]\n" fprintf(stderr, "Usage: %s [-dltv] [-b blocks] [-i inodes]\n"
"\t[-x extra] [-B blocksize] special [proto]\n", "\t[-z zone_shift] [-I offset] [-x extra] [-B blocksize] special [proto]\n",
progname); progname);
exit(4); exit(4);
} }
void void
special(char * string) special(char * string, int insertmode)
{ {
fd = creat(string, 0777); int openmode = O_RDWR;
close(fd); if(!insertmode) openmode |= O_TRUNC;
fd = open(string, O_RDWR); fd = open(string, O_RDWR | O_CREAT, 0644);
if (fd < 0) err(1, "Can't open special file %s", string); if (fd < 0) err(1, "Can't open special file %s", string);
mkfs_seek(0, SEEK_SET);
} }
@ -1569,8 +1570,7 @@ get_block(block_t n, void *buf)
memcpy(buf, zero, block_size); memcpy(buf, zero, block_size);
return; return;
} }
if (lseek64(fd, mul64u(n, block_size), SEEK_SET, NULL) == (off_t)(-1)) mkfs_seek(mul64u(n, block_size), SEEK_SET);
pexit("get_block couldn't seek");
k = read(fd, buf, block_size); k = read(fd, buf, block_size);
if (k != block_size) if (k != block_size)
pexit("get_block couldn't read block #%u", (unsigned)n); pexit("get_block couldn't read block #%u", (unsigned)n);
@ -1582,8 +1582,7 @@ get_super_block(void *buf)
{ {
ssize_t k; ssize_t k;
if(lseek(fd, (off_t) SUPER_BLOCK_BYTES, SEEK_SET) == (off_t) -1) mkfs_seek((off_t) SUPER_BLOCK_BYTES, SEEK_SET);
err(1, "seek for superblock failed");
k = read(fd, buf, SUPER_BLOCK_BYTES); k = read(fd, buf, SUPER_BLOCK_BYTES);
if (k != SUPER_BLOCK_BYTES) if (k != SUPER_BLOCK_BYTES)
err(1, "get_super_block couldn't read super block"); err(1, "get_super_block couldn't read super block");
@ -1596,8 +1595,49 @@ put_block(block_t n, void *buf)
(void) read_and_set(n); (void) read_and_set(n);
if (lseek64(fd, mul64u(n, block_size), SEEK_SET, NULL) == (off_t) -1) mkfs_seek(mul64u(n, block_size), SEEK_SET);
pexit("put_block couldn't seek"); mkfs_write(buf, block_size);
if (write(fd, buf, block_size)!= block_size) }
pexit("put_block couldn't write block #%u", (unsigned)n);
static ssize_t
mkfs_write(void * buf, size_t count)
{
uint64_t fssize;
ssize_t w;
/* Perform & check write */
w = write(fd, buf, count);
if(w < 0)
err(1, "mkfs_write: write failed");
if(w != count)
errx(1, "mkfs_write: short write: %ld != %ld", w, count);
/* Check if this has made the FS any bigger; count bytes after offset */
fssize = mkfs_seek(0, SEEK_CUR);
assert(fssize >= fs_offset_bytes);
fssize -= fs_offset_bytes;
fssize = roundup(fssize, block_size);
if(fssize > written_fs_size)
written_fs_size = fssize;
return w;
}
/* Seek to position in FS we're creating. */
static uint64_t
mkfs_seek(uint64_t pos, int whence)
{
if(whence == SEEK_SET) pos += fs_offset_bytes;
#ifdef __minix
uint64_t newpos;
if((lseek64(fd, pos, whence, &newpos)) < 0)
err(1, "mkfs_seek: lseek64 failed");
return newpos;
#else
off_t newpos;
if((newpos=lseek(fd, pos, whence)) == (off_t) -1)
err(1, "mkfs_seek: lseek failed");
return newpos;
#endif
} }

View file

@ -12,6 +12,7 @@
.Op Fl b Ar blocks .Op Fl b Ar blocks
.Op Fl z Ar zone_shift .Op Fl z Ar zone_shift
.Op Fl x Ar extra_space .Op Fl x Ar extra_space
.Op Fl I Ar fs_offset
.Ar special .Ar special
.Op Ar prototype .Op Ar prototype
.Sh OPTIONS .Sh OPTIONS
@ -35,6 +36,8 @@ Number of i-nodes (files)
Filesystem block size (in bytes) Filesystem block size (in bytes)
.It Fl b Ar blocks .It Fl b Ar blocks
Filesystem size (in blocks) Filesystem size (in blocks)
.It Fl I Ar fs_offset
Write filesystem starting at offset (in bytes)
.It Fl x Ar extra_space .It Fl x Ar extra_space
Extra space after dynamic sizing (blocks and inodes) Extra space after dynamic sizing (blocks and inodes)
.It Fl z Ar zone_shift .It Fl z Ar zone_shift
@ -118,4 +121,4 @@ For special files, the major and minor devices are needed.
The The
.Nm .Nm
utility was written by utility was written by
.An Andy Tanenbaum, Paul Ogilvie, Frans Meulenbroeks, Bruce Evans .An Andy Tanenbaum, Paul Ogilvie, Frans Meulenbroeks, Bruce Evans