Ví dụ về bar baral chạy được tối thiểu Intel x86
Ví dụ kim loại trần có thể chạy được với tất cả các mẫu nồi hơi cần thiết . Tất cả các phần chính được bảo hiểm dưới đây.
Đã thử nghiệm trên Ubuntu 15.10 QEMU 2.3.0 và Lenovo ThinkPad T400 khách phần cứng thực sự .
Các Intel Manual Tập 3 Hệ thống Lập trình Hướng dẫn - 325384-056US Tháng Chín 2015 bìa SMP trong chương 8, 9 và 10.
Bảng 8-1. "Trình tự phát sóng INIT-SIPI-SIPI và lựa chọn thời gian chờ" chứa một ví dụ về cơ bản chỉ hoạt động:
MOV ESI, ICR_LOW ; Load address of ICR low dword into ESI.
MOV EAX, 000C4500H ; Load ICR encoding for broadcast INIT IPI
; to all APs into EAX.
MOV [ESI], EAX ; Broadcast INIT IPI to all APs
; 10-millisecond delay loop.
MOV EAX, 000C46XXH ; Load ICR encoding for broadcast SIPI IP
; to all APs into EAX, where xx is the vector computed in step 10.
MOV [ESI], EAX ; Broadcast SIPI IPI to all APs
; 200-microsecond delay loop
MOV [ESI], EAX ; Broadcast second SIPI IPI to all APs
; Waits for the timer interrupt until the timer expires
Trên mã đó:
Hầu hết các hệ điều hành sẽ làm cho hầu hết các hoạt động đó không thể thực hiện được từ vòng 3 (chương trình người dùng).
Vì vậy, bạn cần phải viết kernel của riêng mình để chơi tự do với nó: một chương trình Linux dành cho người dùng sẽ không hoạt động.
Lúc đầu, một bộ xử lý duy nhất chạy, được gọi là bộ xử lý bootstrap (BSP).
Nó phải đánh thức những cái khác (được gọi là Bộ xử lý ứng dụng (AP)) thông qua các ngắt đặc biệt gọi là Ngắt bộ xử lý liên bộ (IPI) .
Những ngắt đó có thể được thực hiện bằng cách lập trình Bộ điều khiển ngắt lập trình nâng cao (APIC) thông qua thanh ghi lệnh ngắt (ICR)
Định dạng của ICR được ghi lại tại: 10.6 "PHÁT HÀNH NỘI DUNG INTERPROCESSOR"
IPI xảy ra ngay khi chúng tôi viết thư cho ICR.
ICR_LOW được định nghĩa tại 8.4.4 "Ví dụ khởi tạo MP" là:
ICR_LOW EQU 0FEE00300H
Giá trị ma thuật 0FEE00300
là địa chỉ bộ nhớ của ICR, như được ghi trong Bảng 10-1 "Bản đồ địa chỉ đăng ký APIC cục bộ"
Phương pháp đơn giản nhất có thể được sử dụng trong ví dụ: nó thiết lập ICR để gửi IPI quảng bá được gửi đến tất cả các bộ xử lý khác ngoại trừ bộ xử lý hiện tại.
Nhưng một số cũng có thể, và được một số người khuyên dùng , để có được thông tin về bộ xử lý thông qua các cấu trúc dữ liệu đặc biệt được thiết lập bởi BIOS như bảng ACPI hoặc bảng cấu hình MP của Intel và chỉ đánh thức từng cái bạn cần.
XX
trong 000C46XXH
mã hóa địa chỉ của lệnh đầu tiên mà bộ xử lý sẽ thực thi như:
CS = XX * 0x100
IP = 0
Hãy nhớ rằng CS nhân nhiều địa chỉ theo0x10
, vì vậy địa chỉ bộ nhớ thực của lệnh đầu tiên là:
XX * 0x1000
Vì vậy, nếu ví dụ XX == 1
, bộ xử lý sẽ bắt đầu tại 0x1000
.
Sau đó, chúng tôi phải đảm bảo rằng có mã chế độ thực 16 bit được chạy tại vị trí bộ nhớ đó, ví dụ:
cld
mov $init_len, %ecx
mov $init, %esi
mov 0x1000, %edi
rep movsb
.code16
init:
xor %ax, %ax
mov %ax, %ds
/* Do stuff. */
hlt
.equ init_len, . - init
Sử dụng một tập lệnh liên kết là một khả năng khác.
Các vòng lặp trì hoãn là một phần khó chịu để làm việc: không có cách nào siêu đơn giản để thực hiện những giấc ngủ như vậy một cách chính xác.
Các phương pháp có thể bao gồm:
- Thuế TNCN (được sử dụng trong ví dụ của tôi)
- HPET
- hiệu chỉnh thời gian của một vòng lặp bận rộn với ở trên và sử dụng nó để thay thế
Liên quan: Làm thế nào để hiển thị một số trên màn hình và ngủ trong một giây với lắp ráp DOS x86?
Tôi nghĩ rằng bộ xử lý ban đầu cần ở chế độ được bảo vệ để nó hoạt động khi chúng ta ghi vào địa chỉ 0FEE00300H
quá cao cho 16 bit
Để giao tiếp giữa các bộ xử lý, chúng ta có thể sử dụng một spinlock trên quy trình chính và sửa đổi khóa từ lõi thứ hai.
Chúng ta nên đảm bảo rằng bộ nhớ ghi lại được thực hiện, ví dụ như thông qua wbinvd
.
Trạng thái chia sẻ giữa các bộ xử lý
8.7.1 "Trạng thái của bộ xử lý logic" cho biết:
Các tính năng sau đây là một phần của trạng thái kiến trúc của bộ xử lý logic trong bộ xử lý Intel 64 hoặc IA-32 hỗ trợ Công nghệ siêu phân luồng của Intel. Các tính năng có thể được chia thành ba nhóm:
- Nhân đôi cho mỗi bộ xử lý logic
- Được chia sẻ bởi các bộ xử lý logic trong một bộ xử lý vật lý
- Chia sẻ hoặc nhân đôi, tùy thuộc vào việc thực hiện
Các tính năng sau được nhân đôi cho mỗi bộ xử lý logic:
- Các thanh ghi mục đích chung (EAX, EBX, ECX, EDX, ESI, EDI, ESP và EBP)
- Các thanh ghi phân đoạn (CS, DS, SS, ES, FS và GS)
- Các thanh ghi EFLAGS và EIP. Lưu ý rằng các thanh ghi CS và EIP / RIP cho mỗi bộ xử lý logic trỏ đến luồng lệnh cho luồng được xử lý bởi bộ xử lý logic.
- Các thanh ghi x87 FPU (ST0 đến ST7, từ trạng thái, từ điều khiển, từ thẻ, con trỏ toán hạng dữ liệu và con trỏ lệnh)
- Các thanh ghi MMX (MM0 đến MM7)
- Các thanh ghi XMM (XMM0 đến XMM7) và thanh ghi MXCSR
- Thanh ghi điều khiển và thanh ghi con trỏ bảng hệ thống (GDTR, LDTR, IDTR, thanh ghi tác vụ)
- Các thanh ghi gỡ lỗi (DR0, DR1, DR2, DR3, DR6, DR7) và các MSR kiểm soát gỡ lỗi
- Kiểm tra trạng thái toàn cầu của máy (IA32_MCG_STATUS) và khả năng kiểm tra máy (IA32_MCG_CAP) MSR
- Điều chế đồng hồ nhiệt và các MSR điều khiển quản lý nguồn ACPI
- MSRs tem thời gian
- Hầu hết các thanh ghi MSR khác, bao gồm bảng thuộc tính trang (PAT). Xem các ngoại lệ dưới đây.
- Đăng ký APIC địa phương.
- Các thanh ghi mục đích chung bổ sung (R8-R15), các thanh ghi XMM (XMM8-XMM15), thanh ghi điều khiển, IA32_EFER trên bộ xử lý Intel 64.
Các tính năng sau được chia sẻ bởi các bộ xử lý logic:
- Các thanh ghi phạm vi loại bộ nhớ (MTRR)
Cho dù các tính năng sau được chia sẻ hoặc sao chép là dành riêng cho việc triển khai:
- IA32_MISC_ENABLE MSR (địa chỉ MSR 1A0H)
- Kiến trúc kiểm tra máy (MCA) MSR (ngoại trừ các MSR IA32_MCG_STATUS và IA32_MCG_CAP)
- Kiểm soát giám sát hiệu suất và MSRs truy cập
Chia sẻ bộ nhớ cache được thảo luận tại:
Các siêu phân luồng của Intel có bộ nhớ cache và chia sẻ đường ống lớn hơn các lõi riêng biệt: /superuser/133082/hyper-threading-and-dual-core-whats-the-difference/995858#995858
Nhân Linux 4.2
Các hành động khởi tạo chính dường như là tại arch/x86/kernel/smpboot.c
.
ARM ví dụ tối thiểu có thể chạy được
Ở đây tôi cung cấp một ví dụ ARMv8 aarch64 có thể chạy tối thiểu cho QEMU:
.global mystart
mystart:
/* Reset spinlock. */
mov x0, #0
ldr x1, =spinlock
str x0, [x1]
/* Read cpu id into x1.
* TODO: cores beyond 4th?
* Mnemonic: Main Processor ID Register
*/
mrs x1, mpidr_el1
ands x1, x1, 3
beq cpu0_only
cpu1_only:
/* Only CPU 1 reaches this point and sets the spinlock. */
mov x0, 1
ldr x1, =spinlock
str x0, [x1]
/* Ensure that CPU 0 sees the write right now.
* Optional, but could save some useless CPU 1 loops.
*/
dmb sy
/* Wake up CPU 0 if it is sleeping on wfe.
* Optional, but could save power on a real system.
*/
sev
cpu1_sleep_forever:
/* Hint CPU 1 to enter low power mode.
* Optional, but could save power on a real system.
*/
wfe
b cpu1_sleep_forever
cpu0_only:
/* Only CPU 0 reaches this point. */
/* Wake up CPU 1 from initial sleep!
* See:https://github.com/cirosantilli/linux-kernel-module-cheat#psci
*/
/* PCSI function identifier: CPU_ON. */
ldr w0, =0xc4000003
/* Argument 1: target_cpu */
mov x1, 1
/* Argument 2: entry_point_address */
ldr x2, =cpu1_only
/* Argument 3: context_id */
mov x3, 0
/* Unused hvc args: the Linux kernel zeroes them,
* but I don't think it is required.
*/
hvc 0
spinlock_start:
ldr x0, spinlock
/* Hint CPU 0 to enter low power mode. */
wfe
cbz x0, spinlock_start
/* Semihost exit. */
mov x1, 0x26
movk x1, 2, lsl 16
str x1, [sp, 0]
mov x0, 0
str x0, [sp, 8]
mov x1, sp
mov w0, 0x18
hlt 0xf000
spinlock:
.skip 8
GitHub ngược dòng .
Lắp ráp và chạy:
aarch64-linux-gnu-gcc \
-mcpu=cortex-a57 \
-nostdlib \
-nostartfiles \
-Wl,--section-start=.text=0x40000000 \
-Wl,-N \
-o aarch64.elf \
-T link.ld \
aarch64.S \
;
qemu-system-aarch64 \
-machine virt \
-cpu cortex-a57 \
-d in_asm \
-kernel aarch64.elf \
-nographic \
-semihosting \
-smp 2 \
;
Trong ví dụ này, chúng tôi đặt CPU 0 trong một vòng lặp spinlock và nó chỉ thoát khi CPU 1 giải phóng spinlock.
Sau spinlock, CPU 0 sau đó thực hiện lệnh gọi semihost khiến QEMU thoát.
Nếu bạn khởi động QEMU chỉ với một CPU -smp 1
, thì mô phỏng chỉ bị treo mãi mãi trên spinlock.
CPU 1 được đánh thức với giao diện PSCI, chi tiết hơn tại: ARM: Start / Wakeup / Đưa các lõi CPU / AP khác và vượt qua địa chỉ bắt đầu thực hiện?
Các phiên bản thượng nguồn cũng có một vài điều chỉnh để làm cho nó làm việc trên gem5, vì vậy bạn có thể thử nghiệm với các đặc tính hiệu suất là tốt.
Tôi chưa thử nghiệm nó trên phần cứng thực sự, vì vậy và tôi không chắc nó có khả năng di động như thế nào. Thư mục Raspberry Pi sau đây có thể được quan tâm:
Tài liệu này cung cấp một số hướng dẫn về cách sử dụng các nguyên hàm đồng bộ hóa ARM mà sau đó bạn có thể sử dụng để thực hiện những điều thú vị với nhiều lõi: http://infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008A_arm_syn syncization_primologists.pdf
Đã thử nghiệm trên Ubuntu 18.10, GCC 8.2.0, Binutils 2.31.1, QEMU 2.12.0.
Các bước tiếp theo để lập trình thuận tiện hơn
Các ví dụ trước đánh thức CPU thứ cấp và thực hiện đồng bộ hóa bộ nhớ cơ bản với các hướng dẫn chuyên dụng, đây là một khởi đầu tốt.
Nhưng để làm cho các hệ thống đa lõi dễ lập trình, ví dụ như POSIX pthreads
, bạn cũng cần phải đi sâu vào các chủ đề liên quan sau:
thiết lập ngắt và chạy bộ định thời định kỳ quyết định chủ đề nào sẽ chạy ngay bây giờ. Điều này được gọi là đa luồng ưu tiên .
Hệ thống như vậy cũng cần lưu và khôi phục các thanh ghi luồng khi chúng được khởi động và dừng.
Cũng có thể có các hệ thống đa nhiệm không ưu tiên, nhưng các hệ thống đó có thể yêu cầu bạn sửa đổi mã của mình để mọi luồng đều mang lại (ví dụ như với pthread_yield
việc triển khai) và việc cân bằng khối lượng công việc trở nên khó khăn hơn.
Dưới đây là một số ví dụ hẹn giờ kim loại trần đơn giản:
đối phó với xung đột bộ nhớ. Đáng chú ý, mỗi luồng sẽ cần một ngăn xếp duy nhất nếu bạn muốn mã bằng C hoặc các ngôn ngữ cấp cao khác.
Bạn chỉ có thể giới hạn các luồng để có kích thước ngăn xếp tối đa cố định, nhưng cách tốt hơn để xử lý vấn đề này là phân trang cho phép ngăn xếp "kích thước không giới hạn" hiệu quả.
Dưới đây là một ví dụ aarch64 ngây thơ sẽ nổ tung nếu ngăn xếp phát triển quá sâu
Đó là một số lý do tốt để sử dụng nhân Linux hoặc một số hệ điều hành khác :-)
Nguyên thủy đồng bộ hóa bộ nhớ người dùng
Mặc dù bắt đầu / dừng / quản lý luồng thường vượt quá phạm vi người dùng, tuy nhiên bạn có thể sử dụng các hướng dẫn lắp ráp từ các luồng của người dùng để đồng bộ hóa truy cập bộ nhớ mà không cần các cuộc gọi hệ thống đắt tiền hơn.
Tất nhiên bạn nên thích sử dụng các thư viện bao bọc một cách hợp lý các nguyên thủy cấp thấp này. Các tiêu chuẩn riêng của mình C ++ đã có những bước tiến lớn trên <mutex>
và <atomic>
tiêu đề, và đặc biệt với std::memory_order
. Tôi không chắc liệu nó có bao gồm tất cả các ngữ nghĩa bộ nhớ có thể đạt được hay không, nhưng nó chỉ có thể.
Các ngữ nghĩa tinh tế hơn có liên quan đặc biệt trong bối cảnh khóa cấu trúc dữ liệu miễn phí , có thể mang lại lợi ích hiệu suất trong một số trường hợp nhất định. Để thực hiện những điều đó, bạn có thể sẽ phải tìm hiểu một chút về các loại rào cản bộ nhớ khác nhau: https://preshing.com/20120710/memory-barrier-are-like-source-control-operations/
Ví dụ, Boost có một số triển khai vùng chứa miễn phí khóa tại: https://www.boost.org/doc/libs/1_63_0/doc/html/lockfree.html
Các hướng dẫn về vùng người dùng như vậy cũng dường như được sử dụng để thực hiện lệnh futex
gọi hệ thống Linux , đây là một trong những nguyên tắc đồng bộ hóa chính trong Linux. man futex
4,15 lần đọc:
Cuộc gọi hệ thống futex () cung cấp một phương thức để đợi cho đến khi một điều kiện nhất định trở thành đúng. Nó thường được sử dụng như một cấu trúc chặn trong bối cảnh đồng bộ hóa bộ nhớ chia sẻ. Khi sử dụng Futexes, phần lớn các hoạt động đồng bộ hóa được thực hiện trong không gian người dùng. Một chương trình không gian người dùng chỉ sử dụng lệnh gọi hệ thống futex () khi có khả năng chương trình đó phải chặn trong một thời gian dài hơn cho đến khi điều kiện trở thành đúng. Các hoạt động futex () khác có thể được sử dụng để đánh thức mọi tiến trình hoặc luồng đang chờ một điều kiện cụ thể.
Bản thân tên tòa nhà có nghĩa là "Không gian người dùng nhanh XXX".
Dưới đây là một ví dụ C ++ x86_64 / aarch64 vô dụng tối thiểu với lắp ráp nội tuyến minh họa việc sử dụng cơ bản các hướng dẫn như vậy chủ yếu để giải trí:
main.cpp
#include <atomic>
#include <cassert>
#include <iostream>
#include <thread>
#include <vector>
std::atomic_ulong my_atomic_ulong(0);
unsigned long my_non_atomic_ulong = 0;
#if defined(__x86_64__) || defined(__aarch64__)
unsigned long my_arch_atomic_ulong = 0;
unsigned long my_arch_non_atomic_ulong = 0;
#endif
size_t niters;
void threadMain() {
for (size_t i = 0; i < niters; ++i) {
my_atomic_ulong++;
my_non_atomic_ulong++;
#if defined(__x86_64__)
__asm__ __volatile__ (
"incq %0;"
: "+m" (my_arch_non_atomic_ulong)
:
:
);
// https://github.com/cirosantilli/linux-kernel-module-cheat#x86-lock-prefix
__asm__ __volatile__ (
"lock;"
"incq %0;"
: "+m" (my_arch_atomic_ulong)
:
:
);
#elif defined(__aarch64__)
__asm__ __volatile__ (
"add %0, %0, 1;"
: "+r" (my_arch_non_atomic_ulong)
:
:
);
// https://github.com/cirosantilli/linux-kernel-module-cheat#arm-lse
__asm__ __volatile__ (
"ldadd %[inc], xzr, [%[addr]];"
: "=m" (my_arch_atomic_ulong)
: [inc] "r" (1),
[addr] "r" (&my_arch_atomic_ulong)
:
);
#endif
}
}
int main(int argc, char **argv) {
size_t nthreads;
if (argc > 1) {
nthreads = std::stoull(argv[1], NULL, 0);
} else {
nthreads = 2;
}
if (argc > 2) {
niters = std::stoull(argv[2], NULL, 0);
} else {
niters = 10000;
}
std::vector<std::thread> threads(nthreads);
for (size_t i = 0; i < nthreads; ++i)
threads[i] = std::thread(threadMain);
for (size_t i = 0; i < nthreads; ++i)
threads[i].join();
assert(my_atomic_ulong.load() == nthreads * niters);
// We can also use the atomics direclty through `operator T` conversion.
assert(my_atomic_ulong == my_atomic_ulong.load());
std::cout << "my_non_atomic_ulong " << my_non_atomic_ulong << std::endl;
#if defined(__x86_64__) || defined(__aarch64__)
assert(my_arch_atomic_ulong == nthreads * niters);
std::cout << "my_arch_non_atomic_ulong " << my_arch_non_atomic_ulong << std::endl;
#endif
}
GitHub ngược dòng .
Sản lượng có thể:
my_non_atomic_ulong 15264
my_arch_non_atomic_ulong 15267
Từ đó, chúng ta thấy rằng lệnh x86 LOCK tiền tố / aarch64 LDADD
đã tạo ra nguyên tử bổ sung: không có nó, chúng ta có các điều kiện chạy đua trên nhiều bổ sung và tổng số ở cuối là ít hơn 20000 được đồng bộ hóa.
Xem thêm:
Đã thử nghiệm trong Ubuntu 19.04 amd64 và với chế độ người dùng QEMU aarch64.