linux/arch/s390/kernel/setup.c
<<
>>
Prefs
   1/*
   2 *  arch/s390/kernel/setup.c
   3 *
   4 *  S390 version
   5 *    Copyright (C) 1999,2000 IBM Deutschland Entwicklung GmbH, IBM Corporation
   6 *    Author(s): Hartmut Penner (hp@de.ibm.com),
   7 *               Martin Schwidefsky (schwidefsky@de.ibm.com)
   8 *
   9 *  Derived from "arch/i386/kernel/setup.c"
  10 *    Copyright (C) 1995, Linus Torvalds
  11 */
  12
  13/*
  14 * This file handles the architecture-dependent parts of initialization
  15 */
  16
  17#include <linux/errno.h>
  18#include <linux/module.h>
  19#include <linux/sched.h>
  20#include <linux/kernel.h>
  21#include <linux/mm.h>
  22#include <linux/stddef.h>
  23#include <linux/unistd.h>
  24#include <linux/ptrace.h>
  25#include <linux/slab.h>
  26#include <linux/user.h>
  27#include <linux/a.out.h>
  28#include <linux/tty.h>
  29#include <linux/ioport.h>
  30#include <linux/delay.h>
  31#include <linux/config.h>
  32#include <linux/init.h>
  33#include <linux/initrd.h>
  34#include <linux/bootmem.h>
  35#include <linux/root_dev.h>
  36#include <linux/console.h>
  37#include <linux/seq_file.h>
  38#include <linux/kernel_stat.h>
  39
  40#include <asm/uaccess.h>
  41#include <asm/system.h>
  42#include <asm/smp.h>
  43#include <asm/mmu_context.h>
  44#include <asm/cpcmd.h>
  45#include <asm/lowcore.h>
  46#include <asm/irq.h>
  47
  48/*
  49 * Machine setup..
  50 */
  51unsigned int console_mode = 0;
  52unsigned int console_devno = -1;
  53unsigned int console_irq = -1;
  54unsigned long memory_size = 0;
  55unsigned long machine_flags = 0;
  56unsigned int default_storage_key = 0;
  57struct {
  58        unsigned long addr, size, type;
  59} memory_chunk[MEMORY_CHUNKS] = { { 0 } };
  60#define CHUNK_READ_WRITE 0
  61#define CHUNK_READ_ONLY 1
  62volatile int __cpu_logical_map[NR_CPUS]; /* logical cpu to cpu address */
  63
  64/*
  65 * Setup options
  66 */
  67extern int _text,_etext, _edata, _end;
  68
  69/*
  70 * This is set up by the setup-routine at boot-time
  71 * for S390 need to find out, what we have to setup
  72 * using address 0x10400 ...
  73 */
  74
  75#include <asm/setup.h>
  76
  77static char command_line[COMMAND_LINE_SIZE] = { 0, };
  78
  79static struct resource code_resource = {
  80        .name  = "Kernel code",
  81        .flags = IORESOURCE_BUSY | IORESOURCE_MEM,
  82};
  83
  84static struct resource data_resource = {
  85        .name = "Kernel data",
  86        .flags = IORESOURCE_BUSY | IORESOURCE_MEM,
  87};
  88
  89/*
  90 * cpu_init() initializes state that is per-CPU.
  91 */
  92void __devinit cpu_init (void)
  93{
  94        int addr = hard_smp_processor_id();
  95
  96        /*
  97         * Store processor id in lowcore (used e.g. in timer_interrupt)
  98         */
  99        asm volatile ("stidp %0": "=m" (S390_lowcore.cpu_data.cpu_id));
 100        S390_lowcore.cpu_data.cpu_addr = addr;
 101
 102        /*
 103         * Force FPU initialization:
 104         */
 105        clear_thread_flag(TIF_USEDFPU);
 106        clear_used_math();
 107
 108        atomic_inc(&init_mm.mm_count);
 109        current->active_mm = &init_mm;
 110        if (current->mm)
 111                BUG();
 112        enter_lazy_tlb(&init_mm, current);
 113}
 114
 115/*
 116 * VM halt and poweroff setup routines
 117 */
 118char vmhalt_cmd[128] = "";
 119char vmpoff_cmd[128] = "";
 120
 121static inline void strncpy_skip_quote(char *dst, char *src, int n)
 122{
 123        int sx, dx;
 124
 125        dx = 0;
 126        for (sx = 0; src[sx] != 0; sx++) {
 127                if (src[sx] == '"') continue;
 128                dst[dx++] = src[sx];
 129                if (dx >= n) break;
 130        }
 131}
 132
 133static int __init vmhalt_setup(char *str)
 134{
 135        strncpy_skip_quote(vmhalt_cmd, str, 127);
 136        vmhalt_cmd[127] = 0;
 137        return 1;
 138}
 139
 140__setup("vmhalt=", vmhalt_setup);
 141
 142static int __init vmpoff_setup(char *str)
 143{
 144        strncpy_skip_quote(vmpoff_cmd, str, 127);
 145        vmpoff_cmd[127] = 0;
 146        return 1;
 147}
 148
 149__setup("vmpoff=", vmpoff_setup);
 150
 151/*
 152 * condev= and conmode= setup parameter.
 153 */
 154
 155static int __init condev_setup(char *str)
 156{
 157        int vdev;
 158
 159        vdev = simple_strtoul(str, &str, 0);
 160        if (vdev >= 0 && vdev < 65536) {
 161                console_devno = vdev;
 162                console_irq = -1;
 163        }
 164        return 1;
 165}
 166
 167__setup("condev=", condev_setup);
 168
 169static int __init conmode_setup(char *str)
 170{
 171#if defined(CONFIG_SCLP_CONSOLE)
 172        if (strncmp(str, "hwc", 4) == 0 || strncmp(str, "sclp", 5) == 0)
 173                SET_CONSOLE_SCLP;
 174#endif
 175#if defined(CONFIG_TN3215_CONSOLE)
 176        if (strncmp(str, "3215", 5) == 0)
 177                SET_CONSOLE_3215;
 178#endif
 179#if defined(CONFIG_TN3270_CONSOLE)
 180        if (strncmp(str, "3270", 5) == 0)
 181                SET_CONSOLE_3270;
 182#endif
 183        return 1;
 184}
 185
 186__setup("conmode=", conmode_setup);
 187
 188static void __init conmode_default(void)
 189{
 190        char query_buffer[1024];
 191        char *ptr;
 192
 193        if (MACHINE_IS_VM) {
 194                __cpcmd("QUERY CONSOLE", query_buffer, 1024);
 195                console_devno = simple_strtoul(query_buffer + 5, NULL, 16);
 196                ptr = strstr(query_buffer, "SUBCHANNEL =");
 197                console_irq = simple_strtoul(ptr + 13, NULL, 16);
 198                __cpcmd("QUERY TERM", query_buffer, 1024);
 199                ptr = strstr(query_buffer, "CONMODE");
 200                /*
 201                 * Set the conmode to 3215 so that the device recognition 
 202                 * will set the cu_type of the console to 3215. If the
 203                 * conmode is 3270 and we don't set it back then both
 204                 * 3215 and the 3270 driver will try to access the console
 205                 * device (3215 as console and 3270 as normal tty).
 206                 */
 207                __cpcmd("TERM CONMODE 3215", NULL, 0);
 208                if (ptr == NULL) {
 209#if defined(CONFIG_SCLP_CONSOLE)
 210                        SET_CONSOLE_SCLP;
 211#endif
 212                        return;
 213                }
 214                if (strncmp(ptr + 8, "3270", 4) == 0) {
 215#if defined(CONFIG_TN3270_CONSOLE)
 216                        SET_CONSOLE_3270;
 217#elif defined(CONFIG_TN3215_CONSOLE)
 218                        SET_CONSOLE_3215;
 219#elif defined(CONFIG_SCLP_CONSOLE)
 220                        SET_CONSOLE_SCLP;
 221#endif
 222                } else if (strncmp(ptr + 8, "3215", 4) == 0) {
 223#if defined(CONFIG_TN3215_CONSOLE)
 224                        SET_CONSOLE_3215;
 225#elif defined(CONFIG_TN3270_CONSOLE)
 226                        SET_CONSOLE_3270;
 227#elif defined(CONFIG_SCLP_CONSOLE)
 228                        SET_CONSOLE_SCLP;
 229#endif
 230                }
 231        } else if (MACHINE_IS_P390) {
 232#if defined(CONFIG_TN3215_CONSOLE)
 233                SET_CONSOLE_3215;
 234#elif defined(CONFIG_TN3270_CONSOLE)
 235                SET_CONSOLE_3270;
 236#endif
 237        } else {
 238#if defined(CONFIG_SCLP_CONSOLE)
 239                SET_CONSOLE_SCLP;
 240#endif
 241        }
 242}
 243
 244#ifdef CONFIG_SMP
 245extern void machine_restart_smp(char *);
 246extern void machine_halt_smp(void);
 247extern void machine_power_off_smp(void);
 248
 249void (*_machine_restart)(char *command) = machine_restart_smp;
 250void (*_machine_halt)(void) = machine_halt_smp;
 251void (*_machine_power_off)(void) = machine_power_off_smp;
 252#else
 253/*
 254 * Reboot, halt and power_off routines for non SMP.
 255 */
 256extern void reipl(unsigned long devno);
 257static void do_machine_restart_nonsmp(char * __unused)
 258{
 259        if (MACHINE_IS_VM)
 260                cpcmd ("IPL", NULL, 0);
 261        else
 262                reipl (0x10000 | S390_lowcore.ipl_device);
 263}
 264
 265static void do_machine_halt_nonsmp(void)
 266{
 267        if (MACHINE_IS_VM && strlen(vmhalt_cmd) > 0)
 268                cpcmd(vmhalt_cmd, NULL, 0);
 269        signal_processor(smp_processor_id(), sigp_stop_and_store_status);
 270}
 271
 272static void do_machine_power_off_nonsmp(void)
 273{
 274        if (MACHINE_IS_VM && strlen(vmpoff_cmd) > 0)
 275                cpcmd(vmpoff_cmd, NULL, 0);
 276        signal_processor(smp_processor_id(), sigp_stop_and_store_status);
 277}
 278
 279void (*_machine_restart)(char *command) = do_machine_restart_nonsmp;
 280void (*_machine_halt)(void) = do_machine_halt_nonsmp;
 281void (*_machine_power_off)(void) = do_machine_power_off_nonsmp;
 282#endif
 283
 284 /*
 285 * Reboot, halt and power_off stubs. They just call _machine_restart,
 286 * _machine_halt or _machine_power_off. 
 287 */
 288
 289void machine_restart(char *command)
 290{
 291        console_unblank();
 292        _machine_restart(command);
 293}
 294
 295EXPORT_SYMBOL(machine_restart);
 296
 297void machine_halt(void)
 298{
 299        console_unblank();
 300        _machine_halt();
 301}
 302
 303EXPORT_SYMBOL(machine_halt);
 304
 305void machine_power_off(void)
 306{
 307        console_unblank();
 308        _machine_power_off();
 309}
 310
 311EXPORT_SYMBOL(machine_power_off);
 312
 313/*
 314 * Setup function called from init/main.c just after the banner
 315 * was printed.
 316 */
 317extern char _pstart, _pend, _stext;
 318
 319void __init setup_arch(char **cmdline_p)
 320{
 321        unsigned long bootmap_size;
 322        unsigned long memory_start, memory_end;
 323        char c = ' ', cn, *to = command_line, *from = COMMAND_LINE;
 324        unsigned long start_pfn, end_pfn;
 325        static unsigned int smptrap=0;
 326        unsigned long delay = 0;
 327        struct _lowcore *lc;
 328        int i;
 329
 330        if (smptrap)
 331                return;
 332        smptrap=1;
 333
 334        /*
 335         * print what head.S has found out about the machine 
 336         */
 337#ifndef CONFIG_ARCH_S390X
 338        printk((MACHINE_IS_VM) ?
 339               "We are running under VM (31 bit mode)\n" :
 340               "We are running native (31 bit mode)\n");
 341        printk((MACHINE_HAS_IEEE) ?
 342               "This machine has an IEEE fpu\n" :
 343               "This machine has no IEEE fpu\n");
 344#else /* CONFIG_ARCH_S390X */
 345        printk((MACHINE_IS_VM) ?
 346               "We are running under VM (64 bit mode)\n" :
 347               "We are running native (64 bit mode)\n");
 348#endif /* CONFIG_ARCH_S390X */
 349
 350        ROOT_DEV = Root_RAM0;
 351        memory_start = (unsigned long) &_end;    /* fixit if use $CODELO etc*/
 352#ifndef CONFIG_ARCH_S390X
 353        memory_end = memory_size & ~0x400000UL;  /* align memory end to 4MB */
 354        /*
 355         * We need some free virtual space to be able to do vmalloc.
 356         * On a machine with 2GB memory we make sure that we have at
 357         * least 128 MB free space for vmalloc.
 358         */
 359        if (memory_end > 1920*1024*1024)
 360                memory_end = 1920*1024*1024;
 361#else /* CONFIG_ARCH_S390X */
 362        memory_end = memory_size & ~0x200000UL;  /* detected in head.s */
 363#endif /* CONFIG_ARCH_S390X */
 364        init_mm.start_code = PAGE_OFFSET;
 365        init_mm.end_code = (unsigned long) &_etext;
 366        init_mm.end_data = (unsigned long) &_edata;
 367        init_mm.brk = (unsigned long) &_end;
 368
 369        code_resource.start = (unsigned long) &_text;
 370        code_resource.end = (unsigned long) &_etext - 1;
 371        data_resource.start = (unsigned long) &_etext;
 372        data_resource.end = (unsigned long) &_edata - 1;
 373
 374        /* Save unparsed command line copy for /proc/cmdline */
 375        memcpy(saved_command_line, COMMAND_LINE, COMMAND_LINE_SIZE);
 376        saved_command_line[COMMAND_LINE_SIZE-1] = '\0';
 377
 378        for (;;) {
 379                /*
 380                 * "mem=XXX[kKmM]" sets memsize 
 381                 */
 382                if (c == ' ' && strncmp(from, "mem=", 4) == 0) {
 383                        memory_end = simple_strtoul(from+4, &from, 0);
 384                        if ( *from == 'K' || *from == 'k' ) {
 385                                memory_end = memory_end << 10;
 386                                from++;
 387                        } else if ( *from == 'M' || *from == 'm' ) {
 388                                memory_end = memory_end << 20;
 389                                from++;
 390                        }
 391                }
 392                /*
 393                 * "ipldelay=XXX[sm]" sets ipl delay in seconds or minutes
 394                 */
 395                if (c == ' ' && strncmp(from, "ipldelay=", 9) == 0) {
 396                        delay = simple_strtoul(from+9, &from, 0);
 397                        if (*from == 's' || *from == 'S') {
 398                                delay = delay*1000000;
 399                                from++;
 400                        } else if (*from == 'm' || *from == 'M') {
 401                                delay = delay*60*1000000;
 402                                from++;
 403                        }
 404                        /* now wait for the requested amount of time */
 405                        udelay(delay);
 406                }
 407                cn = *(from++);
 408                if (!cn)
 409                        break;
 410                if (cn == '\n')
 411                        cn = ' ';  /* replace newlines with space */
 412                if (cn == 0x0d)
 413                        cn = ' ';  /* replace 0x0d with space */
 414                if (cn == ' ' && c == ' ')
 415                        continue;  /* remove additional spaces */
 416                c = cn;
 417                if (to - command_line >= COMMAND_LINE_SIZE)
 418                        break;
 419                *(to++) = c;
 420        }
 421        if (c == ' ' && to > command_line) to--;
 422        *to = '\0';
 423        *cmdline_p = command_line;
 424
 425        /*
 426         * partially used pages are not usable - thus
 427         * we are rounding upwards:
 428         */
 429        start_pfn = (__pa(&_end) + PAGE_SIZE - 1) >> PAGE_SHIFT;
 430        end_pfn = max_pfn = memory_end >> PAGE_SHIFT;
 431
 432        /*
 433         * Initialize the boot-time allocator (with low memory only):
 434         */
 435        bootmap_size = init_bootmem(start_pfn, end_pfn);
 436
 437        /*
 438         * Register RAM areas with the bootmem allocator.
 439         */
 440        for (i = 0; i < 16 && memory_chunk[i].size > 0; i++) {
 441                unsigned long start_chunk, end_chunk;
 442
 443                if (memory_chunk[i].type != CHUNK_READ_WRITE)
 444                        continue;
 445                start_chunk = (memory_chunk[i].addr + PAGE_SIZE - 1);
 446                start_chunk >>= PAGE_SHIFT;
 447                end_chunk = (memory_chunk[i].addr + memory_chunk[i].size);
 448                end_chunk >>= PAGE_SHIFT;
 449                if (start_chunk < start_pfn)
 450                        start_chunk = start_pfn;
 451                if (end_chunk > end_pfn)
 452                        end_chunk = end_pfn;
 453                if (start_chunk < end_chunk)
 454                        free_bootmem(start_chunk << PAGE_SHIFT,
 455                                     (end_chunk - start_chunk) << PAGE_SHIFT);
 456        }
 457
 458        /*
 459         * Reserve the bootmem bitmap itself as well. We do this in two
 460         * steps (first step was init_bootmem()) because this catches
 461         * the (very unlikely) case of us accidentally initializing the
 462         * bootmem allocator with an invalid RAM area.
 463         */
 464        reserve_bootmem(start_pfn << PAGE_SHIFT, bootmap_size);
 465
 466#ifdef CONFIG_BLK_DEV_INITRD
 467        if (INITRD_START) {
 468                if (INITRD_START + INITRD_SIZE <= memory_end) {
 469                        reserve_bootmem(INITRD_START, INITRD_SIZE);
 470                        initrd_start = INITRD_START;
 471                        initrd_end = initrd_start + INITRD_SIZE;
 472                } else {
 473                        printk("initrd extends beyond end of memory "
 474                               "(0x%08lx > 0x%08lx)\ndisabling initrd\n",
 475                               initrd_start + INITRD_SIZE, memory_end);
 476                        initrd_start = initrd_end = 0;
 477                }
 478        }
 479#endif
 480
 481        for (i = 0; i < 16 && memory_chunk[i].size > 0; i++) {
 482                struct resource *res;
 483
 484                res = alloc_bootmem_low(sizeof(struct resource));
 485                res->flags = IORESOURCE_BUSY | IORESOURCE_MEM;
 486
 487                switch (memory_chunk[i].type) {
 488                case CHUNK_READ_WRITE:
 489                        res->name = "System RAM";
 490                        break;
 491                case CHUNK_READ_ONLY:
 492                        res->name = "System ROM";
 493                        res->flags |= IORESOURCE_READONLY;
 494                        break;
 495                default:
 496                        res->name = "reserved";
 497                }
 498                res->start = memory_chunk[i].addr;
 499                res->end = memory_chunk[i].addr +  memory_chunk[i].size - 1;
 500                request_resource(&iomem_resource, res);
 501                request_resource(res, &code_resource);
 502                request_resource(res, &data_resource);
 503        }
 504
 505        /*
 506         * Setup lowcore for boot cpu
 507         */
 508#ifndef CONFIG_ARCH_S390X
 509        lc = (struct _lowcore *) __alloc_bootmem(PAGE_SIZE, PAGE_SIZE, 0);
 510        memset(lc, 0, PAGE_SIZE);
 511#else /* CONFIG_ARCH_S390X */
 512        lc = (struct _lowcore *) __alloc_bootmem(2*PAGE_SIZE, 2*PAGE_SIZE, 0);
 513        memset(lc, 0, 2*PAGE_SIZE);
 514#endif /* CONFIG_ARCH_S390X */
 515        lc->restart_psw.mask = PSW_BASE_BITS;
 516        lc->restart_psw.addr =
 517                PSW_ADDR_AMODE | (unsigned long) restart_int_handler;
 518        lc->external_new_psw.mask = PSW_KERNEL_BITS;
 519        lc->external_new_psw.addr =
 520                PSW_ADDR_AMODE | (unsigned long) ext_int_handler;
 521        lc->svc_new_psw.mask = PSW_KERNEL_BITS | PSW_MASK_IO | PSW_MASK_EXT;
 522        lc->svc_new_psw.addr = PSW_ADDR_AMODE | (unsigned long) system_call;
 523        lc->program_new_psw.mask = PSW_KERNEL_BITS;
 524        lc->program_new_psw.addr =
 525                PSW_ADDR_AMODE | (unsigned long)pgm_check_handler;
 526        lc->mcck_new_psw.mask = PSW_KERNEL_BITS;
 527        lc->mcck_new_psw.addr =
 528                PSW_ADDR_AMODE | (unsigned long) mcck_int_handler;
 529        lc->io_new_psw.mask = PSW_KERNEL_BITS;
 530        lc->io_new_psw.addr = PSW_ADDR_AMODE | (unsigned long) io_int_handler;
 531        lc->ipl_device = S390_lowcore.ipl_device;
 532        lc->jiffy_timer = -1LL;
 533        lc->kernel_stack = ((unsigned long) &init_thread_union) + THREAD_SIZE;
 534        lc->async_stack = (unsigned long)
 535                __alloc_bootmem(ASYNC_SIZE, ASYNC_SIZE, 0) + ASYNC_SIZE;
 536#ifdef CONFIG_CHECK_STACK
 537        lc->panic_stack = (unsigned long)
 538                __alloc_bootmem(PAGE_SIZE, PAGE_SIZE, 0) + PAGE_SIZE;
 539#endif
 540        lc->current_task = (unsigned long) init_thread_union.thread_info.task;
 541        lc->thread_info = (unsigned long) &init_thread_union;
 542#ifdef CONFIG_ARCH_S390X
 543        if (MACHINE_HAS_DIAG44)
 544                lc->diag44_opcode = 0x83000044;
 545        else
 546                lc->diag44_opcode = 0x07000700;
 547#endif /* CONFIG_ARCH_S390X */
 548        set_prefix((u32)(unsigned long) lc);
 549        cpu_init();
 550        __cpu_logical_map[0] = S390_lowcore.cpu_data.cpu_addr;
 551
 552        /*
 553         * Create kernel page tables and switch to virtual addressing.
 554         */
 555        paging_init();
 556
 557        /* Setup default console */
 558        conmode_default();
 559}
 560
 561void print_cpu_info(struct cpuinfo_S390 *cpuinfo)
 562{
 563   printk("cpu %d "
 564#ifdef CONFIG_SMP
 565           "phys_idx=%d "
 566#endif
 567           "vers=%02X ident=%06X machine=%04X unused=%04X\n",
 568           cpuinfo->cpu_nr,
 569#ifdef CONFIG_SMP
 570           cpuinfo->cpu_addr,
 571#endif
 572           cpuinfo->cpu_id.version,
 573           cpuinfo->cpu_id.ident,
 574           cpuinfo->cpu_id.machine,
 575           cpuinfo->cpu_id.unused);
 576}
 577
 578/*
 579 * show_cpuinfo - Get information on one CPU for use by procfs.
 580 */
 581
 582static int show_cpuinfo(struct seq_file *m, void *v)
 583{
 584        struct cpuinfo_S390 *cpuinfo;
 585        unsigned long n = (unsigned long) v - 1;
 586
 587        if (!n) {
 588                seq_printf(m, "vendor_id       : IBM/S390\n"
 589                               "# processors    : %i\n"
 590                               "bogomips per cpu: %lu.%02lu\n",
 591                               num_online_cpus(), loops_per_jiffy/(500000/HZ),
 592                               (loops_per_jiffy/(5000/HZ))%100);
 593        }
 594        if (cpu_online(n)) {
 595#ifdef CONFIG_SMP
 596                if (smp_processor_id() == n)
 597                        cpuinfo = &S390_lowcore.cpu_data;
 598                else
 599                        cpuinfo = &lowcore_ptr[n]->cpu_data;
 600#else
 601                cpuinfo = &S390_lowcore.cpu_data;
 602#endif
 603                seq_printf(m, "processor %li: "
 604                               "version = %02X,  "
 605                               "identification = %06X,  "
 606                               "machine = %04X\n",
 607                               n, cpuinfo->cpu_id.version,
 608                               cpuinfo->cpu_id.ident,
 609                               cpuinfo->cpu_id.machine);
 610        }
 611        return 0;
 612}
 613
 614static void *c_start(struct seq_file *m, loff_t *pos)
 615{
 616        return *pos < NR_CPUS ? (void *)((unsigned long) *pos + 1) : NULL;
 617}
 618static void *c_next(struct seq_file *m, void *v, loff_t *pos)
 619{
 620        ++*pos;
 621        return c_start(m, pos);
 622}
 623static void c_stop(struct seq_file *m, void *v)
 624{
 625}
 626struct seq_operations cpuinfo_op = {
 627        .start  = c_start,
 628        .next   = c_next,
 629        .stop   = c_stop,
 630        .show   = show_cpuinfo,
 631};
 632
 633
lxr.linux.no kindly hosted by Redpill Linpro AS, provider of Linux consulting and operations services since 1995.