aboutsummaryrefslogtreecommitdiffstats
path: root/package/kernel/linux/modules/virt.mk
blob: f45cb176147fc8e6e7cb37bf508654c82fed8cc9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#
# Copyright (C) 2016 Yousong Zhou <yszhou4tech@gmail.com>
#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
#
define KernelPackage/irqbypass
  SUBMENU:=Virtualization
  TITLE:=IRQ offload/bypass manager
  KCONFIG:=CONFIG_IRQ_BYPASS_MANAGER
  HIDDEN:=1
  FILES:= $(LINUX_DIR)/virt/lib/irqbypass.ko
  AUTOLOAD:=$(call AutoProbe,irqbypass.ko)
endef
$(eval $(call KernelPackage,irqbypass))


define KernelPackage/kvm-x86
  SUBMENU:=Virtualization
  TITLE:=Kernel-based Virtual Machine (KVM) support
  DEPENDS:=@TARGET_x86_generic||TARGET_x86_64 +kmod-irqbypass
  KCONFIG:=\
	  CONFIG_KVM \
	  CONFIG_KVM_MMU_AUDIT=n \
	  CONFIG_VIRTUALIZATION=y
  FILES:= $(LINUX_DIR)/arch/$(LINUX_KARCH)/kvm/kvm.ko
  AUTOLOAD:=$(call AutoProbe,kvm.ko)
endef

define KernelPackage/kvm-x86/description
  Support hosting fully virtualized guest machines using hardware
  virtualization extensions.  You will need a fairly recent
  processor equipped with virtualization extensions. You will also
  need to select one or more of the processor modules.

  This module provides access to the hardware capabilities through
  a character device node named /dev/kvm.
endef

$(eval $(call KernelPackage,kvm-x86))


define KernelPackage/kvm-intel
  SUBMENU:=Virtualization
  TITLE:=KVM for Intel processors support
  DEPENDS:=+kmod-kvm-x86
  KCONFIG:=CONFIG_KVM_INTEL
  FILES:= $(LINUX_DIR)/arch/$(LINUX_KARCH)/kvm/kvm-intel.ko
  AUTOLOAD:=$(call AutoProbe,kvm-intel.ko)
endef

define KernelPackage/kvm-intel/description
  Provides support for KVM on Intel processors equipped with the VT
  extensions.
endef

$(eval $(call KernelPackage,kvm-intel))


define KernelPackage/kvm-amd
  SUBMENU:=Virtualization
  TITLE:=KVM for AMD processors support
  DEPENDS:=+kmod-kvm-x86
  KCONFIG:=CONFIG_KVM_AMD
  FILES:= $(LINUX_DIR)/arch/$(LINUX_KARCH)/kvm/kvm-amd.ko
  AUTOLOAD:=$(call AutoProbe,kvm-amd.ko)
endef

define KernelPackage/kvm-amd/description
  Provides support for KVM on AMD processors equipped with the AMD-V
  (SVM) extensions.
endef

$(eval $(call KernelPackage,kvm-amd))


define KernelPackage/vfio
  SUBMENU:=Virtualization
  TITLE:=VFIO Non-Privileged userspace driver framework
  DEPENDS:=@TARGET_x86_64||TARGET_armsr_armv8
  KCONFIG:= \
	CONFIG_VFIO \
	CONFIG_VFIO_NOIOMMU=n \
	CONFIG_VFIO_MDEV=n
  FILES:= \
	$(LINUX_DIR)/drivers/vfio/vfio.ko \
	$(LINUX_DIR)/drivers/vfio/vfio_virqfd.ko \
	$(LINUX_DIR)/drivers/vfio/vfio_iommu_type1.ko
  AUTOLOAD:=$(call AutoProbe,vfio vfio_iommu_type1 vfio_virqfd)
endef

define KernelPackage/vfio/description
  VFIO provides a framework for secure userspace device drivers.
endef

$(eval $(call KernelPackage,vfio))


define KernelPackage/vfio-pci
  SUBMENU:=Virtualization
  TITLE:=Generic VFIO support for any PCI device
  DEPENDS:=@TARGET_x86_64||TARGET_armsr_armv8 @PCI_SUPPORT +kmod-vfio +kmod-irqbypass
  KCONFIG:= \
	CONFIG_VFIO_PCI \
	CONFIG_VFIO_PCI_IGD=n
  FILES:= \
	$(LINUX_DIR)/drivers/vfio/pci/vfio-pci-core.ko \
	$(LINUX_DIR)/drivers/vfio/pci/vfio-pci.ko
  AUTOLOAD:=$(call AutoProbe,vfio-pci)
