feat(userspace): shell runs programs (ls, cat) via spawn w/ terminal+storage+args page; ext4 holes read as zeros; storage re-attach same buffer
This commit is contained in:
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -12,7 +12,6 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"domain",
|
"domain",
|
||||||
"elf",
|
"elf",
|
||||||
"hw-virtio",
|
|
||||||
"kernel",
|
"kernel",
|
||||||
"platform",
|
"platform",
|
||||||
"types",
|
"types",
|
||||||
@@ -42,6 +41,14 @@ version = "0.1.0"
|
|||||||
name = "camera-server"
|
name = "camera-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cat"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"libkos",
|
||||||
|
"types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clipboard-server"
|
name = "clipboard-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -224,6 +231,14 @@ dependencies = [
|
|||||||
"types",
|
"types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ls"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"libkos",
|
||||||
|
"types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "media-index-server"
|
name = "media-index-server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ members = [
|
|||||||
"crates/adapter-riscv",
|
"crates/adapter-riscv",
|
||||||
"crates/apps/hello",
|
"crates/apps/hello",
|
||||||
"crates/apps/shell",
|
"crates/apps/shell",
|
||||||
|
"crates/apps/cat",
|
||||||
|
"crates/apps/ls",
|
||||||
"crates/apps/echo",
|
"crates/apps/echo",
|
||||||
"crates/apps/smptest",
|
"crates/apps/smptest",
|
||||||
"crates/apps/spawntest",
|
"crates/apps/spawntest",
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -55,7 +55,7 @@ build:
|
|||||||
-p init -p echo -p snake \
|
-p init -p echo -p snake \
|
||||||
-p display-qemu -p input-qemu -p storage-qemu \
|
-p display-qemu -p input-qemu -p storage-qemu \
|
||||||
-p log-server -p permission-manager -p session-server -p smptest -p spawntest -p hello \
|
-p log-server -p permission-manager -p session-server -p smptest -p spawntest -p hello \
|
||||||
-p terminal-server -p shell
|
-p terminal-server -p shell -p cat -p ls
|
||||||
cargo build --target $(RISCV_TARGET) --release -p adapter-riscv
|
cargo build --target $(RISCV_TARGET) --release -p adapter-riscv
|
||||||
|
|
||||||
image: build
|
image: build
|
||||||
|
|||||||
13
crates/apps/cat/Cargo.toml
Normal file
13
crates/apps/cat/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "cat"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "cat"
|
||||||
|
test = false
|
||||||
|
bench = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
types = { workspace = true }
|
||||||
|
libkos = { workspace = true }
|
||||||
45
crates/apps/cat/src/main.rs
Normal file
45
crates/apps/cat/src/main.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
//! Prints files: `cat <file>...`.
|
||||||
|
|
||||||
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
|
||||||
|
extern crate libkos;
|
||||||
|
|
||||||
|
use libkos::fs::{FsError, Storage};
|
||||||
|
use libkos::println;
|
||||||
|
|
||||||
|
/// Where storage's buffer is mapped.
|
||||||
|
const STORAGE_BUF_VA: usize = 0x0180_0000;
|
||||||
|
|
||||||
|
fn cat(fs: &mut Storage, path: &str) -> Result<(), FsError> {
|
||||||
|
let h = fs.open(path.as_bytes())?;
|
||||||
|
let mut buf = [0u8; 4096];
|
||||||
|
loop {
|
||||||
|
let n = fs.read(h, &mut buf)?;
|
||||||
|
libkos::io::write_bytes(&buf[..n]);
|
||||||
|
if n < buf.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.close(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
extern "C" fn main() {
|
||||||
|
let Some(mut fs) = libkos::start::storage().and_then(|ep| Storage::attach(ep, STORAGE_BUF_VA).ok()) else {
|
||||||
|
println!("cat: no storage");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut any = false;
|
||||||
|
for path in libkos::start::args().skip(1) {
|
||||||
|
any = true;
|
||||||
|
match cat(&mut fs, path) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(FsError::NotFound) => println!("cat: {}: no such file", path),
|
||||||
|
Err(_) => println!("cat: {}: cannot read", path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !any {
|
||||||
|
println!("usage: cat <file>...");
|
||||||
|
}
|
||||||
|
}
|
||||||
13
crates/apps/ls/Cargo.toml
Normal file
13
crates/apps/ls/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "ls"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "ls"
|
||||||
|
test = false
|
||||||
|
bench = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
types = { workspace = true }
|
||||||
|
libkos = { workspace = true }
|
||||||
35
crates/apps/ls/src/main.rs
Normal file
35
crates/apps/ls/src/main.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
//! Lists a directory: `ls [dir]`, the root by default.
|
||||||
|
|
||||||
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
|
||||||
|
extern crate libkos;
|
||||||
|
|
||||||
|
use libkos::fs::Storage;
|
||||||
|
use libkos::println;
|
||||||
|
|
||||||
|
/// Where storage's buffer is mapped.
|
||||||
|
const STORAGE_BUF_VA: usize = 0x0180_0000;
|
||||||
|
|
||||||
|
#[unsafe(no_mangle)]
|
||||||
|
extern "C" fn main() {
|
||||||
|
let Some(mut fs) = libkos::start::storage().and_then(|ep| Storage::attach(ep, STORAGE_BUF_VA).ok()) else {
|
||||||
|
println!("ls: no storage");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let dir = libkos::start::args().nth(1).unwrap_or("/");
|
||||||
|
let listed = fs.list_dir(dir.as_bytes(), |e| {
|
||||||
|
let name = core::str::from_utf8(e.name()).unwrap_or("?");
|
||||||
|
if name == "." || name == ".." {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if e.is_dir {
|
||||||
|
println!("{}/", name);
|
||||||
|
} else {
|
||||||
|
println!("{:<24} {:>8}", name, e.size);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if listed.is_err() {
|
||||||
|
println!("ls: {}: no such directory", dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,33 +1,95 @@
|
|||||||
//! The shell, for now built-ins only; running programs comes next.
|
//! The shell: a few built-ins, and anything else is a program on disk.
|
||||||
|
//! `name args...` runs `name.elf` through init, handing it the terminal as
|
||||||
|
//! its output and the storage server, and nothing else; then waits for it.
|
||||||
|
|
||||||
#![no_std]
|
#![no_std]
|
||||||
#![no_main]
|
#![no_main]
|
||||||
|
|
||||||
extern crate libkos;
|
extern crate libkos;
|
||||||
|
|
||||||
use libkos::{print, println};
|
use libkos::spawn::{SpawnError, spawn};
|
||||||
|
use libkos::{print, println, syscall};
|
||||||
use types::bounded::CapSlot;
|
use types::bounded::CapSlot;
|
||||||
|
use types::budget::Limits;
|
||||||
|
use types::permission::{Permission, PermissionSet};
|
||||||
|
use types::start_arg::StartArg;
|
||||||
|
|
||||||
/// Init's manifest gives it the terminal first.
|
/// Where init's manifest put them: `uses`, then the spawn endpoint.
|
||||||
const TERMINAL: CapSlot = CapSlot::const_new(0);
|
const TERMINAL: CapSlot = CapSlot::const_new(0);
|
||||||
|
const STORAGE: CapSlot = CapSlot::const_new(1);
|
||||||
|
const INIT: CapSlot = CapSlot::const_new(2);
|
||||||
|
|
||||||
fn run(line: &str) {
|
/// What each program gets, out of the shell's allowance.
|
||||||
let mut words = line.split_ascii_whitespace();
|
const PROGRAM: Limits = Limits { pages: 128, endpoints: 1, notifications: 1, regions: 1 };
|
||||||
match words.next() {
|
const MAX_ARGS: usize = 16;
|
||||||
|
|
||||||
|
/// Runs `args[0].elf` with `args` and waits for it to finish.
|
||||||
|
fn run_program(args: &[&str], done: CapSlot) {
|
||||||
|
let mut file = [0u8; types::spawn::MAX_NAME];
|
||||||
|
let name = args[0].as_bytes();
|
||||||
|
let Some(file) = file.get_mut(..name.len() + 4).map(|f| {
|
||||||
|
f[..name.len()].copy_from_slice(name);
|
||||||
|
f[name.len()..].copy_from_slice(b".elf");
|
||||||
|
&*f
|
||||||
|
}) else {
|
||||||
|
println!("shell: {}: name too long", args[0]);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let child = match spawn(INIT, file, PROGRAM) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(SpawnError::NotFound) => return println!("shell: {}: not found", args[0]),
|
||||||
|
Err(SpawnError::OverAllowance) => return println!("shell: no room to run {}", args[0]),
|
||||||
|
Err(SpawnError::Failed) => return println!("shell: {}: cannot run", args[0]),
|
||||||
|
};
|
||||||
|
let call = PermissionSet::empty().with(Permission::Execute);
|
||||||
|
let arg = StartArg {
|
||||||
|
log: child.give(TERMINAL, call),
|
||||||
|
storage: child.give(STORAGE, call),
|
||||||
|
args: child.set_args(args),
|
||||||
|
console: None,
|
||||||
|
};
|
||||||
|
let watched = syscall::watch_death(child.process, done).is_ok();
|
||||||
|
if child.start_with(arg) && watched {
|
||||||
|
let _ = syscall::wait(done);
|
||||||
|
} else {
|
||||||
|
let _ = syscall::kill(child.process);
|
||||||
|
println!("shell: {}: cannot start", args[0]);
|
||||||
|
}
|
||||||
|
syscall::drop_cap(child.process);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(line: &str, done: CapSlot) {
|
||||||
|
let mut args = [""; MAX_ARGS];
|
||||||
|
let mut n = 0;
|
||||||
|
for w in line.split_ascii_whitespace().take(MAX_ARGS) {
|
||||||
|
args[n] = w;
|
||||||
|
n += 1;
|
||||||
|
}
|
||||||
|
let args = &args[..n];
|
||||||
|
match args.first().copied() {
|
||||||
None => {}
|
None => {}
|
||||||
Some("help") => {
|
Some("help") => {
|
||||||
println!("built-ins: help, echo <text>, clear");
|
println!("built-ins: help, echo <text>, clear");
|
||||||
|
println!("anything else runs a program from the disk: try ls, cat readme.txt");
|
||||||
}
|
}
|
||||||
Some("echo") => {
|
Some("echo") => println!("{}", Joined(&args[1..])),
|
||||||
let mut first = true;
|
|
||||||
for w in words {
|
|
||||||
print!("{}{}", if first { "" } else { " " }, w);
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
Some("clear") => libkos::terminal::clear(TERMINAL),
|
Some("clear") => libkos::terminal::clear(TERMINAL),
|
||||||
Some(other) => println!("shell: {}: not found", other),
|
Some(_) => run_program(args, done),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Words with single spaces between them.
|
||||||
|
struct Joined<'a>(&'a [&'a str]);
|
||||||
|
|
||||||
|
impl core::fmt::Display for Joined<'_> {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||||
|
for (i, w) in self.0.iter().enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
f.write_str(" ")?;
|
||||||
|
}
|
||||||
|
f.write_str(w)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,13 +97,17 @@ fn run(line: &str) {
|
|||||||
extern "C" fn main() {
|
extern "C" fn main() {
|
||||||
// Everything this shell prints goes to the terminal.
|
// Everything this shell prints goes to the terminal.
|
||||||
libkos::start::set_log(Some(TERMINAL));
|
libkos::start::set_log(Some(TERMINAL));
|
||||||
|
let Ok(done) = syscall::create_notification() else {
|
||||||
|
println!("shell: no notification to wait on");
|
||||||
|
return;
|
||||||
|
};
|
||||||
println!("k-os shell. Type 'help'.");
|
println!("k-os shell. Type 'help'.");
|
||||||
let mut buf = [0u8; 256];
|
let mut buf = [0u8; 256];
|
||||||
loop {
|
loop {
|
||||||
print!("$ ");
|
print!("$ ");
|
||||||
let n = libkos::terminal::read_line(TERMINAL, &mut buf);
|
let n = libkos::terminal::read_line(TERMINAL, &mut buf);
|
||||||
match core::str::from_utf8(&buf[..n]) {
|
match core::str::from_utf8(&buf[..n]) {
|
||||||
Ok(line) => run(line.trim_end_matches('\n')),
|
Ok(line) => run(line.trim_end_matches('\n'), done),
|
||||||
Err(_) => println!("shell: not text"),
|
Err(_) => println!("shell: not text"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,6 +153,6 @@ fn hand_over_console(log: CapSlot) {
|
|||||||
pub(crate) fn start(program: &LoadedProcess, console: Option<CapSlot>) {
|
pub(crate) fn start(program: &LoadedProcess, console: Option<CapSlot>) {
|
||||||
let log = libkos::start::log()
|
let log = libkos::start::log()
|
||||||
.and_then(|l| syscall::delegate(l, program.process_cap, perms(&[Permission::Execute])));
|
.and_then(|l| syscall::delegate(l, program.process_cap, perms(&[Permission::Execute])));
|
||||||
let arg = types::start_arg::encode(console, log);
|
let arg = types::start_arg::StartArg { console, log, ..Default::default() }.encode();
|
||||||
syscall::spawn_thread_in(program.process_cap, program.entry_point, USER_STACK_TOP, arg);
|
syscall::spawn_thread_in(program.process_cap, program.entry_point, USER_STACK_TOP, arg);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ pub const TESTS: &[Program] = &[Program {
|
|||||||
/// The desktop; the test image runs the SMP self-test instead.
|
/// The desktop; the test image runs the SMP self-test instead.
|
||||||
pub const APPS: &[Program] = &[Program {
|
pub const APPS: &[Program] = &[Program {
|
||||||
serves: false,
|
serves: false,
|
||||||
uses: &["terminal"],
|
uses: &["terminal", "storage"],
|
||||||
|
// Room for one program at a time, the way the shell runs them.
|
||||||
|
spawns: Some(Limits { pages: 256, endpoints: 2, notifications: 2, regions: 2 }),
|
||||||
..server("shell", b"shell.elf", SMALL)
|
..server("shell", b"shell.elf", SMALL)
|
||||||
}];
|
}];
|
||||||
|
|||||||
@@ -4,21 +4,26 @@ struct Writer;
|
|||||||
|
|
||||||
impl fmt::Write for Writer {
|
impl fmt::Write for Writer {
|
||||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||||
// The log server first; a process without either prints nothing.
|
write_bytes(s.as_bytes());
|
||||||
// A call, not a send: the text is out before the caller goes on.
|
|
||||||
if let Some(log) = crate::start::log() {
|
|
||||||
for chunk in s.as_bytes().chunks(types::log::TEXT_BYTES) {
|
|
||||||
let _ = crate::syscall::call(log, types::log::text_words(chunk), None);
|
|
||||||
}
|
|
||||||
} else if let Some(console) = crate::start::console() {
|
|
||||||
for &b in s.as_bytes() {
|
|
||||||
crate::syscall::debug_putchar(console, b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bytes as they are to where `print!` goes: the log endpoint (the log
|
||||||
|
/// server, or a terminal), else the console; nowhere without either.
|
||||||
|
pub fn write_bytes(bytes: &[u8]) {
|
||||||
|
// A call, not a send: the text is out before the caller goes on.
|
||||||
|
if let Some(log) = crate::start::log() {
|
||||||
|
for chunk in bytes.chunks(types::log::TEXT_BYTES) {
|
||||||
|
let _ = crate::syscall::call(log, types::log::text_words(chunk), None);
|
||||||
|
}
|
||||||
|
} else if let Some(console) = crate::start::console() {
|
||||||
|
for &b in bytes {
|
||||||
|
crate::syscall::debug_putchar(console, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn _print(args: fmt::Arguments) {
|
pub fn _print(args: fmt::Arguments) {
|
||||||
use fmt::Write;
|
use fmt::Write;
|
||||||
Writer.write_fmt(args).ok();
|
Writer.write_fmt(args).ok();
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ use crate::syscall;
|
|||||||
use types::address::{EntryPoint, StackPointer};
|
use types::address::{EntryPoint, StackPointer};
|
||||||
use types::bounded::CapSlot;
|
use types::bounded::CapSlot;
|
||||||
use types::budget::Limits;
|
use types::budget::Limits;
|
||||||
|
use types::permission::{Permission, PermissionSet};
|
||||||
use types::spawn::{STACK_TOP, reply, request};
|
use types::spawn::{STACK_TOP, reply, request};
|
||||||
|
use types::start_arg::{ARGS_SIZE, ARGS_VA, StartArg};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum SpawnError {
|
pub enum SpawnError {
|
||||||
@@ -44,10 +46,46 @@ pub fn spawn(init: CapSlot, name: &[u8], limits: Limits) -> Result<Spawned, Spaw
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Spawned {
|
impl Spawned {
|
||||||
/// Runs the program, telling it where its log endpoint is.
|
/// The log endpoint init gave the program.
|
||||||
pub fn start(&self) -> bool {
|
pub fn log(&self) -> Option<CapSlot> {
|
||||||
let arg = types::start_arg::encode(None, self.log);
|
self.log
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A copy of `cap` for the program, with at most `perms` (and never
|
||||||
|
/// Delegate: it is the program's, not to pass on); its slot there.
|
||||||
|
pub fn give(&self, cap: CapSlot, perms: PermissionSet) -> Option<CapSlot> {
|
||||||
|
syscall::delegate(cap, self.process, perms.without(Permission::Delegate))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Puts `args` (its name first) where the program finds them: a page
|
||||||
|
/// this process fills and then moves into the program, which owns it.
|
||||||
|
pub fn set_args(&self, args: &[&str]) -> bool {
|
||||||
|
let need: usize = args.iter().map(|a| a.len() + 1).sum::<usize>() + 1;
|
||||||
|
if need > ARGS_SIZE || args.iter().any(|a| a.contains('\0')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let Some(page) = types::bounded::PageCount::new(1).and_then(syscall::request_pages) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
// SAFETY: the page was just mapped read-write here, ARGS_SIZE bytes.
|
||||||
|
let buf = unsafe { core::slice::from_raw_parts_mut(page.raw() as *mut u8, ARGS_SIZE) };
|
||||||
|
buf.fill(0);
|
||||||
|
let mut at = 0;
|
||||||
|
for a in args {
|
||||||
|
buf[at..at + a.len()].copy_from_slice(a.as_bytes());
|
||||||
|
at += a.len() + 1;
|
||||||
|
}
|
||||||
|
syscall::map_page(self.process, page, types::address::VirtualAddress::from_raw(ARGS_VA), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the program with `arg` in its `a0` (see `types::start_arg`).
|
||||||
|
pub fn start_with(&self, arg: StartArg) -> bool {
|
||||||
let stack = StackPointer::from_raw(STACK_TOP);
|
let stack = StackPointer::from_raw(STACK_TOP);
|
||||||
syscall::spawn_thread_in(self.process, self.entry, stack, arg) != usize::MAX
|
syscall::spawn_thread_in(self.process, self.entry, stack, arg.encode()) != usize::MAX
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the program, printing to the log init gave it.
|
||||||
|
pub fn start(&self) -> bool {
|
||||||
|
self.start_with(StartArg { log: self.log, ..StartArg::default() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use core::sync::atomic::{AtomicUsize, Ordering};
|
use core::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use types::bounded::CapSlot;
|
use types::bounded::CapSlot;
|
||||||
use types::start_arg;
|
use types::start_arg::{ARGS_SIZE, ARGS_VA, StartArg};
|
||||||
|
|
||||||
/// Set by `_start` from `a0` before `main` runs.
|
/// Set by `_start` from `a0` before `main` runs.
|
||||||
#[unsafe(no_mangle)]
|
#[unsafe(no_mangle)]
|
||||||
@@ -20,7 +20,7 @@ fn arg() -> usize {
|
|||||||
|
|
||||||
/// The console capability this process prints through, if any.
|
/// The console capability this process prints through, if any.
|
||||||
pub fn console() -> Option<CapSlot> {
|
pub fn console() -> Option<CapSlot> {
|
||||||
read(&CONSOLE, start_arg::console)
|
read(&CONSOLE, |a| StartArg::decode(a).console)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For a process that gets its console some other way (init, from BootInfo).
|
/// For a process that gets its console some other way (init, from BootInfo).
|
||||||
@@ -30,7 +30,25 @@ pub fn set_console(slot: Option<CapSlot>) {
|
|||||||
|
|
||||||
/// The log server endpoint this process prints to, if any.
|
/// The log server endpoint this process prints to, if any.
|
||||||
pub fn log() -> Option<CapSlot> {
|
pub fn log() -> Option<CapSlot> {
|
||||||
read(&LOG, start_arg::log)
|
read(&LOG, |a| StartArg::decode(a).log)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The storage server endpoint this process was given, if any.
|
||||||
|
pub fn storage() -> Option<CapSlot> {
|
||||||
|
StartArg::decode(arg()).storage
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This program's arguments, its name first; none without an args page.
|
||||||
|
pub fn args() -> impl Iterator<Item = &'static str> {
|
||||||
|
let page: &'static [u8] = if StartArg::decode(arg()).args {
|
||||||
|
// SAFETY: the args flag says the parent mapped this page here
|
||||||
|
// before starting the program; nothing writes it afterwards.
|
||||||
|
unsafe { core::slice::from_raw_parts(ARGS_VA as *const u8, ARGS_SIZE) }
|
||||||
|
} else {
|
||||||
|
&[]
|
||||||
|
};
|
||||||
|
let end = page.windows(2).position(|w| w == [0, 0]).map_or(0, |i| i + 1);
|
||||||
|
page[..end].split(|b| *b == 0).filter(|a| !a.is_empty()).filter_map(|a| core::str::from_utf8(a).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For a process given its log endpoint after it started.
|
/// For a process given its log endpoint after it started.
|
||||||
|
|||||||
@@ -106,9 +106,15 @@ impl<B: BlockDevice> FileSystem for Ext4<B> {
|
|||||||
let mut block_buf = [0u8; 4096];
|
let mut block_buf = [0u8; 4096];
|
||||||
while done < to_read {
|
while done < to_read {
|
||||||
let offset = file.offset + done;
|
let offset = file.offset + done;
|
||||||
let phys_block = self.map_logical_block(&inode, (offset / bs) as u32)?;
|
// A block no extent maps is a hole (mkfs leaves zero blocks
|
||||||
if !read_block(&mut self.device, phys_block, self.sb.sectors_per_block(), &mut block_buf) {
|
// out): it reads as zeros.
|
||||||
return None;
|
match self.map_logical_block(&inode, (offset / bs) as u32) {
|
||||||
|
Some(phys_block) => {
|
||||||
|
if !read_block(&mut self.device, phys_block, self.sb.sectors_per_block(), &mut block_buf) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => block_buf.fill(0),
|
||||||
}
|
}
|
||||||
let byte_in_block = offset % bs;
|
let byte_in_block = offset % bs;
|
||||||
let chunk = (to_read - done).min(bs - byte_in_block);
|
let chunk = (to_read - done).min(bs - byte_in_block);
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ pub trait Buffers {
|
|||||||
struct Client {
|
struct Client {
|
||||||
badge: usize,
|
badge: usize,
|
||||||
buf: &'static mut [u8],
|
buf: &'static mut [u8],
|
||||||
|
/// What maps `buf`, sent again when the same client attaches again.
|
||||||
|
cap: Option<CapSlot>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct StorageServer<F: FileSystem, B: Buffers> {
|
pub struct StorageServer<F: FileSystem, B: Buffers> {
|
||||||
@@ -77,9 +79,11 @@ impl<F: FileSystem, B: Buffers> StorageServer<F, B> {
|
|||||||
self.clients.iter_mut().flatten().find(|c| c.badge == badge)
|
self.clients.iter_mut().flatten().find(|c| c.badge == badge)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A client attaching again gets its buffer again: the programs a shell
|
||||||
|
/// runs one after another share its badge.
|
||||||
fn attach(&mut self, badge: usize) -> (StorageResponse, Option<CapSlot>) {
|
fn attach(&mut self, badge: usize) -> (StorageResponse, Option<CapSlot>) {
|
||||||
if self.client(badge).is_some() {
|
if let Some(c) = self.client(badge) {
|
||||||
return (StorageResponse::Error, None);
|
return (StorageResponse::Attached { size: c.buf.len() }, c.cap);
|
||||||
}
|
}
|
||||||
let Some(index) = self.clients.iter().position(Option::is_none) else {
|
let Some(index) = self.clients.iter().position(Option::is_none) else {
|
||||||
return (StorageResponse::Error, None);
|
return (StorageResponse::Error, None);
|
||||||
@@ -89,7 +93,7 @@ impl<F: FileSystem, B: Buffers> StorageServer<F, B> {
|
|||||||
};
|
};
|
||||||
let size = buf.len();
|
let size = buf.len();
|
||||||
if let Some(slot) = self.clients.get_mut(index) {
|
if let Some(slot) = self.clients.get_mut(index) {
|
||||||
*slot = Some(Client { badge, buf });
|
*slot = Some(Client { badge, buf, cap });
|
||||||
}
|
}
|
||||||
(StorageResponse::Attached { size }, cap)
|
(StorageResponse::Attached { size }, cap)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,3 +170,16 @@ fn one_read_fills_the_buffer_across_blocks() {
|
|||||||
assert_eq!(fs.read(h, &mut buf), Some(total - 10_000));
|
assert_eq!(fs.read(h, &mut buf), Some(total - 10_000));
|
||||||
assert_eq!(fs.read(h, &mut buf), Some(0));
|
assert_eq!(fs.read(h, &mut buf), Some(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn holes_read_as_zeros() {
|
||||||
|
let dir = scratch("sparse");
|
||||||
|
// Data, then a zero block mkfs stores as a hole, then data again.
|
||||||
|
let mut data = std::vec![b'x'; 4096];
|
||||||
|
data.extend(std::vec![0u8; 4096]);
|
||||||
|
data.extend(b"end");
|
||||||
|
std::fs::write(dir.join("root/sparse.bin"), &data).unwrap();
|
||||||
|
let Some(img) = mkfs(&dir, MKFS_FEATURES) else { return };
|
||||||
|
let mut fs = Ext4::mount(FileDisk(img)).expect("mount");
|
||||||
|
assert_eq!(read_all(&mut fs, b"sparse.bin"), data);
|
||||||
|
}
|
||||||
|
|||||||
@@ -67,10 +67,14 @@ fn nothing_works_before_attaching() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_client_attaches_once() {
|
fn attaching_again_hands_back_the_same_buffer() {
|
||||||
let mut s = server();
|
let mut s = server();
|
||||||
Client::attach(&mut s, 0);
|
let (first, cap) = call(&mut s, 0, StorageRequest::Attach);
|
||||||
assert_eq!(call(&mut s, 0, StorageRequest::Attach).0, StorageResponse::Error);
|
assert_eq!(call(&mut s, 0, StorageRequest::Attach), (first, cap));
|
||||||
|
// Not a new client: the other seven places are still free.
|
||||||
|
for badge in 1..8 {
|
||||||
|
assert!(matches!(call(&mut s, badge, StorageRequest::Attach).0, StorageResponse::Attached { .. }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,11 +1,27 @@
|
|||||||
//! The word a process's first thread gets in `a0`: the slots of the
|
//! The word a process's first thread gets in `a0`: the slots of the
|
||||||
//! capabilities any process may be handed at start (console, log endpoint).
|
//! capabilities any process may be handed at start, and whether it has an
|
||||||
//! Each field is slot + 1 in one byte; 0 means none.
|
//! arguments page. Each slot field is slot + 1 in one byte; 0 means none.
|
||||||
|
|
||||||
use crate::bounded::CapSlot;
|
use crate::bounded::CapSlot;
|
||||||
|
|
||||||
const CONSOLE_SHIFT: u32 = 0;
|
const CONSOLE_SHIFT: u32 = 0;
|
||||||
const LOG_SHIFT: u32 = 8;
|
const LOG_SHIFT: u32 = 8;
|
||||||
|
const STORAGE_SHIFT: u32 = 16;
|
||||||
|
const ARGS_BIT: usize = 1 << 24;
|
||||||
|
|
||||||
|
/// Where the arguments page sits: the program's arguments, NUL separated,
|
||||||
|
/// the first its name. Below the stack, which ends at the image.
|
||||||
|
pub const ARGS_VA: usize = 0x8000;
|
||||||
|
pub const ARGS_SIZE: usize = 4096;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub struct StartArg {
|
||||||
|
pub console: Option<CapSlot>,
|
||||||
|
/// Where `println!` goes: the log server, or a terminal.
|
||||||
|
pub log: Option<CapSlot>,
|
||||||
|
pub storage: Option<CapSlot>,
|
||||||
|
pub args: bool,
|
||||||
|
}
|
||||||
|
|
||||||
fn field(arg: usize, shift: u32) -> Option<CapSlot> {
|
fn field(arg: usize, shift: u32) -> Option<CapSlot> {
|
||||||
let v = (arg >> shift) & 0xFF;
|
let v = (arg >> shift) & 0xFF;
|
||||||
@@ -16,16 +32,22 @@ fn put(slot: Option<CapSlot>, shift: u32) -> usize {
|
|||||||
slot.map_or(0, |s| (s.value() as usize + 1) << shift)
|
slot.map_or(0, |s| (s.value() as usize + 1) << shift)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn encode(console: Option<CapSlot>, log: Option<CapSlot>) -> usize {
|
impl StartArg {
|
||||||
put(console, CONSOLE_SHIFT) | put(log, LOG_SHIFT)
|
pub fn encode(&self) -> usize {
|
||||||
}
|
put(self.console, CONSOLE_SHIFT)
|
||||||
|
| put(self.log, LOG_SHIFT)
|
||||||
|
| put(self.storage, STORAGE_SHIFT)
|
||||||
|
| if self.args { ARGS_BIT } else { 0 }
|
||||||
|
}
|
||||||
|
|
||||||
pub fn console(arg: usize) -> Option<CapSlot> {
|
pub fn decode(arg: usize) -> Self {
|
||||||
field(arg, CONSOLE_SHIFT)
|
Self {
|
||||||
}
|
console: field(arg, CONSOLE_SHIFT),
|
||||||
|
log: field(arg, LOG_SHIFT),
|
||||||
pub fn log(arg: usize) -> Option<CapSlot> {
|
storage: field(arg, STORAGE_SHIFT),
|
||||||
field(arg, LOG_SHIFT)
|
args: arg & ARGS_BIT != 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -33,11 +55,14 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn slots_round_trip_and_zero_is_none() {
|
fn every_field_round_trips_and_zero_is_none() {
|
||||||
let (c, l) = (CapSlot::new(0), CapSlot::new(63));
|
let a = StartArg {
|
||||||
let arg = encode(c, l);
|
console: CapSlot::new(0),
|
||||||
assert_eq!((console(arg), log(arg)), (c, l));
|
log: CapSlot::new(63),
|
||||||
assert_eq!((console(0), log(0)), (None, None));
|
storage: CapSlot::new(5),
|
||||||
assert_eq!(log(encode(c, None)), None);
|
args: true,
|
||||||
|
};
|
||||||
|
assert_eq!(StartArg::decode(a.encode()), a);
|
||||||
|
assert_eq!(StartArg::decode(0), StartArg::default());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
disk/docs/shell.txt
Normal file
7
disk/docs/shell.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
The shell runs a program by asking init to load it (init alone may
|
||||||
|
create processes), then gives the program the terminal as its output
|
||||||
|
and the storage server, writes its arguments onto a page it moves
|
||||||
|
into the program, starts it and waits for it to die.
|
||||||
|
|
||||||
|
Built-ins: help, echo, clear. Anything else is a program on disk:
|
||||||
|
`name args...` runs name.elf.
|
||||||
7
disk/readme.txt
Normal file
7
disk/readme.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Welcome to k-os.
|
||||||
|
|
||||||
|
Every program here runs with only the capabilities it was given:
|
||||||
|
this file reached your screen because the shell handed `cat` the
|
||||||
|
terminal and the storage server, and nothing else.
|
||||||
|
|
||||||
|
Try: ls ls docs cat docs/shell.txt
|
||||||
12
docs/ipc.md
12
docs/ipc.md
@@ -74,7 +74,10 @@ calling it (`types::log`) if init gave it a log endpoint.
|
|||||||
|
|
||||||
A thread made by `SpawnThread`/`SpawnThreadIn` starts with the caller's
|
A thread made by `SpawnThread`/`SpawnThreadIn` starts with the caller's
|
||||||
`arg` in `a0`. A process's first thread reads it as `types::start_arg`:
|
`arg` in `a0`. A process's first thread reads it as `types::start_arg`:
|
||||||
the slots of the console and log capabilities it was handed.
|
the slots of the console, log and storage capabilities it was handed, and
|
||||||
|
whether its parent put an arguments page at `ARGS_VA` (NUL-separated, the
|
||||||
|
program's name first; the parent fills a page and `MapPage`s it over, so
|
||||||
|
the program owns it and it goes when the program does).
|
||||||
|
|
||||||
Boot loads only init and storage, both packed into the kernel image (they
|
Boot loads only init and storage, both packed into the kernel image (they
|
||||||
are what reads the disk, so they cannot come from it). The disk is one ext4
|
are what reads the disk, so they cannot come from it). The disk is one ext4
|
||||||
@@ -102,7 +105,12 @@ font. Its output side is the log protocol (`types::log`), so a program whose
|
|||||||
log endpoint is the terminal prints there with `println!`: standard output
|
log endpoint is the terminal prints there with `println!`: standard output
|
||||||
is whatever text endpoint a program was given. `READ` (`types::terminal`)
|
is whatever text endpoint a program was given. `READ` (`types::terminal`)
|
||||||
blocks until a line is typed and returns it in chunks, the last ending in
|
blocks until a line is typed and returns it in chunks, the last ending in
|
||||||
`\n`. The desktop is the shell, given the terminal.
|
`\n`. The desktop is the shell, given the terminal, storage and a spawn
|
||||||
|
allowance. A command that is not a built-in runs `name.elf`: the shell
|
||||||
|
spawns it, gives it call-only copies of the terminal (as its log, so its
|
||||||
|
output) and of storage, writes its arguments page, starts it and waits for
|
||||||
|
its death. The programs share the shell's storage badge; storage hands a
|
||||||
|
client that attaches again the same buffer, so they run one at a time.
|
||||||
|
|
||||||
Only init holds `ProcessCreate`. Everyone else runs programs through init's
|
Only init holds `ProcessCreate`. Everyone else runs programs through init's
|
||||||
spawn service (`types::spawn`, client `libkos::spawn`), which init serves
|
spawn service (`types::spawn`, client `libkos::spawn`), which init serves
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ copy permission-manager "permis.elf"
|
|||||||
copy session-server "session.elf"
|
copy session-server "session.elf"
|
||||||
copy terminal-server "terminal.elf"
|
copy terminal-server "terminal.elf"
|
||||||
copy shell "shell.elf"
|
copy shell "shell.elf"
|
||||||
|
copy cat "cat.elf"
|
||||||
|
copy ls "ls.elf"
|
||||||
|
# Files that ship on the disk.
|
||||||
|
cp -r disk/. "${ROOT}/"
|
||||||
# Only the test image: its presence makes init run the self-test.
|
# Only the test image: its presence makes init run the self-test.
|
||||||
if [ -n "${SMPTEST:-}" ]; then
|
if [ -n "${SMPTEST:-}" ]; then
|
||||||
copy smptest "smptest.elf"
|
copy smptest "smptest.elf"
|
||||||
|
|||||||
Reference in New Issue
Block a user