commit 10ef6c622b262361c4dd98306a2dda55416534de
parent 038a2e90c7beb02793240812fd79a8e73acea0fd
Author: Christos Margiolis <christos@margiolis.net>
Date: Sun, 10 Jan 2021 22:30:40 +0200
new assignments done
Diffstat:
11 files changed, 459 insertions(+), 68 deletions(-)
diff --git a/c-parallel-computing/ex2/doc.pdf b/c-parallel-computing/ex2/doc.pdf
Binary files differ.
diff --git a/c-parallel-computing/ex2/doc.tex b/c-parallel-computing/ex2/doc.tex
@@ -6,11 +6,19 @@
\usepackage{listings}
\usepackage{mathtools}
\usepackage{xcolor}
-\usepackage[backend=biber]{biblatex}
+\usepackage{biblatex}
+\usepackage[left=2cm,right=2cm]{geometry}
+
+\lstset {
+ basicstyle=\ttfamily,
+ columns=fullflexible,
+ breaklines=true,
+ keepspaces=true
+}
\title{Εργαστήριο Παράλληλου Υπολογισμού - Εργασία 2}
\author{Χρήστος Μαργιώλης}
-\date{Δεκέμβριος 2020}
+\date{Ιανουάριος 2020}
\begin{document}
@@ -21,4 +29,220 @@
\renewcommand{\contentsname}{Περιεχόμενα}
\tableofcontents
+\section{Τεκμηρίωση}
+
+Το πρόγραμμα, προκειμένου να υλοποίησει τα ερωτήματα της άσκησης,
+ακολουθεί την εξής δομή:
+\begin{itemize}
+ \item Δέχεται το διάνυσμα $X$ και το μήκος του στον επεξεργαστή 0.
+ \item Υπολογίζει τα παρακάτω ωστέ να είναι πιο
+ εύκολη και οργανωμένη η υλοποίηση του υπόλοιπου
+ προγράμματος.
+ \begin{itemize}
+ \item $X_{min}$
+ \item $X_{max}$
+ \item $m$ - Μέση τιμή
+ \item \lstinline{scounts} και \lstinline{displs} - Για να
+ χρησιμοποιηθούν από τις \lstinline{MPI_Scatterv(3)}
+ και \lstinline{MPI_Gatherv(3)}.
+ \item Τοπικό $n$ για τον κάθε επεξεργαστή καθώς και το
+ διάνυσμα που του αναλογεί.
+ \end{itemize}
+ \item Αφού γίνουν επιτυχώς τα παραπάνω, υλοποιεί τα ερωτήματα.
+ \begin{itemize}
+ \item Πόσα στοιχεία είναι μικρότερα ή μεγαλύτερα της μέσης τιμής $m$.
+ \item Την διασπορά των στοιχείων του διανύσματος $X$.
+ \item Διάνυσμα $Δ$.
+ \item Την μεγαλύτερη τιμή του διανύσματος $Δ$ καθώς και την
+ θέση της στο διάνυσμα.
+ \item Το διάνυσμα προθεμάτων (prefix sums) των στοιχείων
+ του διανύσματος $X$.
+ \end{itemize}
+ \item Εμφανίζει όλα τα ζητούμενα αποτελέσματα στον επεξεργαστή 0.
+\end{itemize}
+
+Αυτό που αξίζει προσοχή είναι οι πίνακες \lstinline{scounts} και
+\lstinline{displs} που ανέφερα παραπάνω. Προκειμένου να μοιράζεται σωστά το
+διάνυσμα ακόμα και στην περίπτωση που το $n$ δεν είναι ακέραιο πολλαπλάσιο του
+$p$, πρέπει να χρησιμοποιηθούνε οι συναρτήσεις \lstinline{MPI_Scatterv(3)} και
+\lstinline{MPI_Gatherv(3)} - αυτές οι συναρτήσεις είναι παρόμοιες με τις
+\lstinline{MPI_Scatter(3)} και \lstinline{MPI_Gather(3)}, με την διαφορά ότι
+μπορούνε να δεχτούνε μεταβλητό αριθμό στοιχείων προς αποστολή στον κάθε επεξεργαστή.
+Για παράδειγμα, ας υποθέσουμε ότι είμαστε στην περίπτωση που το $n$ \textit{είναι}
+πολλαπλάσιο του $p$ και έχουμε το εξής input:
+
+\[p = 4\]
+\[n = 8 \]
+\[X = \{1, 2, 3, 4, 5, 6, 7, 8\}\]
+Τότε, μπορούμε να ισομοιράσουμε τα στοιχεία στους $p = 4$ επεξεργαστές ως εξής:
+\[p_0 = \{1, 2\}\]
+\[p_1 = \{3, 4\}\]
+\[p_2 = \{5, 6\}\]
+\[p_3 = \{7, 8\}\]
+
+Στην περίπτωση όμως που το $n$ \textit{δεν} είναι ακέραιο πολλαπλάσιο του $p$,
+χρειαζόμαστε να μοιράσουμε τα στοιχεία έτσι ώστε όλοι οι επεξεργαστές να έχουνε
+μέχρι 1 στοιχείο παραπάνω, προκειμένου ο υπολογιστικός φόρτος να είναι όσο το
+δυνατόν πιο ίσα μοιρασμένος. Οπότε, έχοντας το παρακάτω input ως παράδειγμα:
+
+\[p = 4\]
+\[n = 11 \]
+\[X = \{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11\}\]
+Με βάση τα παραπάνω λεγόμενά μου, τα στοιχεία πρέπει να μοιραστούν ως εξής:
+\[p_0 = \{1, 2, 3\}\]
+\[p_1 = \{4, 5, 6\}\]
+\[p_2 = \{7, 8, 9\}\]
+\[p_3 = \{10, 11\}\]
+
+Προκειμένου να επιλυθεί αυτό το πρόβλημα, χρησιμοποιούμε, όπως προανέφερα,
+τις συναρτήσεις \lstinline{MPI_Scatterv(3)} και \lstinline{MPI_Gatherv(3)}.
+Τα νέα ορίσματα που μάς απασχολούνε είναι τα εξής δύο:
+
+\begin{itemize}
+ \item \lstinline{scounts} - αριθμός στοιχείων που παίρνει ο κάθε επεξεργαστής
+ \item \lstinline{displs} - offset στο γενικό διάνυσμα
+\end{itemize}
+
+Και τα δύο ορίσματα (πίνακες) έχουνε μήκος $p$, ωστέ σε κάθε
+θέση του πίνακα να υπάρχουνε οι κατάλληλες πληροφορίες για το πώς θα μοιραστούν
+τα στοιχεία στον κάθε επεξεργαστή.
+
+Έπειτα, θέτουμε τις κατάλληλες τιμές για το τοπικό $n$ και διάνυσμα. Στον κώδικα
+έχω κάνει \lstinline{localn = scounts[rank]} το οποίο, αν και προφανές, σημαίνει
+ότι το κάθε τοπικό διάνυσμα έχει μήκος όσο και τα στοιχεία που υπολογίσαμε ότι
+θα έχει ο επεξεργαστής που θα το δεχτεί. Στην συνέχεια στέλνουμε - επιτέλους -
+μέσω της \lstinline{MPI_Scatterv(3)} σε όλους τους επεξεργαστές το διάνυσμα που
+τούς αναλογεί.
+
+Μετά από τα παραπάνω βήματα, ξεκινάνε οι υπολογισμοί για τα ζητούμενα της άσκησης,
+οπότε θα εξήγησω στο επόμενος μέρος περιληπτικά τί κάνει η κάθε συνάρτηση που έχω
+υλοποιήσει με την σειρά που εκτελούνται στο πρόγραμμα.
+
+\section{Συναρτήσεις}
+
+\subsection{\lstinline{float input(const char *fmt, int i)}}
+
+Δίνει την δυνατότητα για formatted input.
+
+\subsection{\lstinline{float find(int flag)}} \label{find}
+
+Βρίσκει το $X_{min}$ και $X_{max}$ ανάλογα με το flag που
+τής έχει δωθεί. Τα flags που μπορούνε να δωθούν είναι τα εξής
+\begin{itemize}
+ \item \lstinline{FIND_XMIN}
+ \item \lstinline{FIND_XMAX}
+\end{itemize}
+Προκειμένου να υπολογίσει οποιαδήποτε από τις δύο τιμές ακολουθεί τον
+εξής αλγόριθμο:
+\begin{itemize}
+ \item Για κάθε στοιχείο του τοπικού διανύσματος του εκάστοτε
+ επεξεργαστή, βρες το τοπικό μέγιστο ή ελάχιστο.
+ \item Μάζεψε τα αποτέλεσματα από όλους τους επεξεργαστές στον
+ \lstinline{root}.
+ \item Βρες το ολικό μέγιστο ή ελάχιστο με την ίδια λογική όπως
+ στο πρώτο βήμα.
+ \item Στείλε το αποτέλεσμα σε όλους τους επεξεργαστές.
+\end{itemize}
+
+\subsection{\lstinline{float calcavg(void)}} \label{calcavg}
+
+Υπολογίζει την ολική μέση τιμή. Αρχικά βρίσκει όλα τα
+τοπικά μέγιστα, τα μαζεύει στον \lstinline{root} επεξεργαστή,
+ο οποίος βρίσκει την ολική μέση τιμή και την στέλνει σε
+όλους τους υπόλοιπους επεξεργαστές.
+
+\subsection{\lstinline{float findavg(float *v, int len)}}
+
+Βοηθητική συνάρτηση για υπολογισμό μέσης τιμής. Χρησιμοποιείται από την
+\lstinline{calcavg} [\ref{calcavg}].
+
+\subsection{\lstinline{int count(float avg, int flag)}}
+
+Υπολογίζει πόσα στοιχεία είναι είτε μεγαλύτερα είτε μικρότερα της ολικής
+μέσης τιμής $m$. Το τί από τα δύο θα υπολογίσει εξαρτάται από το flag που
+θα τής δωθεί. Τα flags που δέχεται είναι τα εξής:
+\begin{itemize}
+ \item \lstinline{COUNT_BELOW_AVG}
+ \item \lstinline{COUNT_ABOVE_AVG}
+\end{itemize}
+
+Για τους υπολογισμούς ακολουθεί παρόμοια λογική με την \lstinline{find} [\ref{find}].
+
+\subsection{\lstinline{float calcvar(float avg)}}
+
+Υπολογίζει την ολική διασπορά των στοιχείων του διανύσματος $X$. Ως όρισμα
+δέχεται την μέση τιμή $m$ που υπολογίζει η \lstinline{calcavg} [\ref{calcavg}].
+Ο τύπος που χρησιμοποιεί για τον υπολογισμό της τοπικής διασποράς είναι:
+\[var_{local} = \sum_{i = 0}^{n_{local}} (x_{i} - m)^2\]
+Αφού μαζέψει όλα τα τοπικά αποτελέσματα στον επεξεργαστή \lstinline{root},
+τα αθροίζει και τα διαιρεί δια $n - 1$, ώστε να ολοκληρωθεί ο τύπος της διασποράς.
+\[var = \frac{1}{n - 1} \cdot \sum_{i = 0}^{n - 1} (x_i - m)^2\]
+
+\subsection{\lstinline{float *calcd(float xmin, float xmax)}}
+
+Δημιουργεί ένα νέο διάνυσμα $Δ$, του οποίο το κάθε στοιχείο $δ_i$ είναι ίσο
+με
+\[δ_i = ((x_i - x_{min}) / (x_{max} - x_{min})) \cdot 100 \]
+
+Αφού υπολογίσει τον παραπάνω τύπο για κάθε στοιχείο όλων των τοπικών διανυσμάτων,
+μαζεύει όλα τα διανύσματα στον επεξεργαστή \lstinline{root} μέσω της
+\lstinline{MPI_Gatherv(3)}.
+
+\subsection{\lstinline{Pair findmax(float *d)}}
+
+Βρίσκει το ολικό μέγιστο στο διάνυσμα $Δ$ καθώς και την θέση του στο διάνυσμα.
+Αρχικά, αυτό που επιστρέφει η συνάρτηση είναι ένα \lstinline{struct Pair}, το
+οποίο είναι ένα \lstinline{struct} που δημιούργησα ώστε να αποθηκεύσω τα δύο
+αποτέλεσματα που θα παράξει η συνάρτηση αυτή ($D_{max}$ και $D_{maxloc}$).
+
+Ο αλγόριθμος που ακολουθεί η συνάρτηση είναι ο εξής:
+\begin{itemize}
+ \item Για κάθε στοιχείο του γενικού διανύσματος $Δ$, ψάξε
+ το μέγιστο στοιχείο και την θέση του.
+ \item Αφού βρεθεί, με την χρήση της \lstinline{MPI_Reduce(3)},
+ βρες την θέση το ολικό μέγιστο καθώς και την θέση του και αποθήκευσέ
+ τα στο \lstinline{out} στον επεξεργαστή \lstinline{root}.
+\end{itemize}
+
+\subsection{\lstinline{float *calcpfxsums(void)}}
+
+Υπολογίζει το διάνυσμα προθεμάτων (prefix sums) των στοιχείων του
+διανύσματος $X$. Προκειμένου να γίνει αυτό χρησιμοποείται η συνάρτηση
+\lstinline{MPI_Scan(3)}, η οποία κάνει όλους τους απαραίτητους υπολογισμούς.
+Εμείς το μόνο που έχουμε να κάνουμε είναι να αποθηκεύσουμε τα αποτελέσματα στον
+πίνακα \lstinline{pfxsums}. Σημαντικό να σημειωθεί ότι αυτή η συνάρτηση
+εκτελείται επιτυχώς \textit{μόνο} στην περίπτωση που $n = p$.
+
+\subsection{\lstinline{void printv(const char *str, const float *v)}}
+
+Βοηθτική συνάρτηση για να τυπώνει διανύσματα με πιο όμορφο τρόπο.
+
+\subsection{\lstinline{void *emalloc(size_t nb)}}
+
+\lstinline{malloc(3)} με error checks.
+
+\section{Κώδικας}
+\lstinputlisting[language=C]{ex2.c}
+
+\section{Προβλήματα}
+
+Δεν κατάφερα να βρω πώς να υλοποιηθεί η εύρεση του διανύσματος προθεμάτων
+στην περίπτωση που το $n \neq p$.
+
+\section{Ενδεικτικά τρεξίματα}
+
+Input: $p = 4$, $n = 7$, $X = \{1, 2, 3, 4, -1, -2, 6\}$ \\
+
+\includegraphics[width=\textwidth]{res/run1.png} \\
+
+Input: $p = 4$, $n = 11$, $X = \{1, 2, -5, 21, 13, 6, -10, 8, 9, 4, -6\}$ \\
+
+\includegraphics[width=\textwidth]{res/run2.png} \\
+
+Input: $p = 8$, $n = 8$, $X = \{2, 3, 4, 5, 6, -1, -2\}$ \\
+Εφόσον τώρα ισχύει $n = p$, η συνάρτηση για την έυρεση των
+prefix sums θα λειτουργήσει σωστά. \\
+
+\includegraphics[width=\textwidth]{res/run3.png} \\
+
\end{document}
diff --git a/c-parallel-computing/ex2/ex2.c b/c-parallel-computing/ex2/ex2.c
@@ -2,18 +2,18 @@
#include <stdlib.h>
#include <mpi.h>
-/* constants */
+/* Flags */
#define FIND_XMIN 1 << 0
#define FIND_XMAX 1 << 1
#define COUNT_BELOW_AVG 1 << 2
#define COUNT_ABOVE_AVG 1 << 3
-struct Pair {
- float val; /* max value in array */
- int i; /* index of max */
-};
+typedef struct {
+ float val; /* Max value in array */
+ int i; /* Index of max */
+} Pair;
-/* function declarations */
+/* Function declarations */
static float input(const char *, int);
static float find(int);
static float findavg(float *, int);
@@ -21,18 +21,19 @@ static float calcavg(void);
static int count(float, int);
static float calcvar(float);
static float *calcd(float, float);
-struct Pair findmax(float *);
+static Pair findmax(float *);
static float *calcpfxsums(void);
static void printv(const char *, const float *);
static void *emalloc(size_t);
-/* global variables */
+/* Global variables */
static int rank, nproc, root = 0;
static int *scounts, *displs;
static float *vec, *localvec;
static int n, localn;
-/* function implementations */
+/* Function implementations */
+/* Formatted input */
static float
input(const char *fmt, int i)
{
@@ -43,9 +44,11 @@ input(const char *fmt, int i)
printf("%s", buf);
scanf("%f", &n);
getchar();
+
return n;
}
+/* Find `xmin` and `xmax` depending on the `flag` argument. */
static float
find(int flag)
{
@@ -55,35 +58,47 @@ find(int flag)
int i;
res = emalloc(nproc * sizeof(float));
+ /*
+ * Loop through each local vector and assign the local
+ * result depending on which of the two flags is set
+ */
for (i = 0; i < localn; i++)
if ((flag & FIND_XMIN && localvec[i] < localres)
|| (flag & FIND_XMAX && localvec[i] > localres))
localres = localvec[i];
+ /* Send local results to `root` */
MPI_Gather(&localres, 1, MPI_FLOAT, res, 1, MPI_FLOAT, root, MPI_COMM_WORLD);
if (rank == root)
+ /* Same process as above */
for (i = 0; i < nproc; i++)
if ((flag & FIND_XMIN && res[i] < finalres)
|| (flag & FIND_XMAX && res[i] > finalres))
finalres = res[i];
+ /* Everyone has to know the final result */
MPI_Bcast(&finalres, 1, MPI_FLOAT, root, MPI_COMM_WORLD);
free(res);
return finalres;
}
+/*
+ * Small utility function for `calcavg` to avoid code duplication.
+ * Calcuates the average for a given vector
+ */
static float
-findavg(float *vec, int len)
+findavg(float *v, int len)
{
float sum = 0.0f;
int i = 0;
for (; i < len; i++)
- sum += vec[i];
+ sum += v[i];
return (sum / (float)len);
}
+/* Calculate the global average */
static float
calcavg(void)
{
@@ -101,6 +116,10 @@ calcavg(void)
return finalavg;
}
+/*
+ * Count how many elements are below or above average based on the
+ * `flag` argument. Similar logic as with `find` above.
+ */
static int
count(float avg, int flag)
{
@@ -122,6 +141,7 @@ count(float avg, int flag)
return finalres;
}
+/* Calculate the global variance */
static float
calcvar(float avg)
{
@@ -145,6 +165,9 @@ calcvar(float avg)
return finalvar;
}
+/* Generate D. A vector where each element is
+ * ((xi - xmin) / (xmax - xmin)) * 100.
+*/
static float *
calcd(float xmin, float xmax)
{
@@ -165,49 +188,49 @@ calcd(float xmin, float xmax)
return finald;
}
-struct Pair
+/* Find global max and MAXLOC */
+static Pair
findmax(float *d)
{
- struct Pair in, out;
- float *locald;
- int i = 0;
+ Pair in, out;
+ int i = 1;
- locald = emalloc(localn * sizeof(float));
- MPI_Scatterv(d, scounts, displs, MPI_FLOAT, locald, localn, MPI_FLOAT,
- root, MPI_COMM_WORLD);
-
- in.val = *locald;
+ in.val = *d;
in.i = 0;
- for (; i < localn; i++) {
- if (in.val < locald[i]) {
- in.val = locald[i];
+ for (; i < n; i++) {
+ if (in.val < d[i]) {
+ in.val = d[i];
in.i = i;
}
}
in.i += rank * localn;
MPI_Reduce(&in, &out, 1, MPI_FLOAT_INT, MPI_MAXLOC, root, MPI_COMM_WORLD);
- free(locald);
return out;
}
+/* Calucate the prefix sums of `vec`. Only world when
+ * n == nproc
+ */
static float *
calcpfxsums(void)
{
- float *localsums, *finalsums;
+ float *pfxsums;
float sum = 0.0f;
- localsums = emalloc(nproc * sizeof(float));
- finalsums = emalloc(n * sizeof(float));
+ pfxsums = emalloc(n * sizeof(float));
- MPI_Scan(localsums, &sum, 1, MPI_FLOAT, MPI_SUM, MPI_COMM_WORLD);
- /*MPI_Gather(&sum, 1, MPI_FLOAT, finalsums, 1, MPI_FLOAT, root, MPI_COMM_WORLD);*/
+ /* Scan each local vector and assign the result to `sum`. */
+ MPI_Scan(localvec, &sum, 1, MPI_FLOAT, MPI_SUM, MPI_COMM_WORLD);
+ /* Be in sync. */
+ MPI_Barrier(MPI_COMM_WORLD);
+ /* Get results in `pfxsums` */
+ MPI_Gather(&sum, 1, MPI_FLOAT, pfxsums, 1, MPI_FLOAT, root, MPI_COMM_WORLD);
- free(localsums);
-
- return finalsums;
+ return pfxsums;
}
+/* Utility function to print a vector in a prettier way. */
static void
printv(const char *str, const float *v)
{
@@ -219,6 +242,7 @@ printv(const char *str, const float *v)
printf("]\n");
}
+/* Error checking `malloc(3)`. */
static void *
emalloc(size_t nb)
{
@@ -234,16 +258,20 @@ emalloc(size_t nb)
int
main(int argc, char *argv[])
{
- struct Pair dmax;
+ Pair dmax;
float avg, var, xmin, xmax;
float *d, *pfxsums;
int belowavg, aboveavg;
- int i;
+ int i, rc;
- MPI_Init(&argc, &argv);
+ if ((rc = MPI_Init(&argc, &argv)) != 0) {
+ fprintf(stderr, "%s: cannot initialize MPI.\n", argv[0]);
+ MPI_Abort(MPI_COMM_WORLD, rc);
+ }
MPI_Comm_size(MPI_COMM_WORLD, &nproc);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
+ /* Read global vector. */
if (rank == root) {
n = input("N: ", 0);
vec = emalloc(n * sizeof(float));
@@ -251,51 +279,54 @@ main(int argc, char *argv[])
vec[i] = input("vec[%d]: ", i);
}
- /* move? */
+ /* Send `n` to everyone. */
MPI_Bcast(&n, 1, MPI_INT, root, MPI_COMM_WORLD);
+ /* Will be used for `MPI_Scatterv(3)` and `MPI_Gatherv(3)` later on. */
scounts = emalloc(nproc * sizeof(int));
displs = emalloc(nproc * sizeof(int));
-
for (i = 0; i < nproc; i++) {
- /* TODO: explain */
+ /* make it work even if `n` is not a multiple of `nproc`. */
scounts[i] = (i != nproc - 1) ? n / nproc : n / nproc + n % nproc;
+ /* take the last `scounts` so that we don't offset +1 each time. */
displs[i] = i * scounts[i != 0 ? i-1 : i];
}
+ /*
+ * Each local `n` is the same as the `scounts` of each process, so we
+ * assign it to `localn` for readablity.
+ */
localn = scounts[rank];
localvec = emalloc(localn * sizeof(float));
+ /* Scatter the array to each process. */
MPI_Scatterv(vec, scounts, displs, MPI_FLOAT, localvec, localn,
MPI_FLOAT, root, MPI_COMM_WORLD);
- /* part 0.1 - calculate min and max */
+ /* Part 0.1 - Calculate global minimum and maximum. */
xmin = find(FIND_XMIN);
xmax = find(FIND_XMAX);
- /* part 0.2 - calculate average */
+ /* Part 0.2 - Calculate average. */
avg = calcavg();
- /* part 1 - find how many elements are above or below average */
+ /* Part 1 - Find how many elements are above or below average. */
belowavg = count(avg, COUNT_BELOW_AVG);
aboveavg = count(avg, COUNT_ABOVE_AVG);
- /* part 2 - calculate variance */
+ /* Part 2 - Calculate variance. */
var = calcvar(avg);
- /*
- * part 3 - make a new vector where each element is:
- * ((xi - xmin) / (xmax - xmin)) * 100
- */
+ /* Part 3 - Generate D. */
d = calcd(xmin, xmax);
- /* part 4 - find dmax and dmaxloc */
+ /* Part 4 - Find dmax and dmaxloc. */
dmax = findmax(d);
- /* part 5 - prefixs sum of vec */
+ /* Part 5 - Prefix sums of `vec`. */
pfxsums = calcpfxsums();
- /* print all results */
+ /* Print all results */
if (rank == root) {
printf("\n");
printv("X: ", vec);
diff --git a/c-parallel-computing/ex2/res/run1.png b/c-parallel-computing/ex2/res/run1.png
Binary files differ.
diff --git a/c-parallel-computing/ex2/res/run2.png b/c-parallel-computing/ex2/res/run2.png
Binary files differ.
diff --git a/c-parallel-computing/ex2/res/run3.png b/c-parallel-computing/ex2/res/run3.png
Binary files differ.
diff --git a/sh-os1/ex2/bck b/sh-os1/ex2/bck
@@ -1,24 +1,40 @@
#!/bin/sh
+# ΧΡΗΣΤΟΣ ΜΑΡΓΙΩΛΗΣ - [REDACTED]
+
main() {
- # allow > 3?
+ # We could allow more than 4 command line arguments
+ # since it's not going to make a difference, but for
+ # safety, we'll exit if they're not exactly 4.
test $# -eq 3 || usage
+ # Give readable names to each argument.
username=${1}
src=${2}
dst=${3}
+ # Error checks.
+ # In order for `username` to really be a user's name, it
+ # has to exist in `/etc/passwd`.
test ! -z "$(grep -w "^${username}" /etc/passwd)" ||
err "'${username}' is not in /etc/passwd"
+
+ # The -e options means anything (file, directory, device, etc.).
test -e ${src} || err "${src}: no such file or directory"
test -e ${dst} || err "${dst}: no such file or directory"
+ # Create an archive if `dst` is a directory.
test -d ${dst} && tar -cvf "${dst}/${src##*/}.tar" ${src}
- # handle tar.*
+
+ # Append to `dst` only if it is a tar archive. If it's a non-archive
+ # then we cannot append since that would not make sense.
+ # `{dst##*.}` means that we want to get only its extension, if any.
test ${dst##*.} = "tar" && tar -rf ${dst} ${src}
}
usage() {
+ # `{0##*/}` means the first argument (i.e the script's name) with
+ # its path stripped, if it exists.
echo "usage: ${0##*/} username src dst" 1>&2
exit 1
}
@@ -28,4 +44,5 @@ err() {
exit 1
}
+# Pass all command line arguments to `main`.
main "$@"
diff --git a/sh-os1/ex2/createpvs b/sh-os1/ex2/createpvs
@@ -1,24 +1,37 @@
#!/bin/sh
+# ΧΡΗΣΤΟΣ ΜΑΡΓΙΩΛΗΣ - [REDACTED]
+
main() {
+ # We could allow more than 4 command line arguments
+ # since it's not going to make a difference, but for
+ # safety, we'll exit if they're not exactly 4.
test $# -eq 4 || usage
+ # Give readable names to each argument.
rootdir=${1}
ndbdirs=${2}
ndatadirs=${3}
username=${4}
+ # Error checks.
test -d "${rootdir}" || err "${rootdir}: no such directory"
isnumber ${ndbdirs} || err "'${ndbdirs}' is not a number"
isnumber ${ndatadirs} || err "'${ndatadirs}' is not a number"
+
+ # In order for `username` to really be a user's name, it
+ # has to exist in `/etc/passwd`.
test ! -z "$(grep -w "^${username}" /etc/passwd)" ||
err "'${username}' is not in /etc/passwd"
+ # The actual exercise.
makedirs "dbfolder" ${ndbdirs}
makedirs "datafolder" ${ndatadirs}
}
usage() {
+ # `{0##*/}` means the first argument (i.e the script's name) with
+ # its path stripped, if it exists.
echo "usage: ${0##*/} root_dir num_dbdirs num_datadirs username" 1>&2
exit 1
}
@@ -29,20 +42,40 @@ err() {
}
isnumber() {
+ # If the argument does not begin *and* end in a number, then
+ # it's not a number. This also excludes negative numbers, but
+ # we shouldn't accept negative numbers anyway.
test ! -z "$(echo "${1}" | grep "^[0-9]\+$")"
}
+# Make directories in the following format: dir1, dir2, dir3...
+# If there are already directories with the same name (e.g dir1, dir2), we need
+# to number the new ones taking the existing ones into account (i.e dir3, dir4....).
makedirs() {
+ # Number of directories to create.
n=${2}
+
+ # Get the last directory, if any, so that we know what the last number was.
lastdir=$(ls ${rootdir} | grep "${1}" | sed "s/${1}//g" | sort -V | tail -1)
+
+ # If we didn't find any, we'll start numbering them from 1.
test -z ${lastdir} && lastdir=0
+ # If `n` was already 0 we won't do anything.
while ! [ $(expr ${n}) = 0 ]; do
+ # `lastdir` now holds the next number.
lastdir=$(expr ${lastdir} + 1)
+
+ # Make a directory using the format described above
+ # and change the owner to `username`.
mkdir "${rootdir}/${1}${lastdir}" &&
chown ${username}: "${rootdir}/${1}${lastdir}"
+
+ # Decrement by 1 until we reach `n = 0` so that
+ # we can exit the loop.
n=$(expr ${n} - 1)
done
}
+# Pass all command line arguments to `main`.
main "$@"
diff --git a/sh-os1/ex2/mfproc b/sh-os1/ex2/mfproc
@@ -1,5 +1,7 @@
#!/bin/sh
+# ΧΡΗΣΤΟΣ ΜΑΡΓΙΩΛΗΣ - [REDACTED]
+
# Exit codes:
# 0 Success
# 1 No user
@@ -7,47 +9,82 @@
# 3 Usage
main() {
- # TODO: explain getopts(1)
+ # Using getopts(1) because it's easier to parse
+ # optional command line options. getopts(1) works
+ # by passing it all the available options. If an option
+ # is followed by a ':', that means that getopts(1) is
+ # expecting an argument as well. If no argument is passed,
+ # it exits with an error code of 1. In this case I've
+ # passed it the string "u:s:" which means that the options
+ # are '-u' and '-s' and both of them take an argument.
+ # The argument is always stored in the `OPTARG` environmental
+ # variable.
while getopts u:s: arg; do
case "${arg}" in
+ # In order for `username` to really be a user's name, it
+ # has to exist in `/etc/passwd`.
u) test ! -z "$(grep -w "^${OPTARG}" /etc/passwd)" ||
err "'${OPTARG}' is not in /etc/passwd" 1
+ # Get UID from username.
uid=$(id -u ${OPTARG}) ;;
s) state="${OPTARG}"
+ # `state` needs to be either S, R or Z in order
+ # to be valid.
validstate ${state} || usage ;;
*) usage
esac
done
count=0
+ # Print header. expand(1) will will expand \t to 16 spaces
+ # in order for the output to be aligned.
printf "Name\tPID\tPPID\tUID\tGID\tState\n" | expand -t 16
+
+ # Parse each process in `/proc`. This is a slow way to parse it though.
for proc in /proc/*/status; do
procuid=$(getfield "Uid:\s*${uid}" ${proc})
procgid=$(getfield "Gid" ${proc})
+ # If the `-u` option was passed, ignore every UID that
+ # does not match the UID we gave it as an argument.
+ # We're using a `continue` command here so that it
+ # stop searching for anything else in this file since
+ # we don't need it.
test -z ${uid} || [ "${procuid}" == "${uid}" ] || continue
procstate=$(getfield "State:\s*${state}" ${proc})
+
# Ignore any state other than S|R|Z
validstate ${procstate} || continue
+
+ # If the `-s` option was passed, ignore every state that does
+ # not match the one we gave it as an argument.
test -z ${state} || [ "${procstate}" == "${state}" ] || continue
procname=$(getfield "Name" ${proc})
procpid=$(getfield "Pid" ${proc})
procppid=$(getfield "PPid" ${proc})
+ # Print everything in a top(1)-like format.
printf "%s\t%s\t%s\t%s\t%s\t%s\n" \
${procname} ${procpid} ${procppid} ${procuid} ${procgid} ${procstate} |
expand -t 16
+
+ # Increment counter by 1 to keep track of how many processes
+ # we have printed. If `count` is 0 after the loop, the script
+ # will exit with error code 2, as is done below.
count=$(expr ${count} + 1)
done
# We didn't print any process
test ${count} -eq 0 && exit 2
+
# Success!
exit 0
}
usage() {
+ # `{0##*/}` means the first argument (i.e the script's name) with
+ # its path stripped, if it exists.
echo "usage: ${0##*/} [-u username] [-s S|R|Z]" 1>&2
exit 3
}
@@ -57,12 +94,17 @@ err() {
exit ${2}
}
+# Check if process is either S, R or Z.
validstate() {
[ "${1}" = "S" ] || [ "${1}" = "R" ] || [ "${1}" = "Z" ]
}
+# Get wanted field. grep(1) will return the line
+# that begins with the first argument we passed.
+# awk(1) is used to extract the 2nd field in the string.
getfield() {
grep -w "^${1}" ${2} | awk '{print $2}'
}
+# Pass all command line arguments to `main`.
main "$@"
diff --git a/sh-os1/ex2/searching b/sh-os1/ex2/searching
@@ -1,32 +1,43 @@
#!/bin/sh
+# ΧΡΗΣΤΟΣ ΜΑΡΓΙΩΛΗΣ - [REDACTED]
+
main() {
+ mode=${1}
+ days=${2}
+ # Command line arguments must be 2.
test $# -eq 2 || usage
- isnumber ${1}
- test ${1} -ge 0 && test ${1} -le 7777 || err "'${1}' is not a valid mode"
- isnumber ${2}
+ # Check if `1` and `2` are numbers, otherwise exit.
+ isnumber ${mode} || err "'${mode}' is not a valid number"
+ isnumber ${days} || err "'${days}' is not a valid number"
+ # In order for `mode` to really be a valid mode, `0 <= x <= 7777` must
+ # satisfied, since permission modes can range from 0 to 7777.
+ test ${mode} -ge 0 && test ${mode} -le 7777 || err "'${mode}' is not a valid mode"
+ # Pretty ugly loop.
sum1=0; sum2=0; sum3=0; sum4=0; sum5=0
+ # Exit if `cont` is "n".
while [ ! "${cont}" = "n" ] ; do
read -erp "Directory name: " dir
+ # Exit if nothing was typed.
test ! -z "${dir}" || err "please give a directory name"
+ # Check if `dir` really is a directory.
test -d "${dir}" || err "'${dir}' is not a directory"
- # TODO: minimize calls
- echo "Files with permissions ${1}: "
- find "${dir}" -perm ${1} 2> /dev/null
- sum1=$((${sum1}+$(find "${dir}" -perm ${1} 2> /dev/null | wc -l)))
+ echo "Files with permissions ${mode}: "
+ find "${dir}" -perm ${mode} 2> /dev/null
+ sum1=$((${sum1}+$(find "${dir}" -perm ${mode} 2> /dev/null | wc -l)))
echo ""
- echo "Files modified within the last ${2} days: "
- find "${dir}" -mtime -${2} 2> /dev/null
- sum2=$((${sum2}+$(find "${dir}" -mtime -${2} 2> /dev/null | wc -l)))
+ echo "Files modified within the last ${days} days: "
+ find "${dir}" -mtime -${days} 2> /dev/null
+ sum2=$((${sum2}+$(find "${dir}" -mtime -${days} 2> /dev/null | wc -l)))
echo ""
- echo "Subdirectories accessed within the last ${2} days: "
- find "${dir}" -type d -atime -${2} 2> /dev/null
- sum3=$((${sum3}+$(find "${dir}" -type d -atime -${2} 2> /dev/null | wc -l)))
+ echo "Subdirectories accessed within the last ${days} days: "
+ find "${dir}" -type d -atime -${days} 2> /dev/null
+ sum3=$((${sum3}+$(find "${dir}" -type d -atime -${days} 2> /dev/null | wc -l)))
echo ""
echo "Files which all users have read access to: "
@@ -41,6 +52,7 @@ main() {
read -erp "Do you want to continue (y/n)? " cont
test "${cont}" = "n" &&
+
printf "Total entries found for:\n1. %s\n2. %s\n3. %s\n4. %s\n5. %s\n" \
"${sum1}" "${sum2}" "${sum3}" "${sum4}" "${sum5}"
done
@@ -48,6 +60,8 @@ main() {
}
usage() {
+ # `{0##*/}` means the first argument (i.e the script's name) with
+ # its path stripped, if it exists.
echo "usage: ${0##*/} perms days" 1>&2
exit 1
}
@@ -58,7 +72,11 @@ err() {
}
isnumber() {
- test ! -z "$(echo "${1}" | grep "^[0-9]\+$")" || err "'${1}' is not a valid number"
+ # If the argument does not begin *and* end in a number, then
+ # it's not a number. This also excludes negative numbers, but
+ # we shouldn't accept negative numbers anyway.
+ test ! -z "$(echo "${1}" | grep "^[0-9]\+$")"
}
+# Pass all command line arguments to `main`.
main "$@"
diff --git a/sh-os1/ex2/teldb b/sh-os1/ex2/teldb
@@ -1,8 +1,12 @@
#!/bin/sh
+# ΧΡΗΣΤΟΣ ΜΑΡΓΙΩΛΗΣ - [REDACTED]
+
+# We'll use this later.
emptylnregex="^\ *$"
main() {
+ # Parse command line options, exit if no option was passed.
case ${1} in
-a) newentry ;;
-l) list ;;
@@ -15,6 +19,10 @@ main() {
}
usage() {
+ # `{0##*/}` means the first argument (i.e the script's name) with
+ # its path stripped, if it exists.
+ # The {} around the options denote that only one option has to be
+ # passed to the script.
echo "usage: ${0##*/} {-a | -l | -n | -c keyword | -d keyword {-b | -r} | -s col}" 1>&2
echo "" 1>&2
echo "options:" 1>&2
@@ -30,15 +38,21 @@ usage() {
exit 1
}
+# Exit with a `usage` message if the string that was passed is empty.
+# This is used to make sure that we'll not write empty fields in `catalog`.
errempty() {
test ! -z "${1}" || usage
}
+# Skip empty lines
skipempty() {
grep -v "${emptylnregex}"
}
+# -a option - Make a new entry for `catalog`.
newentry() {
+ # Read fields and include an error check so that
+ # we don't pass an empty field.
read -erp "First name: " fname
errempty "${fname}"
@@ -51,38 +65,50 @@ newentry() {
read -erp "Phone number: " phone
errempty "${phone}"
+ # Append all the things we've read to `catalog`.
printf "%s %s %s %s\n" "${fname}" "${lname}" "${town}" "${phone}" >> catalog
}
+# -l option - Print `catalog` with each line numbered. It also skips empty lines.
list() {
nl catalog | skipempty
}
+# -s option - Sort `catalog` by column. We pass the desired column in `main`.
sortcol() {
errempty "${1}"
sort -k "${1}" catalog | skipempty
}
+# -c option - Print only the lines that match a specific keyword.
filter() {
errempty "${1}"
grep "${1}" catalog
}
+# -d option - Delete a line.
deleteln() {
errempty "${1}"
errempty "${2}"
+ # Parse sub-options.
case ${2} in
+ # Replace line with an empty line.
-b) sed -i "s/.*${1}.*//" catalog ;;
+ # Just delete the line.
-r) sed -i "/${1}/d" catalog ;;
esac
}
+# -n option - Count empty lines and delete them if the user answers with `y`.
emptyln() {
printf "Empty lines in 'catalog': "
grep "${emptylnregex}" catalog | wc -l
read -erp "Delete them (y/n)? " del
+ # Delete lines using `deleteln` with `-r` option so that it
+ # just deletes the line, without replacing it with an empty one.
test "${del}" = "y" && deleteln "${emptylnregex}" "-r"
}
+# Pass all command line arguments to `main`.
main "$@"