endef

define KernelPackage/vfio-pci/description
  Support for the generic PCI VFIO bus driver which can connect any PCI
  device to the VFIO framework.
endef

$(eval $(call KernelPackage,vfio-pci))


define KernelPackage/vhost
  SUBMENU:=Virtualization
  TITLE:=Host kernel accelerator for virtio (base)
  KCONFIG:=CONFIG_VHOST
  FILES:=$(LINUX_DIR)/drivers/vhost/vhost.ko \
    $(LINUX_DIR)/drivers/vhost/vhost_iotlb.ko
  AUTOLOAD:=$(call AutoProbe,vhost vhost_iotlb)
endef

$(eval $(call KernelPackage,vhost))


define KernelPackage/vhost-net
  SUBMENU:=Virtualization
  TITLE:=Host kernel accelerator for virtio-net
  DEPENDS:=+kmod-tun +kmod-vhost
  KCONFIG:=CONFIG_VHOST_NET
  FILES:=$(LINUX_DIR)/drivers/vhost/vhost_net.ko
  AUTOLOAD:=$(call AutoProbe,vhost_net)
endef

$(eval $(call KernelPackage,vhost-net))
or: #555555 } /* Name.Decorator */ .highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */ .highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */ .highlight .nl { color: #336699; font-style: italic } /* Name.Label */ .highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */ .highlight .py { color: #336699; font-weight: bold } /* Name.Property */ .highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */ .highlight .nv { color: #336699 } /* Name.Variable */ .highlight .ow { color: #008800 } /* Operator.Word */ .highlight .w { color: #bbbbbb } /* Text.Whitespace */ .highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */ .highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */ .highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */ .highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */ .highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */ .highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */ .highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */ .highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */ .highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */ .highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */ .highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */ .highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */ .highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */ .highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */ .highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */ .highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */ .highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */ .highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */ .highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #336699 } /* Name.Variable.Class */ .highlight .vg { color: #dd7700 } /* Name.Variable.Global */ .highlight .vi { color: #3333bb } /* Name.Variable.Instance */ .highlight .vm { color: #336699 } /* Name.Variable.Magic */ .highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
/******************************************************************************
 * xc_ia64_linux_save.c
 *
 * Save the state of a running Linux session.
 *
 * Copyright (c) 2003, K A Fraser.
 *  Rewritten for ia64 by Tristan Gingold <tristan.gingold@bull.net>
 */

#include <inttypes.h>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>

#include "xg_private.h"

/*
** Default values for important tuning parameters. Can override by passing
** non-zero replacement values to xc_linux_save().
**
** XXX SMH: should consider if want to be able to override MAX_MBIT_RATE too.
**
*/
#define DEF_MAX_ITERS    (4 - 1)        /* limit us to 4 times round loop  */
#define DEF_MAX_FACTOR   3              /* never send more than 3x nr_pfns */

/*
** During (live) save/migrate, we maintain a number of bitmaps to track
** which pages we have to send, and to skip.
*/

#define BITS_PER_LONG (sizeof(unsigned long) * 8)

#define BITMAP_ENTRY(_nr,_bmap) \
   ((unsigned long *)(_bmap))[(_nr)/BITS_PER_LONG]

#define BITMAP_SHIFT(_nr) ((_nr) % BITS_PER_LONG)

static inline int test_bit(int nr, volatile void * addr)
{
    return (BITMAP_ENTRY(nr, addr) >> BITMAP_SHIFT(nr)) & 1;
}

static inline void clear_bit(int nr, volatile void * addr)
{
    BITMAP_ENTRY(nr, addr) &= ~(1UL << BITMAP_SHIFT(nr));
}

static inline void set_bit(int nr, volatile void * addr)
{
    BITMAP_ENTRY(nr, addr) |= (1UL << BITMAP_SHIFT(nr));
}

static int xc_ia64_shadow_control(int xc_handle,
                                  uint32_t domid,
                                  unsigned int sop,
                                  unsigned long *dirty_bitmap,
                                  unsigned long pages,
                                  xc_shadow_op_stats_t *stats)
{
    if (dirty_bitmap != NULL && pages > 0) {
        int i;
        unsigned char *bmap = (unsigned char *)dirty_bitmap;
        unsigned long bmap_bytes =
            ((pages + BITS_PER_LONG - 1) & ~(BITS_PER_LONG - 1)) / 8;
        unsigned int bmap_pages = (bmap_bytes + PAGE_SIZE - 1) / PAGE_SIZE;

        /* Touch the page so that it is in the TC.
           FIXME: use a more reliable method.  */
        for (i = 0 ; i < bmap_pages ; i++)
            bmap[i * PAGE_SIZE] = 0;
        /* Because bmap is not page aligned (allocated by malloc), be sure the
           last page is touched.  */
        bmap[bmap_bytes - 1] = 0;
    }

    return xc_shadow_control(xc_handle, domid, sop,
                             dirty_bitmap, pages, NULL, 0, stats);
}

