From cbadacbb881200b601c7b53b29aa0c1b78747692 Mon Sep 17 00:00:00 2001 From: Alexis211 Date: Tue, 23 Mar 2010 16:34:36 +0100 Subject: More work on IPC --- doc/directories.txt | 11 +++++ doc/objects-requests.txt | 11 +++++ doc/syscalls.txt | 4 ++ src/include/gc/syscall.h | 17 +++++++ src/kernel/Makefile | 2 +- src/kernel/ipc/object.c | 113 +++++++++++++++++++++++++++++++++++++++++++ src/kernel/ipc/object.h | 29 ++++++++--- src/kernel/ipc/request.c | 55 +++++++++++++++++++++ src/kernel/ipc/request.h | 20 +++++--- src/kernel/ipc/shm.c | 26 ++++++++++ src/kernel/ipc/shm.h | 7 ++- src/kernel/task/idt.c | 3 +- src/kernel/task/syscall.c | 11 ++++- src/kernel/task/task.c | 18 +++++-- src/kernel/task/task.h | 6 ++- src/library/Makefile | 4 +- src/library/gc/syscall.c | 47 ++++++++++++++++++ src/library/grapes/syscall.c | 35 -------------- src/library/grapes/syscall.h | 12 ----- src/library/start.c | 2 +- src/modules/test/Makefile | 2 +- src/modules/test/main.c | 8 +-- 22 files changed, 362 insertions(+), 81 deletions(-) create mode 100644 doc/directories.txt create mode 100644 doc/objects-requests.txt create mode 100644 src/include/gc/syscall.h create mode 100644 src/kernel/ipc/object.c create mode 100644 src/kernel/ipc/request.c create mode 100644 src/library/gc/syscall.c delete mode 100644 src/library/grapes/syscall.c delete mode 100644 src/library/grapes/syscall.h diff --git a/doc/directories.txt b/doc/directories.txt new file mode 100644 index 0000000..b0adf7b --- /dev/null +++ b/doc/directories.txt @@ -0,0 +1,11 @@ +This file explains all the directories of the source tree : + +doc documentation about the system +mnt temporary folder for mounting disk images +src source code +src/include all includable headers to be used in userland +src/include/gc Grapes Core (communication with the kernel) headers +src/include/gm Grapes Modules headers +src/kernel kernel source +src/library source and linkable objects to be used by userland applications +src/modules userland applications diff --git a/doc/objects-requests.txt b/doc/objects-requests.txt new file mode 100644 index 0000000..9169552 --- /dev/null +++ b/doc/objects-requests.txt @@ -0,0 +1,11 @@ +Requests are identified by a 32bit function number, composed as follows : + (8 bit) parameter and return type ; (24bit) function number + +the first 8 bits are : + 2bit answer type ; 2bit parameter a ; 2bit parameter b ; 2bit parameter c + +each two bit couple can be one of the following : +- 00 : void +- 01 : object descriptor number (will be copied with a new number to reciever) +- 10 : integer +- 11 : long long (replies), or shared memory offset (requests and messages) diff --git a/doc/syscalls.txt b/doc/syscalls.txt index baa537e..663618d 100644 --- a/doc/syscalls.txt +++ b/doc/syscalls.txt @@ -12,5 +12,9 @@ id=eax Name Parameters Description 5 thread_new ebx: entry point Creates a new thread ecx: data pointer 6 irq_wait ebx: irq number Waits for an IRQ (requires privilege PL_DRIVER) + 7 proc_priv none Returns current process privilege level + 8 shm_create ebx: offset Create a shared memory segment at offset (ret = errcode) + ecx: length + 9 shm_delete ebx: offset Delete a shared memory segment at offset (ret = errcode) If a processes wishes to exit with an error code, it HAS to use process_exit. thread_exit will do nothing. diff --git a/src/include/gc/syscall.h b/src/include/gc/syscall.h new file mode 100644 index 0000000..4001420 --- /dev/null +++ b/src/include/gc/syscall.h @@ -0,0 +1,17 @@ +#ifndef DEF_SYSCALL_H +#define DEF_SYSCALL_H + +typedef unsigned size_t; + +void thread_exit(); +void schedule(); +void thread_sleep(int time); +void process_exit(int retval); +void printk(char* str); +void thread_new(void (*entry)(void*), void *data); +void irq_wait(int number); +int proc_priv(); +int shm_create(size_t offset, size_t length); +int shm_delete(size_t offset); + +#endif diff --git a/src/kernel/Makefile b/src/kernel/Makefile index 3508bb9..5de25f8 100644 --- a/src/kernel/Makefile +++ b/src/kernel/Makefile @@ -14,7 +14,7 @@ OBJECTS = core/loader_.o core/kmain.o core/sys.o \ task/idt.o task/idt_.o task/task.o task/task_.o task/syscall.o \ lib/stdlib.o lib/bitset.o lib/mutex.o \ mem/mem.o mem/paging.o mem/gdt.o mem/heap.o mem/seg.o \ - ipc/shm.o \ + ipc/shm.o ipc/object.o \ linker/elf.o OUT = kernel.elf diff --git a/src/kernel/ipc/object.c b/src/kernel/ipc/object.c new file mode 100644 index 0000000..70c31b2 --- /dev/null +++ b/src/kernel/ipc/object.c @@ -0,0 +1,113 @@ +#include "object.h" +#include +#include + +struct object* obj_new(struct process* owner) { + struct object* ret = kmalloc(sizeof(struct object)); + ret->owner = owner; + ret->descriptors = 0; + ret->busyMutex = MUTEX_UNLOCKED; + ret->request = 0; + return ret; +} + +void obj_delete(struct object* obj) { + if (obj->descriptors > 0) return; + if (obj->busyMutex != MUTEX_UNLOCKED) return; + if (obj->request != 0) return; + kfree(obj); +} + +int obj_createP(struct process* p) { + return objdesc_add(p, obj_new(p)); +} + +void obj_closeP(struct process* p, int id) { + struct object* obj = objdesc_read(p, id); + if (obj == 0) return; + objdesc_rm(p, id); + if (obj->owner == p) { + if (obj->descriptors > 0) { + obj->owner = 0; // set object to be invalid + //if a request was being handled, give it a null response + } else { + obj_delete(obj); + } + } else { + if (obj->descriptors == 0 && obj->owner == 0) { + obj_delete(obj); + } + //future : send message becuz object was closed + } +} + +void obj_closeall(struct process* p) { + while (p->objects != 0) obj_closeP(p, p->objects->id); +} + +// DESCRIPTORS + +int objdesc_add(struct process* proc, struct object* obj) { + struct obj_descriptor *ret = kmalloc(sizeof(struct obj_descriptor)); + ret->obj = obj; + ret->id = proc->next_objdesc; + ret->next = proc->objects; + proc->objects = ret; + obj->descriptors++; + proc->next_objdesc++; + return ret->id; +} + +int objdesc_get(struct process* proc, struct object* obj) { + struct obj_descriptor *it = proc->objects; + while (it != 0) { + if (it->obj == obj) return it->id; + it = it->next; + } + return -1; +} + +struct object* objdesc_read(struct process* proc, int id) { + struct obj_descriptor *it = proc->objects; + while (it != 0) { + if (it->id == id) return it->obj; + it = it->next; + } + return 0; +} + +void objdesc_rm(struct process* proc, int id) { + struct obj_descriptor *e = proc->objects; + if (e != 0 && e->id == id) { + proc->objects = e->next; + e->obj->descriptors--; + kfree(e); + return; + } + while (e->next != 0) { + if (e->next->id == id) { + e->next = e->next->next; + e->next->obj->descriptors--; + kfree(e->next); + return; + } + e = e->next; + } +} + +// SYSCALLS + +int object_create() { + return obj_createP(current_thread->process); +} + +int object_owned(int id) { + struct object *obj = objdesc_read(current_thread->process, id); + if (obj == 0) return -1; + if (obj->owner == current_thread->process) return 1; + return 0; +} + +void object_close(int id) { + obj_closeP(current_thread->process, id); +} diff --git a/src/kernel/ipc/object.h b/src/kernel/ipc/object.h index ef90a66..9dd3a1e 100644 --- a/src/kernel/ipc/object.h +++ b/src/kernel/ipc/object.h @@ -3,24 +3,37 @@ #include -#define OS_INACTIVE 0 //No one doing anything on this -#define OS_LISTENING 1 //A thread is waiting for a request on this object -#define OS_REQUESTPENDING 2 //A request is waiting for a thread to handle it -#define OS_BUSY 3 //A thread is currently handling a request - struct object { - struct process *owner; + struct process *owner; //when 0, object is invalid and cannot handle requests int descriptors; - uint32_t busyMutex; + uint32_t busyMutex; //if busy, either a blocking request is being processed, or a sent message is waiting for being recieved struct request *request; }; struct obj_descriptor { struct object *obj; int id; - int owned; struct obj_descriptor *next; }; +//Objects +struct object* obj_new(struct process *owner); +void obj_delete(struct object* obj); + +int obj_createP(struct process* p); +void obj_closeP(struct process* p, int id); +void obj_closeall(struct process* p); + +//Object descriptors +int objdesc_add(struct process* proc, struct object* obj); // add a descriptor +int objdesc_get(struct process* proc, struct object* obj); // look in descriptors for the one corresponding to the object +struct object* objdesc_read(struct process* proc, int id); // get the object correspoinding to the id +void objdesc_rm(struct process* proc, int id); // remove descriptor for an object + +//Syscalls +int object_create(); +int object_owned(int id); //does current process own object ? 1=yes 0=no +void object_close(int id); //closes descriptor to specified object. if we are the owner, make all requests to object fail. + #endif diff --git a/src/kernel/ipc/request.c b/src/kernel/ipc/request.c new file mode 100644 index 0000000..51a0bbc --- /dev/null +++ b/src/kernel/ipc/request.c @@ -0,0 +1,55 @@ +#include "request.h" + +int request_get(int obj, uint32_t ptr, int wait) { + //check if we own the object, if not return -2 + //check if a request is pending. if request is being processed, return -3 + //if not (busymutex unlocked or request==0) && wait, then wait, else return -1 + //if request pending, write it to ptr + // if request is nonblocking and no shm is to be mapped, delete it and unlock objects busymutex + //return 0 +} + +int request_has(int obj) { + //check if we own the object, if not return -2 + //check if a request is pending. + // if being processed, return 2 + // if waiting for ack, return 1 + // if none (busymutex unlocked or request==0), return 0 +} + +void request_answer(int obj, uint32_t answer) { + //check if we own the object, if not return + //if no blocking request is being processed (including non-acknowledged waiting requests), return + //set blocking request to finished, and set its answer + //dereference request from object, unlock objects busymutex +} + +int request_mapShm(int obj, uint32_t pos, int number) { + //check if we own the object, if not return -2 + //if no request is being processes (including non-acknowledged waiting requests), return -3 + //check if the requests has shm in parameter [number], if not return -4 + //map shm to position + //if request is nonblocking and no more shm is to be mapped, delete request and free object busymutex + //return 0 +} + +static struct request *mkrequest(int obj, struct thread *requester, + uint32_t func, uint32_t a, uint32_t b, uint32_t c) { + // get object from descriptor id, if none return 0 + // waitlock object's busy mutex + // if object cannot answer (owner == 0) return 0 + // create request, fill it up, reference it from object + // return request +} + +int request(int obj, uint32_t func, uint32_t a, uint32_t b, uint32_t c, uint32_t answerptr) { + //call mkrequest with parameters (requester thread = current thread) + //if returned value is 0, return -1 + //sleep until request is handled + //write answer to *answerptr, delete request, return 0 +} + +int send_msg(int obj, uint32_t func, uint32_t a, uint32_t b, uint32_t c) { + //call mkrequest with parameters (requester thread = 0) + //if returned value is 0, return -1 else return 0 +} diff --git a/src/kernel/ipc/request.h b/src/kernel/ipc/request.h index bf90c68..4a35dc3 100644 --- a/src/kernel/ipc/request.h +++ b/src/kernel/ipc/request.h @@ -10,13 +10,12 @@ struct request { struct object *obj; - struct thread *requester; - uint32_t func, param1, param2, param3; - struct seg_map *shm_cli, *shm_srv; - int answerType; + struct thread *requester; //0 if nonblocking message + uint32_t func, params[3]; + struct seg_map *shm_srv[3], *shm_cli[3]; union { - int num; - struct object* obj; + int64_t ll; + uint32_t n; } answer; }; @@ -25,5 +24,14 @@ struct user_request { int hasShm; }; +//syscalls +int request_get(int obj, uint32_t ptr, int wait); +int request_has(int obj); +void request_answer(int obj, uint32_t answer); +int request_mapShm(int obj, uint32_t pos, int number); + +int request(int obj, uint32_t func, uint32_t a, uint32_t b, uint32_t c, uint32_t answerptr); +int send_msg(int obj, uint32_t func, uint32_t a, uint32_t b, uint32_t c); + #endif diff --git a/src/kernel/ipc/shm.c b/src/kernel/ipc/shm.c index 0a0d3ee..1cdacfb 100644 --- a/src/kernel/ipc/shm.c +++ b/src/kernel/ipc/shm.c @@ -1,6 +1,7 @@ #include "shm.h" #include #include +#include struct segment* shmseg_make(size_t len, struct process* owner) { struct shmseg *ss = kmalloc(sizeof(struct shmseg)); @@ -54,3 +55,28 @@ void shmseg_delete(struct segment *seg) { } kfree(ss->frames); } + +struct segment_map* shmseg_getByOff(struct process* pr, size_t offset) { + struct segment_map* m = pr->pagedir->mappedSegs; + while (m != 0) { + if (m->start == offset && m->seg->delete == shmseg_delete) return m; + m = m->next; + } + return 0; +} + +// **** **** SHM syscalls **** **** +int shm_create(size_t offset, size_t len) { + if (offset >= 0xE0000000) return -1; + if (len >= 0x10000000) return -1; + if (offset+len >= 0xE0000000) return -1; + seg_map(shmseg_make(len, current_thread->process), current_thread->process->pagedir, offset); + return 0; +} + +int shm_delete(size_t offset) { + struct segment_map *s = shmseg_getByOff(current_thread->process, offset); + if (s == 0) return -1; + seg_unmap(s); + return 0; +} diff --git a/src/kernel/ipc/shm.h b/src/kernel/ipc/shm.h index b409955..895a619 100644 --- a/src/kernel/ipc/shm.h +++ b/src/kernel/ipc/shm.h @@ -16,9 +16,12 @@ void shmseg_unmap(struct segment_map*); void shmseg_delete(struct segment *seg); int shmseg_handleFault(struct segment_map *map, size_t addr, int write); + //find a shared memory segment in current address space by its offset +struct segment_map* shmseg_getByOff(struct process* pr, size_t offset); + //Shared memory syscalls -void shm_create(size_t offset, size_t len); -void shm_delete(size_t offset); +int shm_create(size_t offset, size_t len); +int shm_delete(size_t offset); #endif diff --git a/src/kernel/task/idt.c b/src/kernel/task/idt.c index cff4ae9..0f3c2f2 100644 --- a/src/kernel/task/idt.c +++ b/src/kernel/task/idt.c @@ -198,7 +198,6 @@ void idt_handleIrq(int number, int_callback func) { void idt_waitIrq(int number) { if (number < 16 && number >= 0 && proc_priv() <= PL_DRIVER) { irq_wakeup[number] = current_thread; - current_thread->state = TS_WAKEWAIT; - tasking_schedule(); + thread_goInactive(); } } diff --git a/src/kernel/task/syscall.c b/src/kernel/task/syscall.c index 752864c..51c46f1 100644 --- a/src/kernel/task/syscall.c +++ b/src/kernel/task/syscall.c @@ -8,6 +8,7 @@ r->eax = name(r->ebx, r->ecx); } #define CALL0V(name, scname) static void scname(struct registers* r) { name(); } #define CALL1V(name, scname) static void scname(struct registers* r) { name(r->ebx); } +#define CALL2V(name, scname) static void scname(struct registers* r) { name(r->ebx, r->ecx); } CALL0V(thread_exit, thread_exit_sc); CALL0V(tasking_switch, schedule_sc); @@ -15,17 +16,23 @@ CALL1V(thread_sleep, thread_sleep_sc); CALL1V(process_exit, process_exit_sc); CALL1(monitor_write, printk_sc); CALL1V(idt_waitIrq, irq_wait_sc); +CALL0(proc_priv, proc_priv_sc); +CALL2(shm_create, shm_create_sc); +CALL1(shm_delete, shm_delete_sc); static void thread_new_sc(struct registers* r) { thread_new(current_thread->process, (thread_entry)r->ebx, (void*)r->ecx); } int_callback syscalls[] = { - thread_exit_sc, + thread_exit_sc, //0 schedule_sc, thread_sleep_sc, process_exit_sc, printk_sc, - thread_new_sc, + thread_new_sc, //5 irq_wait_sc, + proc_priv_sc, + shm_create_sc, + shm_delete_sc, 0 }; diff --git a/src/kernel/task/task.c b/src/kernel/task/task.c index 622eaa1..6959676 100644 --- a/src/kernel/task/task.c +++ b/src/kernel/task/task.c @@ -88,10 +88,6 @@ void tasking_switch() { : : "r"(current_thread->ebp), "r"(current_thread->esp), "r"(current_thread->eip)); } -void tasking_schedule() { - asm volatile("int $64" : : "a"(1)); -} - void tasking_updateKernelPagetable(uint32_t idx, struct page_table *table, uint32_t tablephysical) { if (idx < 896) return; struct process* it = processes; @@ -119,13 +115,19 @@ uint32_t tasking_handleException(struct registers *regs) { thread_exit_stackJmp(EX_TH_EXCEPTION); } PANIC("This should never have happened. Please report this."); + return 0; } void thread_sleep(uint32_t msecs) { if (current_thread == 0) return; current_thread->state = TS_SLEEPING; current_thread->timeWait = timer_time() + msecs; - tasking_schedule(); + tasking_switch(); +} + +void thread_goInactive() { + current_thread->state = TS_WAKEWAIT; + tasking_switch(); } int proc_priv() { @@ -271,6 +273,11 @@ struct process *process_new(struct process* parent, uint32_t uid, uint32_t privi p->pagedir = pagedir_new(); p->next = processes; p->stacksBottom = 0xDF000000; + + p->next_objdesc = 0; + p->objects = 0; + obj_createP(p); //create process' root object and add descriptor 0 to it + processes = p; return p; } @@ -296,6 +303,7 @@ static void process_delete(struct process *pr) { while (it->next->process == pr) thread_delete(it->next); it = it->next; } + obj_closeall(pr); pagedir_delete(pr->pagedir); if (processes == pr) { processes = pr->next; diff --git a/src/kernel/task/task.h b/src/kernel/task/task.h index dd5461e..b2a5434 100644 --- a/src/kernel/task/task.h +++ b/src/kernel/task/task.h @@ -3,6 +3,7 @@ #include #include +#include #include "idt.h" #define TS_RUNNING 0 @@ -29,6 +30,9 @@ struct process { struct page_directory *pagedir; size_t stacksBottom; + struct obj_descriptor *objects; + uint32_t next_objdesc; + struct process *next; //Forms a linked list }; @@ -48,11 +52,11 @@ extern struct thread *current_thread; void tasking_init(); void tasking_switch(); -void tasking_schedule(); void tasking_updateKernelPagetable(uint32_t idx, struct page_table *table, uint32_t tablePhysical); uint32_t tasking_handleException(struct registers *regs); void thread_sleep(uint32_t msecs); +void thread_goInactive(); //Blocks the current thread. another one must be there to wake it up at some point. int proc_priv(); //Returns current privilege level void thread_exit(); void process_exit(uint32_t retval); diff --git a/src/library/Makefile b/src/library/Makefile index c562dec..74eb525 100644 --- a/src/library/Makefile +++ b/src/library/Makefile @@ -1,13 +1,13 @@ .PHONY: clean, mrproper CC = gcc -CFLAGS = -nostdlib -nostartfiles -nodefaultlibs -fno-builtin -fno-stack-protector -Wall -Wextra +CFLAGS = -nostdlib -nostartfiles -nodefaultlibs -fno-builtin -fno-stack-protector -Wall -Wextra -I../include LD = ld LDFLAGS = -r Library = grapes.o -Objects = grapes/syscall.o \ +Objects = gc/syscall.o \ start.o all: $(Library) diff --git a/src/library/gc/syscall.c b/src/library/gc/syscall.c new file mode 100644 index 0000000..e1ef3cd --- /dev/null +++ b/src/library/gc/syscall.c @@ -0,0 +1,47 @@ +#include + +static int call(unsigned a, unsigned b, unsigned c, unsigned d, unsigned e, unsigned f) { + unsigned ret; + asm volatile("int $64" : "=a"(ret) : "a"(a), "b"(b), "c"(c), "d"(d), "S"(e), "D"(f)); + return ret; +} + +void thread_exit() { + call(0, 0, 0, 0, 0, 0); +} + +void schedule() { + call(1, 0, 0,0, 0, 0); +} + +void thread_sleep(int time) { + call(2, time, 0, 0, 0, 0); +} + +void process_exit(int retval) { + call(3, retval, 0, 0, 0, 0); +} + +void printk(char* str) { + call(4, (unsigned)str, 0, 0, 0, 0); +} + +void thread_new(void (*entry)(void*), void *data) { + call(5, (unsigned)entry, (unsigned)data, 0, 0, 0); +} + +void irq_wait(int number) { + call(6, number, 0, 0, 0, 0); +} + +int proc_priv() { + return call(7, 0, 0, 0, 0, 0); +} + +int shm_create(size_t offset, size_t length) { + return call(8, offset, length, 0, 0, 0); +} + +int shm_delete(size_t offset) { + return call(9, offset, 0, 0, 0, 0); +} diff --git a/src/library/grapes/syscall.c b/src/library/grapes/syscall.c deleted file mode 100644 index 38adc01..0000000 --- a/src/library/grapes/syscall.c +++ /dev/null @@ -1,35 +0,0 @@ -#include "syscall.h" - -static int call(unsigned a, unsigned b, unsigned c, unsigned d, unsigned e, unsigned f) { - unsigned ret; - asm volatile("int $64" : "=a"(ret) : "a"(a), "b"(b), "c"(c), "d"(d), "S"(e), "D"(f)); - return ret; -} - -void thread_exit() { - call(0, 0, 0, 0, 0, 0); -} - -void schedule() { - call(1, 0, 0,0, 0, 0); -} - -void thread_sleep(int time) { - call(2, time, 0, 0, 0, 0); -} - -void process_exit(int retval) { - call(3, retval, 0, 0, 0, 0); -} - -void printk(char* str) { - call(4, (unsigned)str, 0, 0, 0, 0); -} - -void thread_new(void (*entry)(void*), void *data) { - call(5, (unsigned)entry, (unsigned)data, 0, 0, 0); -} - -void irq_wait(int number) { - call(6, number, 0, 0, 0, 0); -} diff --git a/src/library/grapes/syscall.h b/src/library/grapes/syscall.h deleted file mode 100644 index d2f80e5..0000000 --- a/src/library/grapes/syscall.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef DEF_SYSCALL_H -#define DEF_SYSCALL_H - -void thread_exit(); -void schedule(); -void thread_sleep(int time); -void process_exit(int retval); -void printk(char* str); -void thread_new(void (*entry)(void*), void *data); -void irq_wait(int number); - -#endif diff --git a/src/library/start.c b/src/library/start.c index 8f83a23..b3c5541 100644 --- a/src/library/start.c +++ b/src/library/start.c @@ -1,4 +1,4 @@ -#include "grapes/syscall.h" +#include extern int main(); diff --git a/src/modules/test/Makefile b/src/modules/test/Makefile index d12e57d..e571da9 100644 --- a/src/modules/test/Makefile +++ b/src/modules/test/Makefile @@ -1,7 +1,7 @@ .PHONY: clean, mrproper CC = gcc -CFLAGS = -nostdlib -nostartfiles -nodefaultlibs -fno-builtin -fno-stack-protector -Wall -Wextra -I ../../library +CFLAGS = -nostdlib -nostartfiles -nodefaultlibs -fno-builtin -fno-stack-protector -Wall -Wextra -I ../../include LD = ld LDFLAGS = -T ../../library/link.ld -L ../../library -Map main.map diff --git a/src/modules/test/main.c b/src/modules/test/main.c index 01ab836..9e03ee8 100644 --- a/src/modules/test/main.c +++ b/src/modules/test/main.c @@ -1,9 +1,11 @@ -#include +#include + +#define FACTOR 4 void thread2(void* d) { while (1) { - thread_sleep(1400); printk("$"); + thread_sleep(35*FACTOR); } } @@ -12,8 +14,8 @@ int main() { printk("[module:test] Creating new thread...\n"); thread_new(thread2, 0); while (1) { - thread_sleep(2000); printk("."); + thread_sleep(50*FACTOR); } return 0; } -- cgit v1.2.3