Merge branch kvm-arm64/selftest/timer into kvmarm-master/next
* kvm-arm64/selftest/timer: : . : Add a set of selftests for the KVM/arm64 timer emulation. : Comes with a minimal GICv3 infrastructure. : . KVM: arm64: selftests: arch_timer: Support vCPU migration KVM: arm64: selftests: Add arch_timer test KVM: arm64: selftests: Add host support for vGIC KVM: arm64: selftests: Add basic GICv3 support KVM: arm64: selftests: Add light-weight spinlock support KVM: arm64: selftests: Add guest support to get the vcpuid KVM: arm64: selftests: Maintain consistency for vcpuid type KVM: arm64: selftests: Add support to disable and enable local IRQs KVM: arm64: selftests: Add basic support to generate delays KVM: arm64: selftests: Add basic support for arch_timers KVM: arm64: selftests: Add support for cpu_relax KVM: arm64: selftests: Introduce ARM64_SYS_KVM_REG tools: arm64: Import sysreg.h KVM: arm64: selftests: Add MMIO readl/writel support Signed-off-by: Marc Zyngier <maz@kernel.org>
This commit is contained in:
commit
551a13346e
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +1,5 @@
|
|||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
/aarch64/arch_timer
|
||||
/aarch64/debug-exceptions
|
||||
/aarch64/get-reg-list
|
||||
/aarch64/psci_cpu_on_test
|
||||
|
|
|
@ -35,7 +35,7 @@ endif
|
|||
|
||||
LIBKVM = lib/assert.c lib/elf.c lib/io.c lib/kvm_util.c lib/rbtree.c lib/sparsebit.c lib/test_util.c lib/guest_modes.c lib/perf_test_util.c
|
||||
LIBKVM_x86_64 = lib/x86_64/apic.c lib/x86_64/processor.c lib/x86_64/vmx.c lib/x86_64/svm.c lib/x86_64/ucall.c lib/x86_64/handlers.S
|
||||
LIBKVM_aarch64 = lib/aarch64/processor.c lib/aarch64/ucall.c lib/aarch64/handlers.S
|
||||
LIBKVM_aarch64 = lib/aarch64/processor.c lib/aarch64/ucall.c lib/aarch64/handlers.S lib/aarch64/spinlock.c lib/aarch64/gic.c lib/aarch64/gic_v3.c lib/aarch64/vgic.c
|
||||
LIBKVM_s390x = lib/s390x/processor.c lib/s390x/ucall.c lib/s390x/diag318_test_handler.c
|
||||
|
||||
TEST_GEN_PROGS_x86_64 = x86_64/cr4_cpuid_sync_test
|
||||
|
@ -86,6 +86,7 @@ TEST_GEN_PROGS_x86_64 += set_memory_region_test
|
|||
TEST_GEN_PROGS_x86_64 += steal_time
|
||||
TEST_GEN_PROGS_x86_64 += kvm_binary_stats_test
|
||||
|
||||
TEST_GEN_PROGS_aarch64 += aarch64/arch_timer
|
||||
TEST_GEN_PROGS_aarch64 += aarch64/debug-exceptions
|
||||
TEST_GEN_PROGS_aarch64 += aarch64/get-reg-list
|
||||
TEST_GEN_PROGS_aarch64 += aarch64/psci_cpu_on_test
|
||||
|
|
|
@ -0,0 +1,479 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
/*
|
||||
* arch_timer.c - Tests the aarch64 timer IRQ functionality
|
||||
*
|
||||
* The test validates both the virtual and physical timer IRQs using
|
||||
* CVAL and TVAL registers. This consitutes the four stages in the test.
|
||||
* The guest's main thread configures the timer interrupt for a stage
|
||||
* and waits for it to fire, with a timeout equal to the timer period.
|
||||
* It asserts that the timeout doesn't exceed the timer period.
|
||||
*
|
||||
* On the other hand, upon receipt of an interrupt, the guest's interrupt
|
||||
* handler validates the interrupt by checking if the architectural state
|
||||
* is in compliance with the specifications.
|
||||
*
|
||||
* The test provides command-line options to configure the timer's
|
||||
* period (-p), number of vCPUs (-n), and iterations per stage (-i).
|
||||
* To stress-test the timer stack even more, an option to migrate the
|
||||
* vCPUs across pCPUs (-m), at a particular rate, is also provided.
|
||||
*
|
||||
* Copyright (c) 2021, Google LLC.
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <pthread.h>
|
||||
#include <linux/kvm.h>
|
||||
#include <linux/sizes.h>
|
||||
#include <linux/bitmap.h>
|
||||
#include <sys/sysinfo.h>
|
||||
|
||||
#include "kvm_util.h"
|
||||
#include "processor.h"
|
||||
#include "delay.h"
|
||||
#include "arch_timer.h"
|
||||
#include "gic.h"
|
||||
#include "vgic.h"
|
||||
|
||||
#define NR_VCPUS_DEF 4
|
||||
#define NR_TEST_ITERS_DEF 5
|
||||
#define TIMER_TEST_PERIOD_MS_DEF 10
|
||||
#define TIMER_TEST_ERR_MARGIN_US 100
|
||||
#define TIMER_TEST_MIGRATION_FREQ_MS 2
|
||||
|
||||
struct test_args {
|
||||
int nr_vcpus;
|
||||
int nr_iter;
|
||||
int timer_period_ms;
|
||||
int migration_freq_ms;
|
||||
};
|
||||
|
||||
static struct test_args test_args = {
|
||||
.nr_vcpus = NR_VCPUS_DEF,
|
||||
.nr_iter = NR_TEST_ITERS_DEF,
|
||||
.timer_period_ms = TIMER_TEST_PERIOD_MS_DEF,
|
||||
.migration_freq_ms = TIMER_TEST_MIGRATION_FREQ_MS,
|
||||
};
|
||||
|
||||
#define msecs_to_usecs(msec) ((msec) * 1000LL)
|
||||
|
||||
#define GICD_BASE_GPA 0x8000000ULL
|
||||
#define GICR_BASE_GPA 0x80A0000ULL
|
||||
|
||||
enum guest_stage {
|
||||
GUEST_STAGE_VTIMER_CVAL = 1,
|
||||
GUEST_STAGE_VTIMER_TVAL,
|
||||
GUEST_STAGE_PTIMER_CVAL,
|
||||
GUEST_STAGE_PTIMER_TVAL,
|
||||
GUEST_STAGE_MAX,
|
||||
};
|
||||
|
||||
/* Shared variables between host and guest */
|
||||
struct test_vcpu_shared_data {
|
||||
int nr_iter;
|
||||
enum guest_stage guest_stage;
|
||||
uint64_t xcnt;
|
||||
};
|
||||
|
||||
struct test_vcpu {
|
||||
uint32_t vcpuid;
|
||||
pthread_t pt_vcpu_run;
|
||||
struct kvm_vm *vm;
|
||||
};
|
||||
|
||||
static struct test_vcpu test_vcpu[KVM_MAX_VCPUS];
|
||||
static struct test_vcpu_shared_data vcpu_shared_data[KVM_MAX_VCPUS];
|
||||
|
||||
static int vtimer_irq, ptimer_irq;
|
||||
|
||||
static unsigned long *vcpu_done_map;
|
||||
static pthread_mutex_t vcpu_done_map_lock;
|
||||
|
||||
static void
|
||||
guest_configure_timer_action(struct test_vcpu_shared_data *shared_data)
|
||||
{
|
||||
switch (shared_data->guest_stage) {
|
||||
case GUEST_STAGE_VTIMER_CVAL:
|
||||
timer_set_next_cval_ms(VIRTUAL, test_args.timer_period_ms);
|
||||
shared_data->xcnt = timer_get_cntct(VIRTUAL);
|
||||
timer_set_ctl(VIRTUAL, CTL_ENABLE);
|
||||
break;
|
||||
case GUEST_STAGE_VTIMER_TVAL:
|
||||
timer_set_next_tval_ms(VIRTUAL, test_args.timer_period_ms);
|
||||
shared_data->xcnt = timer_get_cntct(VIRTUAL);
|
||||
timer_set_ctl(VIRTUAL, CTL_ENABLE);
|
||||
break;
|
||||
case GUEST_STAGE_PTIMER_CVAL:
|
||||
timer_set_next_cval_ms(PHYSICAL, test_args.timer_period_ms);
|
||||
shared_data->xcnt = timer_get_cntct(PHYSICAL);
|
||||
timer_set_ctl(PHYSICAL, CTL_ENABLE);
|
||||
break;
|
||||
case GUEST_STAGE_PTIMER_TVAL:
|
||||
timer_set_next_tval_ms(PHYSICAL, test_args.timer_period_ms);
|
||||
shared_data->xcnt = timer_get_cntct(PHYSICAL);
|
||||
timer_set_ctl(PHYSICAL, CTL_ENABLE);
|
||||
break;
|
||||
default:
|
||||
GUEST_ASSERT(0);
|
||||
}
|
||||
}
|
||||
|
||||
static void guest_validate_irq(unsigned int intid,
|
||||
struct test_vcpu_shared_data *shared_data)
|
||||
{
|
||||
enum guest_stage stage = shared_data->guest_stage;
|
||||
uint64_t xcnt = 0, xcnt_diff_us, cval = 0;
|
||||
unsigned long xctl = 0;
|
||||
unsigned int timer_irq = 0;
|
||||
|
||||
if (stage == GUEST_STAGE_VTIMER_CVAL ||
|
||||
stage == GUEST_STAGE_VTIMER_TVAL) {
|
||||
xctl = timer_get_ctl(VIRTUAL);
|
||||
timer_set_ctl(VIRTUAL, CTL_IMASK);
|
||||
xcnt = timer_get_cntct(VIRTUAL);
|
||||
cval = timer_get_cval(VIRTUAL);
|
||||
timer_irq = vtimer_irq;
|
||||
} else if (stage == GUEST_STAGE_PTIMER_CVAL ||
|
||||
stage == GUEST_STAGE_PTIMER_TVAL) {
|
||||
xctl = timer_get_ctl(PHYSICAL);
|
||||
timer_set_ctl(PHYSICAL, CTL_IMASK);
|
||||
xcnt = timer_get_cntct(PHYSICAL);
|
||||
cval = timer_get_cval(PHYSICAL);
|
||||
timer_irq = ptimer_irq;
|
||||
} else {
|
||||
GUEST_ASSERT(0);
|
||||
}
|
||||
|
||||
xcnt_diff_us = cycles_to_usec(xcnt - shared_data->xcnt);
|
||||
|
||||
/* Make sure we are dealing with the correct timer IRQ */
|
||||
GUEST_ASSERT_2(intid == timer_irq, intid, timer_irq);
|
||||
|
||||
/* Basic 'timer condition met' check */
|
||||
GUEST_ASSERT_3(xcnt >= cval, xcnt, cval, xcnt_diff_us);
|
||||
GUEST_ASSERT_1(xctl & CTL_ISTATUS, xctl);
|
||||
}
|
||||
|
||||
static void guest_irq_handler(struct ex_regs *regs)
|
||||
{
|
||||
unsigned int intid = gic_get_and_ack_irq();
|
||||
uint32_t cpu = guest_get_vcpuid();
|
||||
struct test_vcpu_shared_data *shared_data = &vcpu_shared_data[cpu];
|
||||
|
||||
guest_validate_irq(intid, shared_data);
|
||||
|
||||
WRITE_ONCE(shared_data->nr_iter, shared_data->nr_iter + 1);
|
||||
|
||||
gic_set_eoi(intid);
|
||||
}
|
||||
|
||||
static void guest_run_stage(struct test_vcpu_shared_data *shared_data,
|
||||
enum guest_stage stage)
|
||||
{
|
||||
uint32_t irq_iter, config_iter;
|
||||
|
||||
shared_data->guest_stage = stage;
|
||||
shared_data->nr_iter = 0;
|
||||
|
||||
for (config_iter = 0; config_iter < test_args.nr_iter; config_iter++) {
|
||||
/* Setup the next interrupt */
|
||||
guest_configure_timer_action(shared_data);
|
||||
|
||||
/* Setup a timeout for the interrupt to arrive */
|
||||
udelay(msecs_to_usecs(test_args.timer_period_ms) +
|
||||
TIMER_TEST_ERR_MARGIN_US);
|
||||
|
||||
irq_iter = READ_ONCE(shared_data->nr_iter);
|
||||
GUEST_ASSERT_2(config_iter + 1 == irq_iter,
|
||||
config_iter + 1, irq_iter);
|
||||
}
|
||||
}
|
||||
|
||||
static void guest_code(void)
|
||||
{
|
||||
uint32_t cpu = guest_get_vcpuid();
|
||||
struct test_vcpu_shared_data *shared_data = &vcpu_shared_data[cpu];
|
||||
|
||||
local_irq_disable();
|
||||
|
||||
gic_init(GIC_V3, test_args.nr_vcpus,
|
||||
(void *)GICD_BASE_GPA, (void *)GICR_BASE_GPA);
|
||||
|
||||
timer_set_ctl(VIRTUAL, CTL_IMASK);
|
||||
timer_set_ctl(PHYSICAL, CTL_IMASK);
|
||||
|
||||
gic_irq_enable(vtimer_irq);
|
||||
gic_irq_enable(ptimer_irq);
|
||||
local_irq_enable();
|
||||
|
||||
guest_run_stage(shared_data, GUEST_STAGE_VTIMER_CVAL);
|
||||
guest_run_stage(shared_data, GUEST_STAGE_VTIMER_TVAL);
|
||||
guest_run_stage(shared_data, GUEST_STAGE_PTIMER_CVAL);
|
||||
guest_run_stage(shared_data, GUEST_STAGE_PTIMER_TVAL);
|
||||
|
||||
GUEST_DONE();
|
||||
}
|
||||
|
||||
static void *test_vcpu_run(void *arg)
|
||||
{
|
||||
struct ucall uc;
|
||||
struct test_vcpu *vcpu = arg;
|
||||
struct kvm_vm *vm = vcpu->vm;
|
||||
uint32_t vcpuid = vcpu->vcpuid;
|
||||
struct test_vcpu_shared_data *shared_data = &vcpu_shared_data[vcpuid];
|
||||
|
||||
vcpu_run(vm, vcpuid);
|
||||
|
||||
/* Currently, any exit from guest is an indication of completion */
|
||||
pthread_mutex_lock(&vcpu_done_map_lock);
|
||||
set_bit(vcpuid, vcpu_done_map);
|
||||
pthread_mutex_unlock(&vcpu_done_map_lock);
|
||||
|
||||
switch (get_ucall(vm, vcpuid, &uc)) {
|
||||
case UCALL_SYNC:
|
||||
case UCALL_DONE:
|
||||
break;
|
||||
case UCALL_ABORT:
|
||||
sync_global_from_guest(vm, *shared_data);
|
||||
TEST_FAIL("%s at %s:%ld\n\tvalues: %lu, %lu; %lu, vcpu: %u; stage: %u; iter: %u",
|
||||
(const char *)uc.args[0], __FILE__, uc.args[1],
|
||||
uc.args[2], uc.args[3], uc.args[4], vcpuid,
|
||||
shared_data->guest_stage, shared_data->nr_iter);
|
||||
break;
|
||||
default:
|
||||
TEST_FAIL("Unexpected guest exit\n");
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static uint32_t test_get_pcpu(void)
|
||||
{
|
||||
uint32_t pcpu;
|
||||
unsigned int nproc_conf;
|
||||
cpu_set_t online_cpuset;
|
||||
|
||||
nproc_conf = get_nprocs_conf();
|
||||
sched_getaffinity(0, sizeof(cpu_set_t), &online_cpuset);
|
||||
|
||||
/* Randomly find an available pCPU to place a vCPU on */
|
||||
do {
|
||||
pcpu = rand() % nproc_conf;
|
||||
} while (!CPU_ISSET(pcpu, &online_cpuset));
|
||||
|
||||
return pcpu;
|
||||
}
|
||||
|
||||
static int test_migrate_vcpu(struct test_vcpu *vcpu)
|
||||
{
|
||||
int ret;
|
||||
cpu_set_t cpuset;
|
||||
uint32_t new_pcpu = test_get_pcpu();
|
||||
|
||||
CPU_ZERO(&cpuset);
|
||||
CPU_SET(new_pcpu, &cpuset);
|
||||
|
||||
pr_debug("Migrating vCPU: %u to pCPU: %u\n", vcpu->vcpuid, new_pcpu);
|
||||
|
||||
ret = pthread_setaffinity_np(vcpu->pt_vcpu_run,
|
||||
sizeof(cpuset), &cpuset);
|
||||
|
||||
/* Allow the error where the vCPU thread is already finished */
|
||||
TEST_ASSERT(ret == 0 || ret == ESRCH,
|
||||
"Failed to migrate the vCPU:%u to pCPU: %u; ret: %d\n",
|
||||
vcpu->vcpuid, new_pcpu, ret);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void *test_vcpu_migration(void *arg)
|
||||
{
|
||||
unsigned int i, n_done;
|
||||
bool vcpu_done;
|
||||
|
||||
do {
|
||||
usleep(msecs_to_usecs(test_args.migration_freq_ms));
|
||||
|
||||
for (n_done = 0, i = 0; i < test_args.nr_vcpus; i++) {
|
||||
pthread_mutex_lock(&vcpu_done_map_lock);
|
||||
vcpu_done = test_bit(i, vcpu_done_map);
|
||||
pthread_mutex_unlock(&vcpu_done_map_lock);
|
||||
|
||||
if (vcpu_done) {
|
||||
n_done++;
|
||||
continue;
|
||||
}
|
||||
|
||||
test_migrate_vcpu(&test_vcpu[i]);
|
||||
}
|
||||
} while (test_args.nr_vcpus != n_done);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void test_run(struct kvm_vm *vm)
|
||||
{
|
||||
int i, ret;
|
||||
pthread_t pt_vcpu_migration;
|
||||
|
||||
pthread_mutex_init(&vcpu_done_map_lock, NULL);
|
||||
vcpu_done_map = bitmap_zalloc(test_args.nr_vcpus);
|
||||
TEST_ASSERT(vcpu_done_map, "Failed to allocate vcpu done bitmap\n");
|
||||
|
||||
for (i = 0; i < test_args.nr_vcpus; i++) {
|
||||
ret = pthread_create(&test_vcpu[i].pt_vcpu_run, NULL,
|
||||
test_vcpu_run, &test_vcpu[i]);
|
||||
TEST_ASSERT(!ret, "Failed to create vCPU-%d pthread\n", i);
|
||||
}
|
||||
|
||||
/* Spawn a thread to control the vCPU migrations */
|
||||
if (test_args.migration_freq_ms) {
|
||||
srand(time(NULL));
|
||||
|
||||
ret = pthread_create(&pt_vcpu_migration, NULL,
|
||||
test_vcpu_migration, NULL);
|
||||
TEST_ASSERT(!ret, "Failed to create the migration pthread\n");
|
||||
}
|
||||
|
||||
|
||||
for (i = 0; i < test_args.nr_vcpus; i++)
|
||||
pthread_join(test_vcpu[i].pt_vcpu_run, NULL);
|
||||
|
||||
if (test_args.migration_freq_ms)
|
||||
pthread_join(pt_vcpu_migration, NULL);
|
||||
|
||||
bitmap_free(vcpu_done_map);
|
||||
}
|
||||
|
||||
static void test_init_timer_irq(struct kvm_vm *vm)
|
||||
{
|
||||
/* Timer initid should be same for all the vCPUs, so query only vCPU-0 */
|
||||
int vcpu0_fd = vcpu_get_fd(vm, 0);
|
||||
|
||||
kvm_device_access(vcpu0_fd, KVM_ARM_VCPU_TIMER_CTRL,
|
||||
KVM_ARM_VCPU_TIMER_IRQ_PTIMER, &ptimer_irq, false);
|
||||
kvm_device_access(vcpu0_fd, KVM_ARM_VCPU_TIMER_CTRL,
|
||||
KVM_ARM_VCPU_TIMER_IRQ_VTIMER, &vtimer_irq, false);
|
||||
|
||||
sync_global_to_guest(vm, ptimer_irq);
|
||||
sync_global_to_guest(vm, vtimer_irq);
|
||||
|
||||
pr_debug("ptimer_irq: %d; vtimer_irq: %d\n", ptimer_irq, vtimer_irq);
|
||||
}
|
||||
|
||||
static struct kvm_vm *test_vm_create(void)
|
||||
{
|
||||
struct kvm_vm *vm;
|
||||
unsigned int i;
|
||||
int nr_vcpus = test_args.nr_vcpus;
|
||||
|
||||
vm = vm_create_default_with_vcpus(nr_vcpus, 0, 0, guest_code, NULL);
|
||||
|
||||
vm_init_descriptor_tables(vm);
|
||||
vm_install_exception_handler(vm, VECTOR_IRQ_CURRENT, guest_irq_handler);
|
||||
|
||||
for (i = 0; i < nr_vcpus; i++) {
|
||||
vcpu_init_descriptor_tables(vm, i);
|
||||
|
||||
test_vcpu[i].vcpuid = i;
|
||||
test_vcpu[i].vm = vm;
|
||||
}
|
||||
|
||||
ucall_init(vm, NULL);
|
||||
test_init_timer_irq(vm);
|
||||
vgic_v3_setup(vm, nr_vcpus, GICD_BASE_GPA, GICR_BASE_GPA);
|
||||
|
||||
/* Make all the test's cmdline args visible to the guest */
|
||||
sync_global_to_guest(vm, test_args);
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
static void test_print_help(char *name)
|
||||
{
|
||||
pr_info("Usage: %s [-h] [-n nr_vcpus] [-i iterations] [-p timer_period_ms]\n",
|
||||
name);
|
||||
pr_info("\t-n: Number of vCPUs to configure (default: %u; max: %u)\n",
|
||||
NR_VCPUS_DEF, KVM_MAX_VCPUS);
|
||||
pr_info("\t-i: Number of iterations per stage (default: %u)\n",
|
||||
NR_TEST_ITERS_DEF);
|
||||
pr_info("\t-p: Periodicity (in ms) of the guest timer (default: %u)\n",
|
||||
TIMER_TEST_PERIOD_MS_DEF);
|
||||
pr_info("\t-m: Frequency (in ms) of vCPUs to migrate to different pCPU. 0 to turn off (default: %u)\n",
|
||||
TIMER_TEST_MIGRATION_FREQ_MS);
|
||||
pr_info("\t-h: print this help screen\n");
|
||||
}
|
||||
|
||||
static bool parse_args(int argc, char *argv[])
|
||||
{
|
||||
int opt;
|
||||
|
||||
while ((opt = getopt(argc, argv, "hn:i:p:m:")) != -1) {
|
||||
switch (opt) {
|
||||
case 'n':
|
||||
test_args.nr_vcpus = atoi(optarg);
|
||||
if (test_args.nr_vcpus <= 0) {
|
||||
pr_info("Positive value needed for -n\n");
|
||||
goto err;
|
||||
} else if (test_args.nr_vcpus > KVM_MAX_VCPUS) {
|
||||
pr_info("Max allowed vCPUs: %u\n",
|
||||
KVM_MAX_VCPUS);
|
||||
goto err;
|
||||
}
|
||||
break;
|
||||
case 'i':
|
||||
test_args.nr_iter = atoi(optarg);
|
||||
if (test_args.nr_iter <= 0) {
|
||||
pr_info("Positive value needed for -i\n");
|
||||
goto err;
|
||||
}
|
||||
break;
|
||||
case 'p':
|
||||
test_args.timer_period_ms = atoi(optarg);
|
||||
if (test_args.timer_period_ms <= 0) {
|
||||
pr_info("Positive value needed for -p\n");
|
||||
goto err;
|
||||
}
|
||||
break;
|
||||
case 'm':
|
||||
test_args.migration_freq_ms = atoi(optarg);
|
||||
if (test_args.migration_freq_ms < 0) {
|
||||
pr_info("0 or positive value needed for -m\n");
|
||||
goto err;
|
||||
}
|
||||
break;
|
||||
case 'h':
|
||||
default:
|
||||
goto err;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
err:
|
||||
test_print_help(argv[0]);
|
||||
return false;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
struct kvm_vm *vm;
|
||||
|
||||
/* Tell stdout not to buffer its content */
|
||||
setbuf(stdout, NULL);
|
||||
|
||||
if (!parse_args(argc, argv))
|
||||
exit(KSFT_SKIP);
|
||||
|
||||
if (test_args.migration_freq_ms && get_nprocs() < 2) {
|
||||
print_skip("At least two physical CPUs needed for vCPU migration");
|
||||
exit(KSFT_SKIP);
|
||||
}
|
||||
|
||||
vm = test_vm_create();
|
||||
test_run(vm);
|
||||
kvm_vm_free(vm);
|
||||
|
||||
return 0;
|
||||
}
|
|
@ -34,16 +34,16 @@ static void reset_debug_state(void)
|
|||
{
|
||||
asm volatile("msr daifset, #8");
|
||||
|
||||
write_sysreg(osdlr_el1, 0);
|
||||
write_sysreg(oslar_el1, 0);
|
||||
write_sysreg(0, osdlr_el1);
|
||||
write_sysreg(0, oslar_el1);
|
||||
isb();
|
||||
|
||||
write_sysreg(mdscr_el1, 0);
|
||||
write_sysreg(0, mdscr_el1);
|
||||
/* This test only uses the first bp and wp slot. */
|
||||
write_sysreg(dbgbvr0_el1, 0);
|
||||
write_sysreg(dbgbcr0_el1, 0);
|
||||
write_sysreg(dbgwcr0_el1, 0);
|
||||
write_sysreg(dbgwvr0_el1, 0);
|
||||
write_sysreg(0, dbgbvr0_el1);
|
||||
write_sysreg(0, dbgbcr0_el1);
|
||||
write_sysreg(0, dbgwcr0_el1);
|
||||
write_sysreg(0, dbgwvr0_el1);
|
||||
isb();
|
||||
}
|
||||
|
||||
|
@ -53,14 +53,14 @@ static void install_wp(uint64_t addr)
|
|||
uint32_t mdscr;
|
||||
|
||||
wcr = DBGWCR_LEN8 | DBGWCR_RD | DBGWCR_WR | DBGWCR_EL1 | DBGWCR_E;
|
||||
write_sysreg(dbgwcr0_el1, wcr);
|
||||
write_sysreg(dbgwvr0_el1, addr);
|
||||
write_sysreg(wcr, dbgwcr0_el1);
|
||||
write_sysreg(addr, dbgwvr0_el1);
|
||||
isb();
|
||||
|
||||
asm volatile("msr daifclr, #8");
|
||||
|
||||
mdscr = read_sysreg(mdscr_el1) | MDSCR_KDE | MDSCR_MDE;
|
||||
write_sysreg(mdscr_el1, mdscr);
|
||||
write_sysreg(mdscr, mdscr_el1);
|
||||
isb();
|
||||
}
|
||||
|
||||
|
@ -70,14 +70,14 @@ static void install_hw_bp(uint64_t addr)
|
|||
uint32_t mdscr;
|
||||
|
||||
bcr = DBGBCR_LEN8 | DBGBCR_EXEC | DBGBCR_EL1 | DBGBCR_E;
|
||||
write_sysreg(dbgbcr0_el1, bcr);
|
||||
write_sysreg(dbgbvr0_el1, addr);
|
||||
write_sysreg(bcr, dbgbcr0_el1);
|
||||
write_sysreg(addr, dbgbvr0_el1);
|
||||
isb();
|
||||
|
||||
asm volatile("msr daifclr, #8");
|
||||
|
||||
mdscr = read_sysreg(mdscr_el1) | MDSCR_KDE | MDSCR_MDE;
|
||||
write_sysreg(mdscr_el1, mdscr);
|
||||
write_sysreg(mdscr, mdscr_el1);
|
||||
isb();
|
||||
}
|
||||
|
||||
|
@ -88,7 +88,7 @@ static void install_ss(void)
|
|||
asm volatile("msr daifclr, #8");
|
||||
|
||||
mdscr = read_sysreg(mdscr_el1) | MDSCR_KDE | MDSCR_SS;
|
||||
write_sysreg(mdscr_el1, mdscr);
|
||||
write_sysreg(mdscr, mdscr_el1);
|
||||
isb();
|
||||
}
|
||||
|
||||
|
@ -190,7 +190,7 @@ static int debug_version(struct kvm_vm *vm)
|
|||
{
|
||||
uint64_t id_aa64dfr0;
|
||||
|
||||
get_reg(vm, VCPU_ID, ARM64_SYS_REG(ID_AA64DFR0_EL1), &id_aa64dfr0);
|
||||
get_reg(vm, VCPU_ID, KVM_ARM64_SYS_REG(SYS_ID_AA64DFR0_EL1), &id_aa64dfr0);
|
||||
return id_aa64dfr0 & 0xf;
|
||||
}
|
||||
|
||||
|
|
|
@ -91,7 +91,7 @@ int main(void)
|
|||
init.features[0] |= (1 << KVM_ARM_VCPU_POWER_OFF);
|
||||
aarch64_vcpu_add_default(vm, VCPU_ID_TARGET, &init, guest_main);
|
||||
|
||||
get_reg(vm, VCPU_ID_TARGET, ARM64_SYS_REG(MPIDR_EL1), &target_mpidr);
|
||||
get_reg(vm, VCPU_ID_TARGET, KVM_ARM64_SYS_REG(SYS_MPIDR_EL1), &target_mpidr);
|
||||
vcpu_args_set(vm, VCPU_ID_SOURCE, 1, target_mpidr & MPIDR_HWID_BITMASK);
|
||||
vcpu_run(vm, VCPU_ID_SOURCE);
|
||||
|
||||
|
|
|
@ -13,11 +13,10 @@
|
|||
#include "test_util.h"
|
||||
#include "kvm_util.h"
|
||||
#include "processor.h"
|
||||
#include "vgic.h"
|
||||
|
||||
#define NR_VCPUS 4
|
||||
|
||||
#define REDIST_REGION_ATTR_ADDR(count, base, flags, index) (((uint64_t)(count) << 52) | \
|
||||
((uint64_t)((base) >> 16) << 16) | ((uint64_t)(flags) << 12) | index)
|
||||
#define REG_OFFSET(vcpu, offset) (((uint64_t)vcpu << 32) | offset)
|
||||
|
||||
#define GICR_TYPER 0x8
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0 */
|
||||
/*
|
||||
* ARM Generic Timer specific interface
|
||||
*/
|
||||
|
||||
#ifndef SELFTEST_KVM_ARCH_TIMER_H
|
||||
#define SELFTEST_KVM_ARCH_TIMER_H
|
||||
|
||||
#include "processor.h"
|
||||
|
||||
enum arch_timer {
|
||||
VIRTUAL,
|
||||
PHYSICAL,
|
||||
};
|
||||
|
||||
#define CTL_ENABLE (1 << 0)
|
||||
#define CTL_IMASK (1 << 1)
|
||||
#define CTL_ISTATUS (1 << 2)
|
||||
|
||||
#define msec_to_cycles(msec) \
|
||||
(timer_get_cntfrq() * (uint64_t)(msec) / 1000)
|
||||
|
||||
#define usec_to_cycles(usec) \
|
||||
(timer_get_cntfrq() * (uint64_t)(usec) / 1000000)
|
||||
|
||||
#define cycles_to_usec(cycles) \
|
||||
((uint64_t)(cycles) * 1000000 / timer_get_cntfrq())
|
||||
|
||||
static inline uint32_t timer_get_cntfrq(void)
|
||||
{
|
||||
return read_sysreg(cntfrq_el0);
|
||||
}
|
||||
|
||||
static inline uint64_t timer_get_cntct(enum arch_timer timer)
|
||||
{
|
||||
isb();
|
||||
|
||||
switch (timer) {
|
||||
case VIRTUAL:
|
||||
return read_sysreg(cntvct_el0);
|
||||
case PHYSICAL:
|
||||
return read_sysreg(cntpct_el0);
|
||||
default:
|
||||
GUEST_ASSERT_1(0, timer);
|
||||
}
|
||||
|
||||
/* We should not reach here */
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline void timer_set_cval(enum arch_timer timer, uint64_t cval)
|
||||
{
|
||||
switch (timer) {
|
||||
case VIRTUAL:
|
||||
write_sysreg(cval, cntv_cval_el0);
|
||||
break;
|
||||
case PHYSICAL:
|
||||
write_sysreg(cval, cntp_cval_el0);
|
||||
break;
|
||||
default:
|
||||
GUEST_ASSERT_1(0, timer);
|
||||
}
|
||||
|
||||
isb();
|
||||
}
|
||||
|
||||
static inline uint64_t timer_get_cval(enum arch_timer timer)
|
||||
{
|
||||
switch (timer) {
|
||||
case VIRTUAL:
|
||||
return read_sysreg(cntv_cval_el0);
|
||||
case PHYSICAL:
|
||||
return read_sysreg(cntp_cval_el0);
|
||||
default:
|
||||
GUEST_ASSERT_1(0, timer);
|
||||
}
|
||||
|
||||
/* We should not reach here */
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline void timer_set_tval(enum arch_timer timer, uint32_t tval)
|
||||
{
|
||||
switch (timer) {
|
||||
case VIRTUAL:
|
||||
write_sysreg(tval, cntv_tval_el0);
|
||||
break;
|
||||
case PHYSICAL:
|
||||
write_sysreg(tval, cntp_tval_el0);
|
||||
break;
|
||||
default:
|
||||
GUEST_ASSERT_1(0, timer);
|
||||
}
|
||||
|
||||
isb();
|
||||
}
|
||||
|
||||
static inline void timer_set_ctl(enum arch_timer timer, uint32_t ctl)
|
||||
{
|
||||
switch (timer) {
|
||||
case VIRTUAL:
|
||||
write_sysreg(ctl, cntv_ctl_el0);
|
||||
break;
|
||||
case PHYSICAL:
|
||||
write_sysreg(ctl, cntp_ctl_el0);
|
||||
break;
|
||||
default:
|
||||
GUEST_ASSERT_1(0, timer);
|
||||
}
|
||||
|
||||
isb();
|
||||
}
|
||||
|
||||
static inline uint32_t timer_get_ctl(enum arch_timer timer)
|
||||
{
|
||||
switch (timer) {
|
||||
case VIRTUAL:
|
||||
return read_sysreg(cntv_ctl_el0);
|
||||
case PHYSICAL:
|
||||
return read_sysreg(cntp_ctl_el0);
|
||||
default:
|
||||
GUEST_ASSERT_1(0, timer);
|
||||
}
|
||||
|
||||
/* We should not reach here */
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline void timer_set_next_cval_ms(enum arch_timer timer, uint32_t msec)
|
||||
{
|
||||
uint64_t now_ct = timer_get_cntct(timer);
|
||||
uint64_t next_ct = now_ct + msec_to_cycles(msec);
|
||||
|
||||
timer_set_cval(timer, next_ct);
|
||||
}
|
||||
|
||||
static inline void timer_set_next_tval_ms(enum arch_timer timer, uint32_t msec)
|
||||
{
|
||||
timer_set_tval(timer, msec_to_cycles(msec));
|
||||
}
|
||||
|
||||
#endif /* SELFTEST_KVM_ARCH_TIMER_H */
|
|
@ -0,0 +1,25 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0 */
|
||||
/*
|
||||
* ARM simple delay routines
|
||||
*/
|
||||
|
||||
#ifndef SELFTEST_KVM_ARM_DELAY_H
|
||||
#define SELFTEST_KVM_ARM_DELAY_H
|
||||
|
||||
#include "arch_timer.h"
|
||||
|
||||
static inline void __delay(uint64_t cycles)
|
||||
{
|
||||
enum arch_timer timer = VIRTUAL;
|
||||
uint64_t start = timer_get_cntct(timer);
|
||||
|
||||
while ((timer_get_cntct(timer) - start) < cycles)
|
||||
cpu_relax();
|
||||
}
|
||||
|
||||
static inline void udelay(unsigned long usec)
|
||||
{
|
||||
__delay(usec_to_cycles(usec));
|
||||
}
|
||||
|
||||
#endif /* SELFTEST_KVM_ARM_DELAY_H */
|
|
@ -0,0 +1,21 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0 */
|
||||
/*
|
||||
* ARM Generic Interrupt Controller (GIC) specific defines
|
||||
*/
|
||||
|
||||
#ifndef SELFTEST_KVM_GIC_H
|
||||
#define SELFTEST_KVM_GIC_H
|
||||
|
||||
enum gic_type {
|
||||
GIC_V3,
|
||||
GIC_TYPE_MAX,
|
||||
};
|
||||
|
||||
void gic_init(enum gic_type type, unsigned int nr_cpus,
|
||||
void *dist_base, void *redist_base);
|
||||
void gic_irq_enable(unsigned int intid);
|
||||
void gic_irq_disable(unsigned int intid);
|
||||
unsigned int gic_get_and_ack_irq(void);
|
||||
void gic_set_eoi(unsigned int intid);
|
||||
|
||||
#endif /* SELFTEST_KVM_GIC_H */
|
|
@ -9,20 +9,24 @@
|
|||
|
||||
#include "kvm_util.h"
|
||||
#include <linux/stringify.h>
|
||||
#include <linux/types.h>
|
||||
#include <asm/sysreg.h>
|
||||
|
||||
|
||||
#define ARM64_CORE_REG(x) (KVM_REG_ARM64 | KVM_REG_SIZE_U64 | \
|
||||
KVM_REG_ARM_CORE | KVM_REG_ARM_CORE_REG(x))
|
||||
|
||||
#define CPACR_EL1 3, 0, 1, 0, 2
|
||||
#define TCR_EL1 3, 0, 2, 0, 2
|
||||
#define MAIR_EL1 3, 0, 10, 2, 0
|
||||
#define MPIDR_EL1 3, 0, 0, 0, 5
|
||||
#define TTBR0_EL1 3, 0, 2, 0, 0
|
||||
#define SCTLR_EL1 3, 0, 1, 0, 0
|
||||
#define VBAR_EL1 3, 0, 12, 0, 0
|
||||
|
||||
#define ID_AA64DFR0_EL1 3, 0, 0, 5, 0
|
||||
/*
|
||||
* KVM_ARM64_SYS_REG(sys_reg_id): Helper macro to convert
|
||||
* SYS_* register definitions in asm/sysreg.h to use in KVM
|
||||
* calls such as get_reg() and set_reg().
|
||||
*/
|
||||
#define KVM_ARM64_SYS_REG(sys_reg_id) \
|
||||
ARM64_SYS_REG(sys_reg_Op0(sys_reg_id), \
|
||||
sys_reg_Op1(sys_reg_id), \
|
||||
sys_reg_CRn(sys_reg_id), \
|
||||
sys_reg_CRm(sys_reg_id), \
|
||||
sys_reg_Op2(sys_reg_id))
|
||||
|
||||
/*
|
||||
* Default MAIR
|
||||
|
@ -59,7 +63,7 @@ static inline void set_reg(struct kvm_vm *vm, uint32_t vcpuid, uint64_t id, uint
|
|||
vcpu_ioctl(vm, vcpuid, KVM_SET_ONE_REG, ®);
|
||||
}
|
||||
|
||||
void aarch64_vcpu_setup(struct kvm_vm *vm, int vcpuid, struct kvm_vcpu_init *init);
|
||||
void aarch64_vcpu_setup(struct kvm_vm *vm, uint32_t vcpuid, struct kvm_vcpu_init *init);
|
||||
void aarch64_vcpu_add_default(struct kvm_vm *vm, uint32_t vcpuid,
|
||||
struct kvm_vcpu_init *init, void *guest_code);
|
||||
|
||||
|
@ -118,18 +122,64 @@ void vm_install_exception_handler(struct kvm_vm *vm,
|
|||
void vm_install_sync_handler(struct kvm_vm *vm,
|
||||
int vector, int ec, handler_fn handler);
|
||||
|
||||
#define write_sysreg(reg, val) \
|
||||
({ \
|
||||
u64 __val = (u64)(val); \
|
||||
asm volatile("msr " __stringify(reg) ", %x0" : : "rZ" (__val)); \
|
||||
})
|
||||
|
||||
#define read_sysreg(reg) \
|
||||
({ u64 val; \
|
||||
asm volatile("mrs %0, "__stringify(reg) : "=r"(val) : : "memory");\
|
||||
val; \
|
||||
})
|
||||
static inline void cpu_relax(void)
|
||||
{
|
||||
asm volatile("yield" ::: "memory");
|
||||
}
|
||||
|
||||
#define isb() asm volatile("isb" : : : "memory")
|
||||
#define dsb(opt) asm volatile("dsb " #opt : : : "memory")
|
||||
#define dmb(opt) asm volatile("dmb " #opt : : : "memory")
|
||||
|
||||
#define dma_wmb() dmb(oshst)
|
||||
#define __iowmb() dma_wmb()
|
||||
|
||||
#define dma_rmb() dmb(oshld)
|
||||
|
||||
#define __iormb(v) \
|
||||
({ \
|
||||
unsigned long tmp; \
|
||||
\
|
||||
dma_rmb(); \
|
||||
\
|
||||
/* \
|
||||
* Courtesy of arch/arm64/include/asm/io.h: \
|
||||
* Create a dummy control dependency from the IO read to any \
|
||||
* later instructions. This ensures that a subsequent call \
|
||||
* to udelay() will be ordered due to the ISB in __delay(). \
|
||||
*/ \
|
||||
asm volatile("eor %0, %1, %1\n" \
|
||||
"cbnz %0, ." \
|
||||
: "=r" (tmp) : "r" ((unsigned long)(v)) \
|
||||
: "memory"); \
|
||||
})
|
||||
|
||||
static __always_inline void __raw_writel(u32 val, volatile void *addr)
|
||||
{
|
||||
asm volatile("str %w0, [%1]" : : "rZ" (val), "r" (addr));
|
||||
}
|
||||
|
||||
static __always_inline u32 __raw_readl(const volatile void *addr)
|
||||
{
|
||||
u32 val;
|
||||
asm volatile("ldr %w0, [%1]" : "=r" (val) : "r" (addr));
|
||||
return val;
|
||||
}
|
||||
|
||||
#define writel_relaxed(v,c) ((void)__raw_writel((__force u32)cpu_to_le32(v),(c)))
|
||||
#define readl_relaxed(c) ({ u32 __r = le32_to_cpu((__force __le32)__raw_readl(c)); __r; })
|
||||
|
||||
#define writel(v,c) ({ __iowmb(); writel_relaxed((v),(c));})
|
||||
#define readl(c) ({ u32 __v = readl_relaxed(c); __iormb(__v); __v; })
|
||||
|
||||
static inline void local_irq_enable(void)
|
||||
{
|
||||
asm volatile("msr daifclr, #3" : : : "memory");
|
||||
}
|
||||
|
||||
static inline void local_irq_disable(void)
|
||||
{
|
||||
asm volatile("msr daifset, #3" : : : "memory");
|
||||
}
|
||||
|
||||
#endif /* SELFTEST_KVM_PROCESSOR_H */
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0 */
|
||||
|
||||
#ifndef SELFTEST_KVM_ARM64_SPINLOCK_H
|
||||
#define SELFTEST_KVM_ARM64_SPINLOCK_H
|
||||
|
||||
struct spinlock {
|
||||
int v;
|
||||
};
|
||||
|
||||
extern void spin_lock(struct spinlock *lock);
|
||||
extern void spin_unlock(struct spinlock *lock);
|
||||
|
||||
#endif /* SELFTEST_KVM_ARM64_SPINLOCK_H */
|
|
@ -0,0 +1,20 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0 */
|
||||
/*
|
||||
* ARM Generic Interrupt Controller (GIC) host specific defines
|
||||
*/
|
||||
|
||||
#ifndef SELFTEST_KVM_VGIC_H
|
||||
#define SELFTEST_KVM_VGIC_H
|
||||
|
||||
#include <linux/kvm.h>
|
||||
|
||||
#define REDIST_REGION_ATTR_ADDR(count, base, flags, index) \
|
||||
(((uint64_t)(count) << 52) | \
|
||||
((uint64_t)((base) >> 16) << 16) | \
|
||||
((uint64_t)(flags) << 12) | \
|
||||
index)
|
||||
|
||||
int vgic_v3_setup(struct kvm_vm *vm, unsigned int nr_vcpus,
|
||||
uint64_t gicd_base_gpa, uint64_t gicr_base_gpa);
|
||||
|
||||
#endif /* SELFTEST_KVM_VGIC_H */
|
|
@ -400,4 +400,6 @@ uint64_t get_ucall(struct kvm_vm *vm, uint32_t vcpu_id, struct ucall *uc);
|
|||
int vm_get_stats_fd(struct kvm_vm *vm);
|
||||
int vcpu_get_stats_fd(struct kvm_vm *vm, uint32_t vcpuid);
|
||||
|
||||
uint32_t guest_get_vcpuid(void);
|
||||
|
||||
#endif /* SELFTEST_KVM_UTIL_H */
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* ARM Generic Interrupt Controller (GIC) support
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <linux/bits.h>
|
||||
#include <linux/sizes.h>
|
||||
|
||||
#include "kvm_util.h"
|
||||
|
||||
#include <gic.h>
|
||||
#include "gic_private.h"
|
||||
#include "processor.h"
|
||||
#include "spinlock.h"
|
||||
|
||||
static const struct gic_common_ops *gic_common_ops;
|
||||
static struct spinlock gic_lock;
|
||||
|
||||
static void gic_cpu_init(unsigned int cpu, void *redist_base)
|
||||
{
|
||||
gic_common_ops->gic_cpu_init(cpu, redist_base);
|
||||
}
|
||||
|
||||
static void
|
||||
gic_dist_init(enum gic_type type, unsigned int nr_cpus, void *dist_base)
|
||||
{
|
||||
const struct gic_common_ops *gic_ops = NULL;
|
||||
|
||||
spin_lock(&gic_lock);
|
||||
|
||||
/* Distributor initialization is needed only once per VM */
|
||||
if (gic_common_ops) {
|
||||
spin_unlock(&gic_lock);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == GIC_V3)
|
||||
gic_ops = &gicv3_ops;
|
||||
|
||||
GUEST_ASSERT(gic_ops);
|
||||
|
||||
gic_ops->gic_init(nr_cpus, dist_base);
|
||||
gic_common_ops = gic_ops;
|
||||
|
||||
/* Make sure that the initialized data is visible to all the vCPUs */
|
||||
dsb(sy);
|
||||
|
||||
spin_unlock(&gic_lock);
|
||||
}
|
||||
|
||||
void gic_init(enum gic_type type, unsigned int nr_cpus,
|
||||
void *dist_base, void *redist_base)
|
||||
{
|
||||
uint32_t cpu = guest_get_vcpuid();
|
||||
|
||||
GUEST_ASSERT(type < GIC_TYPE_MAX);
|
||||
GUEST_ASSERT(dist_base);
|
||||
GUEST_ASSERT(redist_base);
|
||||
GUEST_ASSERT(nr_cpus);
|
||||
|
||||
gic_dist_init(type, nr_cpus, dist_base);
|
||||
gic_cpu_init(cpu, redist_base);
|
||||
}
|
||||
|
||||
void gic_irq_enable(unsigned int intid)
|
||||
{
|
||||
GUEST_ASSERT(gic_common_ops);
|
||||
gic_common_ops->gic_irq_enable(intid);
|
||||
}
|
||||
|
||||
void gic_irq_disable(unsigned int intid)
|
||||
{
|
||||
GUEST_ASSERT(gic_common_ops);
|
||||
gic_common_ops->gic_irq_disable(intid);
|
||||
}
|
||||
|
||||
unsigned int gic_get_and_ack_irq(void)
|
||||
{
|
||||
uint64_t irqstat;
|
||||
unsigned int intid;
|
||||
|
||||
GUEST_ASSERT(gic_common_ops);
|
||||
|
||||
irqstat = gic_common_ops->gic_read_iar();
|
||||
intid = irqstat & GENMASK(23, 0);
|
||||
|
||||
return intid;
|
||||
}
|
||||
|
||||
void gic_set_eoi(unsigned int intid)
|
||||
{
|
||||
GUEST_ASSERT(gic_common_ops);
|
||||
gic_common_ops->gic_write_eoir(intid);
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0 */
|
||||
/*
|
||||
* ARM Generic Interrupt Controller (GIC) private defines that's only
|
||||
* shared among the GIC library code.
|
||||
*/
|
||||
|
||||
#ifndef SELFTEST_KVM_GIC_PRIVATE_H
|
||||
#define SELFTEST_KVM_GIC_PRIVATE_H
|
||||
|
||||
struct gic_common_ops {
|
||||
void (*gic_init)(unsigned int nr_cpus, void *dist_base);
|
||||
void (*gic_cpu_init)(unsigned int cpu, void *redist_base);
|
||||
void (*gic_irq_enable)(unsigned int intid);
|
||||
void (*gic_irq_disable)(unsigned int intid);
|
||||
uint64_t (*gic_read_iar)(void);
|
||||
void (*gic_write_eoir)(uint32_t irq);
|
||||
};
|
||||
|
||||
extern const struct gic_common_ops gicv3_ops;
|
||||
|
||||
#endif /* SELFTEST_KVM_GIC_PRIVATE_H */
|
|
@ -0,0 +1,240 @@
|
|||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* ARM Generic Interrupt Controller (GIC) v3 support
|
||||
*/
|
||||
|
||||
#include <linux/sizes.h>
|
||||
|
||||
#include "kvm_util.h"
|
||||
#include "processor.h"
|
||||
#include "delay.h"
|
||||
|
||||
#include "gic_v3.h"
|
||||
#include "gic_private.h"
|
||||
|
||||
struct gicv3_data {
|
||||
void *dist_base;
|
||||
void *redist_base[GICV3_MAX_CPUS];
|
||||
unsigned int nr_cpus;
|
||||
unsigned int nr_spis;
|
||||
};
|
||||
|
||||
#define sgi_base_from_redist(redist_base) (redist_base + SZ_64K)
|
||||
|
||||
enum gicv3_intid_range {
|
||||
SGI_RANGE,
|
||||
PPI_RANGE,
|
||||
SPI_RANGE,
|
||||
INVALID_RANGE,
|
||||
};
|
||||
|
||||
static struct gicv3_data gicv3_data;
|
||||
|
||||
static void gicv3_gicd_wait_for_rwp(void)
|
||||
{
|
||||
unsigned int count = 100000; /* 1s */
|
||||
|
||||
while (readl(gicv3_data.dist_base + GICD_CTLR) & GICD_CTLR_RWP) {
|
||||
GUEST_ASSERT(count--);
|
||||
udelay(10);
|
||||
}
|
||||
}
|
||||
|
||||
static void gicv3_gicr_wait_for_rwp(void *redist_base)
|
||||
{
|
||||
unsigned int count = 100000; /* 1s */
|
||||
|
||||
while (readl(redist_base + GICR_CTLR) & GICR_CTLR_RWP) {
|
||||
GUEST_ASSERT(count--);
|
||||
udelay(10);
|
||||
}
|
||||
}
|
||||
|
||||
static enum gicv3_intid_range get_intid_range(unsigned int intid)
|
||||
{
|
||||
switch (intid) {
|
||||
case 0 ... 15:
|
||||
return SGI_RANGE;
|
||||
case 16 ... 31:
|
||||
return PPI_RANGE;
|
||||
case 32 ... 1019:
|
||||
return SPI_RANGE;
|
||||
}
|
||||
|
||||
/* We should not be reaching here */
|
||||
GUEST_ASSERT(0);
|
||||
|
||||
return INVALID_RANGE;
|
||||
}
|
||||
|
||||
static uint64_t gicv3_read_iar(void)
|
||||
{
|
||||
uint64_t irqstat = read_sysreg_s(SYS_ICC_IAR1_EL1);
|
||||
|
||||
dsb(sy);
|
||||
return irqstat;
|
||||
}
|
||||
|
||||
static void gicv3_write_eoir(uint32_t irq)
|
||||
{
|
||||
write_sysreg_s(irq, SYS_ICC_EOIR1_EL1);
|
||||
isb();
|
||||
}
|
||||
|
||||
static void
|
||||
gicv3_config_irq(unsigned int intid, unsigned int offset)
|
||||
{
|
||||
uint32_t cpu = guest_get_vcpuid();
|
||||
uint32_t mask = 1 << (intid % 32);
|
||||
enum gicv3_intid_range intid_range = get_intid_range(intid);
|
||||
void *reg;
|
||||
|
||||
/* We care about 'cpu' only for SGIs or PPIs */
|
||||
if (intid_range == SGI_RANGE || intid_range == PPI_RANGE) {
|
||||
GUEST_ASSERT(cpu < gicv3_data.nr_cpus);
|
||||
|
||||
reg = sgi_base_from_redist(gicv3_data.redist_base[cpu]) +
|
||||
offset;
|
||||
writel(mask, reg);
|
||||
gicv3_gicr_wait_for_rwp(gicv3_data.redist_base[cpu]);
|
||||
} else if (intid_range == SPI_RANGE) {
|
||||
reg = gicv3_data.dist_base + offset + (intid / 32) * 4;
|
||||
writel(mask, reg);
|
||||
gicv3_gicd_wait_for_rwp();
|
||||
} else {
|
||||
GUEST_ASSERT(0);
|
||||
}
|
||||
}
|
||||
|
||||
static void gicv3_irq_enable(unsigned int intid)
|
||||
{
|
||||
gicv3_config_irq(intid, GICD_ISENABLER);
|
||||
}
|
||||
|
||||
static void gicv3_irq_disable(unsigned int intid)
|
||||
{
|
||||
gicv3_config_irq(intid, GICD_ICENABLER);
|
||||
}
|
||||
|
||||
static void gicv3_enable_redist(void *redist_base)
|
||||
{
|
||||
uint32_t val = readl(redist_base + GICR_WAKER);
|
||||
unsigned int count = 100000; /* 1s */
|
||||
|
||||
val &= ~GICR_WAKER_ProcessorSleep;
|
||||
writel(val, redist_base + GICR_WAKER);
|
||||
|
||||
/* Wait until the processor is 'active' */
|
||||
while (readl(redist_base + GICR_WAKER) & GICR_WAKER_ChildrenAsleep) {
|
||||
GUEST_ASSERT(count--);
|
||||
udelay(10);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void *gicr_base_cpu(void *redist_base, uint32_t cpu)
|
||||
{
|
||||
/* Align all the redistributors sequentially */
|
||||
return redist_base + cpu * SZ_64K * 2;
|
||||
}
|
||||
|
||||
static void gicv3_cpu_init(unsigned int cpu, void *redist_base)
|
||||
{
|
||||
void *sgi_base;
|
||||
unsigned int i;
|
||||
void *redist_base_cpu;
|
||||
|
||||
GUEST_ASSERT(cpu < gicv3_data.nr_cpus);
|
||||
|
||||
redist_base_cpu = gicr_base_cpu(redist_base, cpu);
|
||||
sgi_base = sgi_base_from_redist(redist_base_cpu);
|
||||
|
||||
gicv3_enable_redist(redist_base_cpu);
|
||||
|
||||
/*
|
||||
* Mark all the SGI and PPI interrupts as non-secure Group-1.
|
||||
* Also, deactivate and disable them.
|
||||
*/
|
||||
writel(~0, sgi_base + GICR_IGROUPR0);
|
||||
writel(~0, sgi_base + GICR_ICACTIVER0);
|
||||
writel(~0, sgi_base + GICR_ICENABLER0);
|
||||
|
||||
/* Set a default priority for all the SGIs and PPIs */
|
||||
for (i = 0; i < 32; i += 4)
|
||||
writel(GICD_INT_DEF_PRI_X4,
|
||||
sgi_base + GICR_IPRIORITYR0 + i);
|
||||
|
||||
gicv3_gicr_wait_for_rwp(redist_base_cpu);
|
||||
|
||||
/* Enable the GIC system register (ICC_*) access */
|
||||
write_sysreg_s(read_sysreg_s(SYS_ICC_SRE_EL1) | ICC_SRE_EL1_SRE,
|
||||
SYS_ICC_SRE_EL1);
|
||||
|
||||
/* Set a default priority threshold */
|
||||
write_sysreg_s(ICC_PMR_DEF_PRIO, SYS_ICC_PMR_EL1);
|
||||
|
||||
/* Enable non-secure Group-1 interrupts */
|
||||
write_sysreg_s(ICC_IGRPEN1_EL1_ENABLE, SYS_ICC_GRPEN1_EL1);
|
||||
|
||||
gicv3_data.redist_base[cpu] = redist_base_cpu;
|
||||
}
|
||||
|
||||
static void gicv3_dist_init(void)
|
||||
{
|
||||
void *dist_base = gicv3_data.dist_base;
|
||||
unsigned int i;
|
||||
|
||||
/* Disable the distributor until we set things up */
|
||||
writel(0, dist_base + GICD_CTLR);
|
||||
gicv3_gicd_wait_for_rwp();
|
||||
|
||||
/*
|
||||
* Mark all the SPI interrupts as non-secure Group-1.
|
||||
* Also, deactivate and disable them.
|
||||
*/
|
||||
for (i = 32; i < gicv3_data.nr_spis; i += 32) {
|
||||
writel(~0, dist_base + GICD_IGROUPR + i / 8);
|
||||
writel(~0, dist_base + GICD_ICACTIVER + i / 8);
|
||||
writel(~0, dist_base + GICD_ICENABLER + i / 8);
|
||||
}
|
||||
|
||||
/* Set a default priority for all the SPIs */
|
||||
for (i = 32; i < gicv3_data.nr_spis; i += 4)
|
||||
writel(GICD_INT_DEF_PRI_X4,
|
||||
dist_base + GICD_IPRIORITYR + i);
|
||||
|
||||
/* Wait for the settings to sync-in */
|
||||
gicv3_gicd_wait_for_rwp();
|
||||
|
||||
/* Finally, enable the distributor globally with ARE */
|
||||
writel(GICD_CTLR_ARE_NS | GICD_CTLR_ENABLE_G1A |
|
||||
GICD_CTLR_ENABLE_G1, dist_base + GICD_CTLR);
|
||||
gicv3_gicd_wait_for_rwp();
|
||||
}
|
||||
|
||||
static void gicv3_init(unsigned int nr_cpus, void *dist_base)
|
||||
{
|
||||
GUEST_ASSERT(nr_cpus <= GICV3_MAX_CPUS);
|
||||
|
||||
gicv3_data.nr_cpus = nr_cpus;
|
||||
gicv3_data.dist_base = dist_base;
|
||||
gicv3_data.nr_spis = GICD_TYPER_SPIS(
|
||||
readl(gicv3_data.dist_base + GICD_TYPER));
|
||||
if (gicv3_data.nr_spis > 1020)
|
||||
gicv3_data.nr_spis = 1020;
|
||||
|
||||
/*
|
||||
* Initialize only the distributor for now.
|
||||
* The redistributor and CPU interfaces are initialized
|
||||
* later for every PE.
|
||||
*/
|
||||
gicv3_dist_init();
|
||||
}
|
||||
|
||||
const struct gic_common_ops gicv3_ops = {
|
||||
.gic_init = gicv3_init,
|
||||
.gic_cpu_init = gicv3_cpu_init,
|
||||
.gic_irq_enable = gicv3_irq_enable,
|
||||
.gic_irq_disable = gicv3_irq_disable,
|
||||
.gic_read_iar = gicv3_read_iar,
|
||||
.gic_write_eoir = gicv3_write_eoir,
|
||||
};
|
|
@ -0,0 +1,70 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0 */
|
||||
/*
|
||||
* ARM Generic Interrupt Controller (GIC) v3 specific defines
|
||||
*/
|
||||
|
||||
#ifndef SELFTEST_KVM_GICV3_H
|
||||
#define SELFTEST_KVM_GICV3_H
|
||||
|
||||
#include <asm/sysreg.h>
|
||||
|
||||
/*
|
||||
* Distributor registers
|
||||
*/
|
||||
#define GICD_CTLR 0x0000
|
||||
#define GICD_TYPER 0x0004
|
||||
#define GICD_IGROUPR 0x0080
|
||||
#define GICD_ISENABLER 0x0100
|
||||
#define GICD_ICENABLER 0x0180
|
||||
#define GICD_ICACTIVER 0x0380
|
||||
#define GICD_IPRIORITYR 0x0400
|
||||
|
||||
/*
|
||||
* The assumption is that the guest runs in a non-secure mode.
|
||||
* The following bits of GICD_CTLR are defined accordingly.
|
||||
*/
|
||||
#define GICD_CTLR_RWP (1U << 31)
|
||||
#define GICD_CTLR_nASSGIreq (1U << 8)
|
||||
#define GICD_CTLR_ARE_NS (1U << 4)
|
||||
#define GICD_CTLR_ENABLE_G1A (1U << 1)
|
||||
#define GICD_CTLR_ENABLE_G1 (1U << 0)
|
||||
|
||||
#define GICD_TYPER_SPIS(typer) ((((typer) & 0x1f) + 1) * 32)
|
||||
#define GICD_INT_DEF_PRI_X4 0xa0a0a0a0
|
||||
|
||||
/*
|
||||
* Redistributor registers
|
||||
*/
|
||||
#define GICR_CTLR 0x000
|
||||
#define GICR_WAKER 0x014
|
||||
|
||||
#define GICR_CTLR_RWP (1U << 3)
|
||||
|
||||
#define GICR_WAKER_ProcessorSleep (1U << 1)
|
||||
#define GICR_WAKER_ChildrenAsleep (1U << 2)
|
||||
|
||||
/*
|
||||
* Redistributor registers, offsets from SGI base
|
||||
*/
|
||||
#define GICR_IGROUPR0 GICD_IGROUPR
|
||||
#define GICR_ISENABLER0 GICD_ISENABLER
|
||||
#define GICR_ICENABLER0 GICD_ICENABLER
|
||||
#define GICR_ICACTIVER0 GICD_ICACTIVER
|
||||
#define GICR_IPRIORITYR0 GICD_IPRIORITYR
|
||||
|
||||
/* CPU interface registers */
|
||||
#define SYS_ICC_PMR_EL1 sys_reg(3, 0, 4, 6, 0)
|
||||
#define SYS_ICC_IAR1_EL1 sys_reg(3, 0, 12, 12, 0)
|
||||
#define SYS_ICC_EOIR1_EL1 sys_reg(3, 0, 12, 12, 1)
|
||||
#define SYS_ICC_SRE_EL1 sys_reg(3, 0, 12, 12, 5)
|
||||
#define SYS_ICC_GRPEN1_EL1 sys_reg(3, 0, 12, 12, 7)
|
||||
|
||||
#define ICC_PMR_DEF_PRIO 0xf0
|
||||
|
||||
#define ICC_SRE_EL1_SRE (1U << 0)
|
||||
|
||||
#define ICC_IGRPEN1_EL1_ENABLE (1U << 0)
|
||||
|
||||
#define GICV3_MAX_CPUS 512
|
||||
|
||||
#endif /* SELFTEST_KVM_GICV3_H */
|
|
@ -212,7 +212,7 @@ void virt_dump(FILE *stream, struct kvm_vm *vm, uint8_t indent)
|
|||
}
|
||||
}
|
||||
|
||||
void aarch64_vcpu_setup(struct kvm_vm *vm, int vcpuid, struct kvm_vcpu_init *init)
|
||||
void aarch64_vcpu_setup(struct kvm_vm *vm, uint32_t vcpuid, struct kvm_vcpu_init *init)
|
||||
{
|
||||
struct kvm_vcpu_init default_init = { .target = -1, };
|
||||
uint64_t sctlr_el1, tcr_el1;
|
||||
|
@ -232,10 +232,10 @@ void aarch64_vcpu_setup(struct kvm_vm *vm, int vcpuid, struct kvm_vcpu_init *ini
|
|||
* Enable FP/ASIMD to avoid trapping when accessing Q0-Q15
|
||||
* registers, which the variable argument list macros do.
|
||||
*/
|
||||
set_reg(vm, vcpuid, ARM64_SYS_REG(CPACR_EL1), 3 << 20);
|
||||
set_reg(vm, vcpuid, KVM_ARM64_SYS_REG(SYS_CPACR_EL1), 3 << 20);
|
||||
|
||||
get_reg(vm, vcpuid, ARM64_SYS_REG(SCTLR_EL1), &sctlr_el1);
|
||||
get_reg(vm, vcpuid, ARM64_SYS_REG(TCR_EL1), &tcr_el1);
|
||||
get_reg(vm, vcpuid, KVM_ARM64_SYS_REG(SYS_SCTLR_EL1), &sctlr_el1);
|
||||
get_reg(vm, vcpuid, KVM_ARM64_SYS_REG(SYS_TCR_EL1), &tcr_el1);
|
||||
|
||||
switch (vm->mode) {
|
||||
case VM_MODE_P52V48_4K:
|
||||
|
@ -273,10 +273,11 @@ void aarch64_vcpu_setup(struct kvm_vm *vm, int vcpuid, struct kvm_vcpu_init *ini
|
|||
tcr_el1 |= (1 << 8) | (1 << 10) | (3 << 12);
|
||||
tcr_el1 |= (64 - vm->va_bits) /* T0SZ */;
|
||||
|
||||
set_reg(vm, vcpuid, ARM64_SYS_REG(SCTLR_EL1), sctlr_el1);
|
||||
set_reg(vm, vcpuid, ARM64_SYS_REG(TCR_EL1), tcr_el1);
|
||||
set_reg(vm, vcpuid, ARM64_SYS_REG(MAIR_EL1), DEFAULT_MAIR_EL1);
|
||||
set_reg(vm, vcpuid, ARM64_SYS_REG(TTBR0_EL1), vm->pgd);
|
||||
set_reg(vm, vcpuid, KVM_ARM64_SYS_REG(SYS_SCTLR_EL1), sctlr_el1);
|
||||
set_reg(vm, vcpuid, KVM_ARM64_SYS_REG(SYS_TCR_EL1), tcr_el1);
|
||||
set_reg(vm, vcpuid, KVM_ARM64_SYS_REG(SYS_MAIR_EL1), DEFAULT_MAIR_EL1);
|
||||
set_reg(vm, vcpuid, KVM_ARM64_SYS_REG(SYS_TTBR0_EL1), vm->pgd);
|
||||
set_reg(vm, vcpuid, KVM_ARM64_SYS_REG(SYS_TPIDR_EL1), vcpuid);
|
||||
}
|
||||
|
||||
void vcpu_dump(FILE *stream, struct kvm_vm *vm, uint32_t vcpuid, uint8_t indent)
|
||||
|
@ -362,7 +363,7 @@ void vcpu_init_descriptor_tables(struct kvm_vm *vm, uint32_t vcpuid)
|
|||
{
|
||||
extern char vectors;
|
||||
|
||||
set_reg(vm, vcpuid, ARM64_SYS_REG(VBAR_EL1), (uint64_t)&vectors);
|
||||
set_reg(vm, vcpuid, KVM_ARM64_SYS_REG(SYS_VBAR_EL1), (uint64_t)&vectors);
|
||||
}
|
||||
|
||||
void route_exception(struct ex_regs *regs, int vector)
|
||||
|
@ -426,3 +427,8 @@ void vm_install_exception_handler(struct kvm_vm *vm, int vector,
|
|||
assert(vector < VECTOR_NUM);
|
||||
handlers->exception_handlers[vector][0] = handler;
|
||||
}
|
||||
|
||||
uint32_t guest_get_vcpuid(void)
|
||||
{
|
||||
return read_sysreg(tpidr_el1);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* ARM64 Spinlock support
|
||||
*/
|
||||
#include <stdint.h>
|
||||
|
||||
#include "spinlock.h"
|
||||
|
||||
void spin_lock(struct spinlock *lock)
|
||||
{
|
||||
int val, res;
|
||||
|
||||
asm volatile(
|
||||
"1: ldaxr %w0, [%2]\n"
|
||||
" cbnz %w0, 1b\n"
|
||||
" mov %w0, #1\n"
|
||||
" stxr %w1, %w0, [%2]\n"
|
||||
" cbnz %w1, 1b\n"
|
||||
: "=&r" (val), "=&r" (res)
|
||||
: "r" (&lock->v)
|
||||
: "memory");
|
||||
}
|
||||
|
||||
void spin_unlock(struct spinlock *lock)
|
||||
{
|
||||
asm volatile("stlr wzr, [%0]\n" : : "r" (&lock->v) : "memory");
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
// SPDX-License-Identifier: GPL-2.0
|
||||
/*
|
||||
* ARM Generic Interrupt Controller (GIC) v3 host support
|
||||
*/
|
||||
|
||||
#include <linux/kvm.h>
|
||||
#include <linux/sizes.h>
|
||||
#include <asm/kvm.h>
|
||||
|
||||
#include "kvm_util.h"
|
||||
#include "../kvm_util_internal.h"
|
||||
#include "vgic.h"
|
||||
|
||||
/*
|
||||
* vGIC-v3 default host setup
|
||||
*
|
||||
* Input args:
|
||||
* vm - KVM VM
|
||||
* nr_vcpus - Number of vCPUs supported by this VM
|
||||
* gicd_base_gpa - Guest Physical Address of the Distributor region
|
||||
* gicr_base_gpa - Guest Physical Address of the Redistributor region
|
||||
*
|
||||
* Output args: None
|
||||
*
|
||||
* Return: GIC file-descriptor or negative error code upon failure
|
||||
*
|
||||
* The function creates a vGIC-v3 device and maps the distributor and
|
||||
* redistributor regions of the guest. Since it depends on the number of
|
||||
* vCPUs for the VM, it must be called after all the vCPUs have been created.
|
||||
*/
|
||||
int vgic_v3_setup(struct kvm_vm *vm, unsigned int nr_vcpus,
|
||||
uint64_t gicd_base_gpa, uint64_t gicr_base_gpa)
|
||||
{
|
||||
int gic_fd;
|
||||
uint64_t redist_attr;
|
||||
struct list_head *iter;
|
||||
unsigned int nr_gic_pages, nr_vcpus_created = 0;
|
||||
|
||||
TEST_ASSERT(nr_vcpus, "Number of vCPUs cannot be empty\n");
|
||||
|
||||
/*
|
||||
* Make sure that the caller is infact calling this
|
||||
* function after all the vCPUs are added.
|
||||
*/
|
||||
list_for_each(iter, &vm->vcpus)
|
||||
nr_vcpus_created++;
|
||||
TEST_ASSERT(nr_vcpus == nr_vcpus_created,
|
||||
"Number of vCPUs requested (%u) doesn't match with the ones created for the VM (%u)\n",
|
||||
nr_vcpus, nr_vcpus_created);
|
||||
|
||||
/* Distributor setup */
|
||||
gic_fd = kvm_create_device(vm, KVM_DEV_TYPE_ARM_VGIC_V3, false);
|
||||
kvm_device_access(gic_fd, KVM_DEV_ARM_VGIC_GRP_ADDR,
|
||||
KVM_VGIC_V3_ADDR_TYPE_DIST, &gicd_base_gpa, true);
|
||||
nr_gic_pages = vm_calc_num_guest_pages(vm->mode, KVM_VGIC_V3_DIST_SIZE);
|
||||
virt_map(vm, gicd_base_gpa, gicd_base_gpa, nr_gic_pages);
|
||||
|
||||
/* Redistributor setup */
|
||||
redist_attr = REDIST_REGION_ATTR_ADDR(nr_vcpus, gicr_base_gpa, 0, 0);
|
||||
kvm_device_access(gic_fd, KVM_DEV_ARM_VGIC_GRP_ADDR,
|
||||
KVM_VGIC_V3_ADDR_TYPE_REDIST_REGION, &redist_attr, true);
|
||||
nr_gic_pages = vm_calc_num_guest_pages(vm->mode,
|
||||
KVM_VGIC_V3_REDIST_SIZE * nr_vcpus);
|
||||
virt_map(vm, gicr_base_gpa, gicr_base_gpa, nr_gic_pages);
|
||||
|
||||
kvm_device_access(gic_fd, KVM_DEV_ARM_VGIC_GRP_CTRL,
|
||||
KVM_DEV_ARM_VGIC_CTRL_INIT, NULL, true);
|
||||
|
||||
return gic_fd;
|
||||
}
|
Loading…
Reference in New Issue