static inline ssize_t
write_exact(int fd, void *buf, size_t count)
{
    if (write(fd, buf, count) != count)
        return 0;
    return 1;
}

static int
suspend_and_state(int (*suspend)(int), int xc_handle, int io_fd,
                  int dom, xc_dominfo_t *info)
{
    int i = 0;

    if (!(*suspend)(dom)) {
        ERROR("Suspend request failed");
        return -1;
    }

retry:

    if (xc_domain_getinfo(xc_handle, dom, 1, info) != 1) {
        ERROR("Could not get domain info");
        return -1;
    }

    if (info->shutdown && info->shutdown_reason == SHUTDOWN_suspend)
        return 0; // success

    if (info->paused) {
        // try unpausing domain, wait, and retest
        xc_domain_unpause(xc_handle, dom);

        ERROR("Domain was paused. Wait and re-test.");
        usleep(10000);  // 10ms

        goto retry;
    }


    if(++i < 100) {
        ERROR("Retry suspend domain.");
        usleep(10000);  // 10ms
        goto retry;
    }

    ERROR("Unable to suspend domain.");

    return -1;
}

int
xc_domain_save(int xc_handle, int io_fd, uint32_t dom, uint32_t max_iters,
               uint32_t max_factor, uint32_t flags, int (*suspend)(int),
               int hvm, void *(*init_qemu_maps)(int, unsigned),
               void (*qemu_flip_buffer)(int, int))
{
    DECLARE_DOMCTL;
    xc_dominfo_t info;

    int rc = 1;

    //int live  = (flags & XCFLAGS_LIVE);
    int debug = (flags & XCFLAGS_DEBUG);
    int live  = (flags & XCFLAGS_LIVE);

    /* The new domain's shared-info frame number. */
    unsigned long shared_info_frame;

    /* A copy of the CPU context of the guest. */
    vcpu_guest_context_t ctxt;

    unsigned long *page_array = NULL;

    /* Live mapping of shared info structure */
    shared_info_t *live_shinfo = NULL;

    /* Iteration number.  */
    int iter;

    /* Number of pages sent in the last iteration (live only).  */
    unsigned int sent_last_iter;

    /* Number of pages sent (live only).  */
    unsigned int total_sent;

    /* total number of pages used by the current guest */
    unsigned long p2m_size;

    /* Size of the shadow bitmap (live only).  */
    unsigned int bitmap_size = 0;

    /* True if last iteration.  */
    int last_iter;

    /* Bitmap of pages to be sent.  */
    unsigned long *to_send = NULL;
    /* Bitmap of pages not to be sent (because dirtied).  */
    unsigned long *to_skip = NULL;

    char *mem;

    if (debug)
        fprintf(stderr, "xc_linux_save (ia64): started dom=%d\n", dom);

    /* If no explicit control parameters given, use defaults */
    if (!max_iters)
        max_iters = DEF_MAX_ITERS;
    if (!max_factor)
        max_factor = DEF_MAX_FACTOR;

    //initialize_mbit_rate();

    if (xc_domain_getinfo(xc_handle, dom, 1, &info) != 1) {
        ERROR("Could not get domain info");
        return 1;
    }

    shared_info_frame = info.shared_info_frame;

#if 0
    /* cheesy sanity check */
    if ((info.max_memkb >> (PAGE_SHIFT - 10)) > max_mfn) {
        ERROR("Invalid state record -- pfn count out of range: %lu",
            (info.max_memkb >> (PAGE_SHIFT - 10)));
        goto out;
     }
#endif

    /* Map the shared info frame */
    live_shinfo = xc_map_foreign_range(xc_handle, dom, PAGE_SIZE,
                                       PROT_READ, shared_info_frame);
    if (!live_shinfo) {
        ERROR("Couldn't map live_shinfo");
        goto out;
    }

    p2m_size = xc_memory_op(xc_handle, XENMEM_maximum_gpfn, &dom);

    page_array = malloc(p2m_size * sizeof(unsigned long));
    if (page_array == NULL) {
        ERROR("Could not allocate memory");
        goto out;
    }

    /* This is expected by xm restore.  */
    if (!write_exact(io_fd, &p2m_size, sizeof(unsigned long))) {
        ERROR("write: p2m_size");
        goto out;
    }

    /* xc_linux_restore starts to read here.  */
    /* Write a version number.  This can avoid searching for a stupid bug
       if the format change.
       The version is hard-coded, don't forget to change the restore code
       too!  */
    {
        unsigned long version = 1;

        if (!write_exact(io_fd, &version, sizeof(unsigned long))) {
            ERROR("write: version");
            goto out;
        }
    }

    domctl.cmd = XEN_DOMCTL_arch_setup;
    domctl.domain = (domid_t)dom;
    domctl.u.arch_setup.flags = XEN_DOMAINSETUP_query;
    if (xc_domctl(xc_handle, &domctl) < 0) {
        ERROR("Could not get domain setup");
        goto out;
    }
    if (!write_exact(io_fd, &domctl.u.arch_setup,
                     sizeof(domctl.u.arch_setup))) {
        ERROR("write: domain setup");
        goto out;
    }

    /* Domain is still running at this point */
    if (live) {

        if (xc_ia64_shadow_control(xc_handle, dom,
                                   XEN_DOMCTL_SHADOW_OP_ENABLE_LOGDIRTY,
                                   NULL, 0, NULL ) < 0) {
            ERROR("Couldn't enable shadow mode");
            goto out;
        }

        last_iter = 0;

        bitmap_size = ((p2m_size + BITS_PER_LONG-1) & ~(BITS_PER_LONG-1)) / 8;
        to_send = malloc(bitmap_size);
        to_skip = malloc(bitmap_size);

        if (!to_send || !to_skip) {
            ERROR("Couldn't allocate bitmap array");
            goto out;
        }

        /* Initially all the pages must be sent.  */
        memset(to_send, 0xff, bitmap_size);

        if (lock_pages(to_send, bitmap_size)) {
            ERROR("Unable to lock_pages to_send");
            goto out;
        }
        if (lock_pages(to_skip, bitmap_size)) {
            ERROR("Unable to lock_pages to_skip");
            goto out;
        }

    } else {

        /* This is a non-live suspend. Issue the call back to get the
           domain suspended */

        last_iter = 1;

        if (suspend_and_state(suspend, xc_handle, io_fd, dom, &info)) {
            ERROR("Domain appears not to have suspended");
            goto out;
        }

    }

    sent_last_iter = p2m_size;
    total_sent = 0;

    for (iter = 1; ; iter++) {
        unsigned int sent_this_iter, skip_this_iter;
        unsigned long N;

        sent_this_iter = 0;
        skip_this_iter = 0;

        /* Get the pfn list, as it may change.  */
        if (xc_ia64_get_pfn_list(xc_handle, dom, page_array,
                                 0, p2m_size) != p2m_size) {
            ERROR("Could not get the page frame list");
            goto out;
        }

        /* Dirtied pages won't be saved.
           slightly wasteful to peek the whole array evey time,
           but this is fast enough for the moment. */
        if (!last_iter) {
            if (xc_ia64_shadow_control(xc_handle, dom,
                                       XEN_DOMCTL_SHADOW_OP_PEEK,
                                       to_skip, p2m_size, NULL) != p2m_size) {
                ERROR("Error peeking shadow bitmap");
                goto out;
            }
        }

        /* Start writing out the saved-domain record. */
        for (N = 0; N < p2m_size; N++) {
            if (page_array[N] == INVALID_MFN)
                continue;
            if (!last_iter) {
                if (test_bit(N, to_skip) && test_bit(N, to_send))
                    skip_this_iter++;
                if (test_bit(N, to_skip) || !test_bit(N, to_send))
                    continue;
            }

            if (debug)
                fprintf(stderr, "xc_linux_save: page %lx (%lu/%lu)\n",
                        page_array[N], N, p2m_size);

            mem = xc_map_foreign_range(xc_handle, dom, PAGE_SIZE,
                                       PROT_READ|PROT_WRITE, N);
            if (mem == NULL) {
                /* The page may have move.
                   It will be remarked dirty.
                   FIXME: to be tracked.  */
                fprintf(stderr, "cannot map mfn page %lx gpfn %lx: %s\n",
                        page_array[N], N, safe_strerror(errno));
                continue;
            }

            if (!write_exact(io_fd, &N, sizeof(N))) {
                ERROR("write: p2m_size");
                munmap(mem, PAGE_SIZE);
                goto out;
            }

            if (write(io_fd, mem, PAGE_SIZE) != PAGE_SIZE) {
                ERROR("Error when writing to state file (5)");
                munmap(mem, PAGE_SIZE);
                goto out;
            }
            munmap(mem, PAGE_SIZE);
            sent_this_iter++;
            total_sent++;
        }

        if (last_iter)
            break;

        DPRINTF(" %d: sent %d, skipped %d\n",
                iter, sent_this_iter, skip_this_iter );

        if (live) {
            if ( /* ((sent_this_iter > sent_last_iter) && RATE_IS_MAX()) || */
                (iter >= max_iters) || (sent_this_iter+skip_this_iter < 50) ||
                (total_sent > p2m_size*max_factor)) {
                DPRINTF("Start last iteration\n");
                last_iter = 1;

                if (suspend_and_state(suspend, xc_handle, io_fd, dom, &info)) {
                    ERROR("Domain appears not to have suspended");
                    goto out;
                }
            }

            /* Pages to be sent are pages which were dirty.  */
            if (xc_ia64_shadow_control(xc_handle, dom,
                                       XEN_DOMCTL_SHADOW_OP_CLEAN,
                                       to_send, p2m_size, NULL ) != p2m_size) {
                ERROR("Error flushing shadow PT");
                goto out;
            }

            sent_last_iter = sent_this_iter;

            //print_stats(xc_handle, dom, sent_this_iter, &stats, 1);
        }

    }

    fprintf(stderr, "All memory is saved\n");

    /* terminate */
    {
        unsigned long pfn = INVALID_MFN;
        if (!write_exact(io_fd, &pfn, sizeof(pfn))) {
            ERROR("Error when writing to state file (6)");
            goto out;
        }
    }

    /* Send through a list of all the PFNs that were not in map at the close */
    {
        unsigned int i,j;
        unsigned long pfntab[1024];

        for (i = 0, j = 0; i < p2m_size; i++) {
            if (page_array[i] == INVALID_MFN)
                j++;
        }

        if (!write_exact(io_fd, &j, sizeof(unsigned int))) {
            ERROR("Error when writing to state file (6a)");
            goto out;
        }

        for (i = 0, j = 0; i < p2m_size; ) {

            if (page_array[i] == INVALID_MFN)
                pfntab[j++] = i;

            i++;
            if (j == 1024 || i == p2m_size) {
                if (!write_exact(io_fd, &pfntab, sizeof(unsigned long)*j)) {
                    ERROR("Error when writing to state file (6b)");
                    goto out;
                }
                j = 0;
            }
        }

    }

    if (xc_vcpu_getcontext(xc_handle, dom, 0, &ctxt)) {
        ERROR("Could not get vcpu context");
        goto out;
    }

    if (!write_exact(io_fd, &ctxt, sizeof(ctxt))) {
        ERROR("Error when writing to state file (1)");
        goto out;
    }

    fprintf(stderr, "ip=%016lx, b0=%016lx\n", ctxt.regs.ip, ctxt.regs.b[0]);

    mem = xc_map_foreign_range(xc_handle, dom, PAGE_SIZE,
                               PROT_READ|PROT_WRITE, ctxt.privregs_pfn);
    if (mem == NULL) {
        ERROR("cannot map privreg page");
        goto out;
    }
    if (write(io_fd, mem, PAGE_SIZE) != PAGE_SIZE) {
        ERROR("Error when writing privreg to state file (5)");
        munmap(mem, PAGE_SIZE);
        goto out;
    }
    munmap(mem, PAGE_SIZE);

    if (!write_exact(io_fd, live_shinfo, PAGE_SIZE)) {
        ERROR("Error when writing to state file (1)");
        goto out;
    }

    /* Success! */
    rc = 0;

 out:

    if (live) {
        if (xc_ia64_shadow_control(xc_handle, dom, XEN_DOMCTL_SHADOW_OP_OFF,
                                   NULL, 0, NULL ) < 0) {
            DPRINTF("Warning - couldn't disable shadow mode");
        }
    }

    free(page_array);
    unlock_pages(to_send, bitmap_size);
    free(to_send);
    unlock_pages(to_skip, bitmap_size);
    free(to_skip);
    if (live_shinfo)
        munmap(live_shinfo, PAGE_SIZE);

    fprintf(stderr,"Save exit rc=%d\n",rc);

    return !!rc;
}

/*
 * Local variables:
 * mode: C
 * c-set-style: "BSD"
 * c-basic-offset: 4
 * tab-width: 4
 * indent-tabs-mode: nil
 * End:
 */