You are on page 1of 491

Machine Translated by Google

KC Vương

Nhúng và
thời gian thực

Điều hành
Hệ thống
Machine Translated by Google

Hệ điều hành nhúng và thời gian thực


Machine Translated by Google

KC Vương

Nhúng và thời gian thực


Các hệ điều hành

123
Machine Translated by Google

Trường Kỹ
thuật Điện và Khoa học Máy tính
KC Wang Đại học Bang
Washington Pullman, WA Hoa Kỳ

ISBN 978-3-319-51516-8 ISBN 978-3-319-51517-5 (Sách điện tử)


DOI 10.1007/978-3-319-51517-5

Số kiểm soát của Thư viện Quốc hội: 2016961664

© Springer International Publishing AG 2017 Tác


phẩm này có bản quyền. Tất cả các quyền đều được Nhà xuất bản bảo lưu, cho dù toàn bộ hay một phần tài liệu có liên quan,
cụ thể là quyền dịch, in lại, sử dụng lại các hình minh họa, trích dẫn, phát sóng, sao chép trên vi phim hoặc theo bất kỳ
cách vật lý nào khác và truyền tải hoặc lưu trữ thông tin. và truy xuất, điều chỉnh điện tử, phần mềm máy tính hoặc bằng
phương pháp tương tự hoặc khác biệt hiện đã biết hoặc được phát triển sau này.
Việc sử dụng tên mô tả chung, tên đăng ký, nhãn hiệu, nhãn hiệu dịch vụ, v.v. trong ấn phẩm này không ngụ ý, ngay cả khi
không có tuyên bố cụ thể, rằng những tên đó được miễn trừ khỏi các luật và quy định bảo vệ có liên quan và do đó miễn phí
cho các quy định chung. sử dụng.
Nhà xuất bản, các tác giả và các biên tập viên yên tâm khi cho rằng những lời khuyên và thông tin trong cuốn sách này được
cho là đúng sự thật và chính xác vào ngày xuất bản. Cả nhà xuất bản, tác giả hay người biên tập đều không đưa ra sự bảo
đảm, rõ ràng hay ngụ ý, đối với tài liệu có trong tài liệu này hoặc đối với bất kỳ sai sót hoặc thiếu sót nào có thể đã xảy
ra. Nhà xuất bản vẫn giữ thái độ trung lập đối với các khiếu nại về quyền tài phán trong các bản đồ được xuất bản và các tổ
chức liên kết.

In trên giấy không chứa axit

Dấu ấn Springer này được xuất bản bởi Springer Nature


Công ty đã đăng ký là Springer International Publishing AG
Địa chỉ công ty đã đăng ký là: Gewerbestrasse 11, 6330 Cham, Thụy Sĩ
Machine Translated by Google

Dành riêng cho

Cindy
Machine Translated by Google

Lời nói đầu

Kể từ khi Springer xuất bản cuốn sách đầu tiên về Thiết kế và triển khai Hệ điều hành MTX của Springer

vào năm 2015, tôi đã nhận được câu hỏi từ nhiều độc giả nhiệt tình về cách chạy Hệ điều hành MTX trên
các thiết bị di động dựa trên ARM của họ, chẳng hạn như iPod hoặc iPhone, v.v. đã thôi thúc tôi viết cuốn

sách này.

Mục đích của cuốn sách này là cung cấp một nền tảng phù hợp cho việc dạy và học lý thuyết cũng như

thực hành về hệ điều hành nhúng và thời gian thực. Nó bao gồm các khái niệm và nguyên tắc cơ bản của hệ

điều hành, đồng thời chỉ ra cách áp dụng chúng vào việc thiết kế và triển khai hệ điều hành hoàn chỉnh

cho các hệ thống nhúng và thời gian thực. Để thực hiện những điều này một cách cụ thể và có ý nghĩa, nó

sử dụng chuỗi công cụ ARM để phát triển chương trình và sử dụng máy ảo ARM để thể hiện các nguyên tắc

thiết kế và kỹ thuật triển khai.

Do nội dung kỹ thuật của nó, cuốn sách này không dành cho các khóa học đầu vào chỉ dạy các khái niệm

và nguyên tắc của hệ điều hành mà không có bất kỳ thực hành lập trình nào. Nó dành cho các khóa học Khoa

học/Kỹ thuật Máy tính theo định hướng kỹ thuật trên các hệ thống nhúng và thời gian thực, nhấn mạnh cả

lý thuyết và thực hành. Phong cách tiến hóa của cuốn sách, cùng với mã nguồn chi tiết và hệ thống mẫu

làm việc hoàn chỉnh, khiến cuốn sách đặc biệt thích hợp cho việc tự học.

Việc thực hiện dự án viết sách này đã chứng tỏ là một nỗ lực rất khắt khe và tốn thời gian, nhưng tôi

rất thích những thử thách đó. Trong khi chuẩn bị bản thảo cuốn sách để xuất bản, tôi đã nhận được sự động

viên và giúp đỡ từ rất nhiều người, trong đó có nhiều bạn học TaiDa EE60 trước đây của tôi. Tôi muốn nhân

cơ hội này để cảm ơn tất cả họ. Tôi cũng biết ơn Springer International Publishing AG vì đã cho phép tôi

tiết lộ miễn phí mã nguồn của cuốn sách này cho công chúng, mã này có sẵn tại http://www.eecs.wsu.edu/

*cs460/ARMhome để tải xuống.

Xin gửi lời cảm ơn đặc biệt tới Cindy vì sự hỗ trợ và nguồn cảm hứng liên tục của cô đã giúp cuốn

sách này trở thành hiện thực. Cuối cùng nhưng không kém phần quan trọng, tôi xin cảm ơn gia đình một lần

nữa vì đã mang theo tôi vô số lý do bận rộn suốt thời gian qua.

Pullman, WA KC Vương
Tháng 10 năm 2016

vii
Machine Translated by Google

Nội dung

1. Giới thiệu ............................................... 1


1.1 Về cuốn sách này 1.2 Động .... . . . ... . . . .. . . . .... .... . . . .. . . . .... . 1
lực của cuốn sách này .... . . . .. . . . .... .... . . . .. . . . .... . 1

1.3 Đối tượng khách quan và dự định . . .. . . . .... .... . . . .. . . . .... . 1

1.4 Đặc điểm độc đáo của cuốn sách này . . . . .. . . . .... .... . . . .. . . . .... . 2
1.5 Nội dung sách . . . ... . . . .... .... . . . .... .... . . . .. . . . .... . 3

1.6 Sử dụng cuốn sách này làm sách giáo khoa cho các hệ thống nhúng. . . . .... ..... . 4

1.7 Sử dụng cuốn sách này làm sách giáo khoa về hệ điều hành. . . . .... ..... . 5

1.8 Sử dụng cuốn sách này để tự học. . . . . .. . . . .... .... . . . .. . . . .... . 5


Người giới thiệu. . ... ..... .... . . . .... .... . . . .... .... . . . .. . . . .... . 5

2 Kiến trúc và lập trình ARM.................................................. 7


2.1 Chế độ bộ xử lý ARM . . . ... . . . .. . . . .... .... . . . .. . . . .... . số 8

2.2 Thanh ghi CPU ARM . . . . . ... . . . .. . . . .... .... . . . .. . . . .... . số 8

2.2.1 Sổ đăng ký chung . ... . . . .. . . . .... .... . . . .. . . . .... . số 8

2.2.2 Thanh ghi trạng thái . . . . . . . . .. . . . .... .... . . . .. . . . .... . 9

2.2.3 Thay đổi Chế độ bộ xử lý ARM 2.3 . . . .... .... . . . .. . . . .... . 9

Đường dẫn hướng dẫn . .. . . . ... . . . .. . . . .... .... . . . .. . . . .... . 10


2.4 Hướng dẫn CÁNH TAY . ... . . . ... . . . .. . . . .... .... . . . .. . . . .... . 10

2.4.1 Cờ điều kiện và điều kiện . . .... .... . . . .. . . . .... . 10


2.4.2 Hướng dẫn rẽ nhánh . .. . . . .. . . . .... .... . . . .. . . . .... . 11

2.4.3 Các phép toán số học. . . . .. . . . .... .... . . . .. . . . .... . 12

2.4.4 Hoạt động so sánh. . . .. . . . .... .... . . . .. . . . .... . 12

2.4.5 Các thao tác logic . .. . . . .. . . . .... .... . . . .. . . . .... . 12

2.4.6 Hoạt động di chuyển dữ liệu. . . . . .... .... . . . .. . . . .... . 13


2.4.7 Giá trị tức thời và Bộ chuyển đổi thùng. . .. .... . . . .. . . . .... . 13

2.4.8 Hướng dẫn nhân . . . . . .. . . . .... .... . . . .. . . . .... . 14


2.4.9 Hướng dẫn TẢI và Lưu trữ . . . . .... .... . . . .. . . . .... . 14

2.4.10 Thanh ghi cơ sở . . . . ... . . . .. . . . .... .... . . . .. . . . .... . 14


2.4.11 Chặn truyền dữ liệu .. . . . .. . . . .... .... . . . .. . . . .... . 14

2.4.12 Hoạt động ngăn xếp. . ... . . . .. . . . .... .... . . . .. . . . .... . 14


2.4.13 Ngăn xếp và chương trình con . . . . .. . . . .... .... . . . .. . . . .... . 15

2.4.14 Ngắt phần mềm (SWI) . . .. . . . .... .... . . . .. . . . .... . 15


2.4.15 Hướng dẫn chuyển PSR. . .. . . . .... .... . . . .. . . . .... . 15

2.4.16 Hướng dẫn của bộ đồng xử lý . . . . . . . .... .... . . . .. . . . .... . 15


2.5 ARM Toolchain 2.6 . . ... . . . ... . . . .. . . . .... .... . . . .. . . . .... . 16

Trình mô phỏng hệ thống ARM . . ... . . . .. . . . .... .... . . . .. . . . .... . 16

2.7 Lập trình ARM. . . . . . ... . . . .. . . . .... .... . . . .. . . . .... . 17

2.7.1 Ví dụ lập trình hợp ngữ ARM 1 2.7.2 Ví dụ lập ... . . . .... ..... . 17

trình hợp ngữ ARM 2 2.7.3 Kết hợp hợp ngữ với lập ... . . . .... ..... . 19

trình C. . .. . . . .... ..... . 20

ix
Machine Translated by Google

x Nội dung

2.8 Trình điều khiển thiết bị . . . ... . . . .. . . . ... . . . .. . . . .... .... . . . .... . 27

2.8.1 Bản đồ bộ nhớ hệ thống . . . ... . . . .. . . . .... .... . . . .... . 27

2.8.2 Lập trình GPIO. . . . . ... . . . .. . . . .... .... . . . .... . 27


2.8.3 Trình điều khiển UART cho I/O nối tiếp . . . . . .. . . . .... .... . . . .... . 29

2.8.4 Trình điều khiển màn hình LCD màu . . . . . . . . . . . . . . . . . . . . . . . . . . . 33

2.9 Tóm tắt . . .... .... . . . .. . . . ... . . . .. . . . .... .... . . . .... . 45


Người giới thiệu. . . . . . .... .... . . . .. . . . ... . . . .. . . . .... .... . . . .... . 46

3 Xử lý ngắt và ngoại lệ. ... . . . .. . . . .... .... . . . .... . 47

3.1 Ngoại lệ ARM . ... . . . .. . . . ... . . . .. . . . .... .... . . . .... . 47


3.1.1 Chế độ bộ xử lý ARM 3.1.2 . . ... . . . .. . . . .... .... . . . .... . 47

Ngoại lệ ARM . .. . . . ... . . . .. . . . .... .... . . . .... . 47

3.1.3 Bảng vectơ ngoại lệ 3.1.4 .... . . . .. . . . .... .... . . . .... . 49

Trình xử lý ngoại lệ 3.1.5 .. . . . ... . . . .. . . . .... .... . . . .... . 49

Trả về từ Trình xử lý ngoại lệ . . .. . . . .... .... . . . .... . 50

3.2 Ngắt và xử lý ngắt. . 3.2.1 Các loại . . . . .. . . . .... .... . . . .... . 51

ngắt. . . .. . . . ... . . . .. . . . .... .... . . . .... . 51

3.2.2 Bộ điều khiển ngắt. . . . ... . . . .. . . . .... .... . . . .... . 51

3.2.3 Bộ điều khiển ngắt chính và phụ. . . . ... . . . .... . 53


3.3 Xử lý ngắt. . . . . . .. . . . ... . . . .. . . . .... .... . . . .... . 54
3.3.1 Nội dung bảng Vector . . . ... . . . .. . . . .... .... . . . .... . 54

3.3.2 Trình tự ngắt phần cứng . . . . .. . . . .... .... . . . .... . 54

3.3.3 Điều khiển ngắt trong phần mềm. . . . .. . . . .... .... . . . .... . 54

3.3.4 Trình xử lý ngắt 3.3.5 . . . . . . ... . . . .. . . . .... .... . . . .... . 55

Trình xử lý ngắt không lồng nhau 3.4 . . . . .. . . . .... .... . . . .... . 55


.. ....
Trình điều khiển hẹn giờ. . . . .. . . . ... . . . .. . . . .... .... . . . .... . 56
3.4.1 Bộ định thời 926EJS đa năng ARM . . . .. . . . .... .... . . . .... . 56

3.4.2 Chương trình điều khiển hẹn giờ. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57

3.5 Trình điều khiển bàn phím . . ... . . . .. . . . ... . . . .. . . . .... .... . . . .... . 61

3.5.1 Giao diện bàn phím chuột ARM PL050 3.5.2 ..... .... . . . .... . 61

Trình điều khiển bàn phím . . . . . . . ... . . . .. . . . .... .... . . . .... . 62

3.5.3 Thiết kế trình điều khiển điều khiển ngắt . . . . . . . . . . . . . . . . . . . . . . . . 62

3.5.4 Chương trình điều khiển bàn phím. . . . . . .. . . . .... .... . . . .... . 63
3.6 Trình điều khiển UART . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
3.6.1 Giao diện UART ARM PL011. . . . .. . . . .... .... . . . .... . 67

3.6.2 Thanh ghi UART . . .. . . . ... . . . .. . . . .... .... . . . .... . 68

3.6.3 Chương trình trình điều khiển UART điều khiển ngắt. . . . . . . . . . . . . . . . . . 69

3.7 Thẻ kỹ thuật số an toàn (SD) . .. . . . ... . . . .. . . . .... .... . . . .... . 73


3.7.1 Giao thức thẻ SD .. . . . ... . . . .. . . . .... .... . . . .... . 74
3.7.2 Trình điều khiển thẻ SD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74

3.7.3 Trình điều khiển SDC được cải tiến . . . ... . . . .. . . . .... .... . . . .... . 80
3.7.4 Truyền dữ liệu đa ngành 3.8 ... . . . .. . . . .... .... . . . .... . 82

Ngắt vectơ . .. . . . .. . . . ... . . . .. . . . .... .... . . . .... . 84

3.8.1 Bộ điều khiển ngắt vectơ ARM PL190 (VIC) . . . . . .... . 84

3.8.2 Cấu hình VIC cho các ngắt có vectơ. . . .... .... . . . .... . 85

3.8.3 Trình xử lý ngắt theo vectơ 3.8.4 . . . . .. . . . .... .... . . . .... . 86

Trình diễn các ngắt theo vectơ . . . .... .... . . . .... . 86

3.9 Các ngắt lồng nhau. . ... . . . .. . . . ... . . . .. . . . .... .... . . . .... . 87

3.9.1 Tại sao ngắt lồng nhau. . . ... . . . .. . . . .... .... . . . .... . 87

3.9.2 Các ngắt lồng nhau trong ARM ... . . . .. . . . .... .... . . . .... . 88

3.9.3 Xử lý các ngắt lồng nhau trong chế độ SYS . . .... .... . . . .... . 88

3.9.4 Trình diễn các ngắt lồng nhau . . . . . .... .... . . . .... . 89
Machine Translated by Google

Nội dung xi

3.10 Các ngắt lồng nhau và chuyển đổi quy trình . . . . .... ..... . . ... . . .... . 92

3.11 Tóm tắt . . .... ..... . . .... ..... . . .... ..... . . ... . . .... . 92
Người giới thiệu. . ... ..... ..... . . .... ..... . . .... ..... . . ... . . .... . 94

4 mô hình hệ thống nhúng . . .... . . ... . . .... ..... . . ... . . .... . 95

4.1 Cấu trúc chương trình của hệ thống nhúng . .... ..... . . ... . . .... . 95

4.2 Mô hình siêu vòng lặp .... . . .... . . ... . . .... ..... . . ... . . .... . 95

4.2.1 Ví dụ về chương trình siêu vòng lặp. . . . . . . . . . . . . . ... . . .... . 96


4.3 Mô hình hướng sự kiện ... . . .... . . ... . . .... ..... . . ... . . .... . 97

4.3.1 Những thiếu sót của các chương trình siêu vòng lặp. . . . . . . . . . . . . . . . . . 97
4.3.2 Sự kiện . . . .... . . .... . . ... . . .... ..... . . ... . . .... . 97

4.3.3 Chương trình theo sự kiện định kỳ. . . .... ..... . . ... . . .... . 98

4.3.4 Chương trình hướng sự kiện không đồng bộ . . . .... . . ... . . . . . . . 101
4.4 Ưu tiên sự kiện. . . .... . . .... . . ... . . .... ..... . . ... . . . . . . . 103
4.5 Mô hình quy trình. . . .... . . .... ..... . . .... ..... . . ... . . . . . . . 103

4.5.1 Mô hình quy trình đơn bộ xử lý. . . . . .... ..... . . ... . . . . . . . 103

4.5.2 Mô hình quy trình đa bộ xử lý . . . .... ..... . . ... . . . . . . . 103

4.5.3 Mô hình quy trình không gian địa chỉ .... ..... . . ... . . . . . . . 103

thực 4.5.4 Mô hình quy trình không gian địa chỉ .. ..... . . ... . . . . . . . 103
ảo 4.5.5 Mô hình quy trình ... . . ... . . .... ..... . . ... . . . . . . . 104

tĩnh 4.5.6 Mô hình quy trình động . . ... . . .... ..... . . ... . . . . . . . 104

4.5.7 Mô hình quy trình không ưu tiên. . . .... ..... . . ... . . . . . . . 104

4.5.8 Mô hình quy trình ưu tiên .... . . .... ..... . . ... . . . . . . . 104

4.6 Mô hình hạt nhân bộ xử lý đơn (UP) . . ... . . .... ..... . . ... . . . . . . . 104

4.7 Mô hình hệ điều hành bộ xử lý đơn (UP). . .. ..... . . ... . . . . . . . 104

4.8 Mô hình hệ thống đa bộ xử lý (MP) 4.9 ... . . .... ..... . . ... . . . . . . . 105

Mô hình hệ thống thời gian thực (RT) .. . . ... . . .... ..... . . ... . . . . . . . 105

4.10 Phương pháp thiết kế phần mềm hệ thống nhúng. . . . . ... . . . . . . . 105

4.10.1 Hỗ trợ ngôn ngữ cấp cao cho hướng sự kiện


Lập trình . . . . .... . . ... . . .... ..... . . ... . . . . . . . 105
4.10.2 Mô hình máy trạng thái. . . . . ... . . .... ..... . . ... . . . . . . . 105
4.10.3 Mô hình biểu đồ trạng thái ..... . . ... . . .... ..... . . ... . . . . . . . 110

4.11 Tóm tắt . . .... ..... . . .... ..... . . .... ..... . . ... . . . . . . . 110
Người giới thiệu. . ... ..... ..... . . .... ..... . . .... ..... . . ... . . .... . 111

5 Quản lý quy trình trong hệ thống nhúng. . . .... ..... . . ... . . . . . . . 113

5.1 Đa nhiệm. ... ..... . . .... ..... . . .... ..... . . ... . . . . . . . 113

5.2 Khái niệm quy trình . .. . . .... . . ... . . .... ..... . . ... . . . . . . . 113

5.3 Đa nhiệm và Chuyển đổi ngữ cảnh . . ... . . .... ..... . . ... . . .... . 114

5.3.1 Một chương trình đa nhiệm đơn giản . . .... ..... . . ... . . .... . 114

5.3.2 Chuyển ngữ cảnh. . ... . . ... . . .... ..... . . ... . . . . . . . 116

5.3.3 Trình diễn đa nhiệm. . . .... ..... . . ... . . . . . . . 120

5.4 Quy trình động 5.4.1 .... . . .... . . ... . . .... ..... . . ... . . .... . 121

Tạo quy trình động . ... . . .... ..... . . ... . . .... . 121

5.4.2 Trình diễn các quy trình động 5.5 Lập kế .. ..... . . ... . . .... . 124

hoạch quy trình . ... . . .... . . ... . . .... ..... . . ... . . .... . 124

5.5.1 Thuật ngữ lập lịch trình quy trình. . .... ..... . . ... . . .... . 124

5.5.2 Mục tiêu, chính sách và thuật toán lập kế hoạch quy trình. . . . . . . . . 125
5.5.3 Lập kế hoạch quy trình trong hệ thống nhúng. ... . . ... . . . . . . . 125

5.6 Đồng bộ hóa quy trình . . 5.6.1 .... . . ... . . .... ..... . . ... . . . . . . . 126
Ngủ và thức dậy . ... . . ... . . .... ..... . . ... . . . . . . . 126

5.6.2 Trình điều khiển thiết bị sử dụng chế độ Ngủ/Thức . . . . . . . . . . ... . . .... . 127
Machine Translated by Google

xii Nội dung

5.7 Hệ thống nhúng hướng sự kiện sử dụng chế độ Ngủ/Thức . . .... . . . . . . . 129

5.7.1 Trình diễn hệ thống nhúng theo sự kiện


Sử dụng chế độ Ngủ/Thức. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
5.8 Quản lý tài nguyên bằng chế độ Ngủ/Thức . . . . .... . .... . . . . . . . 132

5.8.1 Những thiếu sót về Ngủ/Thức. . . ... . . .... . .... . . . . . . . 133

5.9 Ngữ nghĩa . ... . .... . . ... . . .... . . ... . . .... . .... . . . . . . . 133

5.10 Ứng dụng của Semaphores. .. . . .... . . ... . . .... . .... . . . . . . . 134

5.10.1 Khóa Semaphore . . ... . . .... . . ... . . .... . .... . . . . . . . 134


5.10.2 Khóa Mutex. . .. . . ... . . .... . . ... . . .... . .... . . . . . . . 135

5.10.3 Quản lý tài nguyên bằng Semaphore . .... . .... . . . . . . . 135

5.10.4 Chờ ngắt và tin nhắn. ... . . .... . .... . . . . . . . 135

5.10.5 Quá trình hợp tác. . . . .... . . ... . . .... . .... . . . . . . . 135

5.10.6 Ưu điểm của Semaphores. . .. . . ... . . .... . .... . . . . . . . 136

5.10.7 Thận trọng khi sử dụng Semaphores . . . . . . . . .... . .... . . . . . . . 136

5.10.8 Sử dụng Semaphores trong Hệ thống nhúng. . .... . .... . . . . . . . 137

5.11 Các cơ chế đồng bộ hóa khác. . .. . . ... . . .... . .... . . . . . . . 139

5.11.1 Cờ sự kiện trong OpenVMS . ... . . ... . . .... . .... . . . . . . . 140


5.11.2 Biến sự kiện trong MVS . .... . . ... . . .... . .... . . . . . . . 140

5.11.3 ENQ/DEQ trong MVS . .. . . .... . . ... . . .... . .... . . .... . 141

5.12 Cấu trúc đồng bộ hóa cấp cao . . . ... . . .... . .... . . .... . 141
5.12.1 Biến điều kiện. . . . . .... . . ... . . .... . .... . . .... . 141
5.12.2 Màn hình ..... . . ... . . .... . . ... . . .... . .... . . .... . 142
5.13 Quá trình trao đổi thông tin. . . ... . . .... . . ... . . .... . .... . . .... . 142

5.13.1 Bộ nhớ dùng chung . . . . . . . .... . . ... . . .... . .... . . .... . 142

5.13.2 Ống . .. . .... . . ... . . .... . . ... . . .... . .... . . .... . 142

5.13.3 Tín hiệu . . . . . . . . ... . . .... . . ... . . .... . .... . . .... . 147

5.13.4 Truyền tin nhắn . . ... . . .... . . ... . . .... . .... . . .... . 147

5.14 Hạt nhân hệ thống nhúng bộ xử lý đơn (UP) .. . . .... . .... . . . . . . . 152

5.14.1 Hạt nhân UP không ưu tiên ... . . ... . . .... . .... . . . . . . . 152

5.14.2 Trình diễn hạt nhân UP không ưu tiên 5.14.3 Hạt .. . .... . . . . . . . 158

nhân UP ưu tiên 5.14.4 Trình . . . .... . . ... . . .... . .... . . . . . . . 158

diễn hạt nhân UP ưu tiên 5.15 Tóm tắt . . ..... . .... . . . . . . . 164
.... . .... . . ... . . .... . . ... . . .... . .... . . . . . . . 165
Người giới thiệu. . .. . . .... . .... . . ... . . .... . . ... . . .... . .... . . . . . . . 167

6 Quản lý bộ nhớ trong ARM .... . . .... . . ... . . .... . .... . . . . . . . 169

6.1 Không gian địa chỉ quy trình . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169

6.2 Đơn vị quản lý bộ nhớ (MMU) trong Thanh ghi ARM .. . . .... . .... . . . . . . . 169

6.3 MMU. . .... . . ... . . .... . . ... . . .... . .... . . . . . . . 170

6.4 Truy cập các thanh ghi MMU. . .. . . .... . . ... . . .... . .... . . .... . 171

6.4.1 Kích hoạt và vô hiệu hóa MMU. . .. . . .... . .... . . .... . 171
6.4.2 Kiểm soát truy cập miền . . .... . . ... . . .... . .... . . .... . 172

6.4.3 Thanh ghi cơ sở bảng dịch . . ... . . .... . .... . . . . . . . 173

6.4.4 Đăng ký kiểm soát truy cập tên miền. . . . . . . .... . .... . . . . . . . 173

6.4.5 Thanh ghi trạng thái lỗi . . . . . . . . . ... . . .... . .... . . . . . . . 173

6.4.6 Thanh ghi địa chỉ lỗi. . . .... . . ... . . .... . .... . . .... . 174
6,5 Dịch địa chỉ ảo. . . . . .... . . ... . . .... . .... . . .... . 174
6.5.1 Bảng dịch Cơ sở 6.5.2 Bảng . . .... . . ... . . .... . .... . . . . . . . 175
dịch 6.5.3 Bộ mô tả cấp .... . . .... . . ... . . .... . .... . . . . . . . 175

một . . . .... . . ... . . .... . .... . . . . . . . 175


6.6 Bản dịch tài liệu tham khảo phần ..... . . ... . . .... . .... . . . . . . . 176
Machine Translated by Google

Nội dung xiii

6.7 Dịch tài liệu tham khảo trang .. . . ... . . .... . .... . . ... . . . . . . . 176

6.7.1 Bảng trang cấp 1 6.7.2 . . . . . . ... . . .... . .... . . ... . . . . . . . 176

Bộ mô tả trang cấp 2 . . ... . . .... . .... . . ... . . . . . . . 176

6.7.3 Dịch tài liệu tham khảo trang nhỏ 6.7.4 .. . .... . . ... . . .... . 177

Dịch tài liệu tham khảo trang lớn .. . .... . . ... . . . . . . . 178

6.8 Các chương trình ví dụ về quản lý bộ nhớ . .... . .... . . ... . . . . . . . 178

6.8.1 Phân trang một cấp sử dụng các phần 1 MB . .... . . .... . . . . . . 178

6.8.2 Phân trang hai cấp sử dụng trang 4 KB . . . .... . . ... . . . . . . . 183

6.8.3 Phân trang một cấp với không gian VA cao. . .... . . ... . . . . . . . 184

6.8.4 Phân trang hai cấp với không gian VA cao . .... . . .... . . . . . . 189

6.9 Tóm tắt . . .... . .... . . .... . .... . . .... . .... . . ... . . . . . . . 190
Thẩm quyền giải quyết ..... . .... . .... . . .... . .... . . .... . .... . . ... . . . . . . . 191

7 Cuộc gọi hệ thống và quy trình chế độ người dùng. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193


7.1 Quy trình chế độ người .. . . .... . . ... . . .... . .... . . ... . . . . . . . 193

dùng 7.2 Ánh xạ không gian địa chỉ ảo . . . . . . . . .... . .... . . ... . . . . . . . 193
7.3 Quy trình chế độ người dùng .... . . .... . . ... . . .... . .... . . ... . . . . . . . 194

7.3.1 Hình ảnh chế độ người dùng . .... . . ... . . .... . .... . . ... . . . . . . . 194

7.4 Hạt nhân hệ thống hỗ trợ các quy trình ở chế độ người dùng. . . . . . . . . . . . . . . . . . 197
7.4.1 Cấu trúc PROC. . .... . . ... . . .... . .... . . ... . . . . . . . 197
7.4.2 Trình xử lý đặt lại. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201

7.4.3 Mã hạt nhân. . . . . .... . .... . . .... . .... . . ... . . . . . . . 203

7.4.4 Tập lệnh liên kết biên dịch hạt nhân . . . . . .... . .... . . ... . . . . . . . 209
7.4.5 Trình diễn hạt nhân với quy trình chế độ người dùng. . ... . . . . . . 209

7.5 Hệ thống nhúng với các quy trình ở chế độ người .. . .... . . ... . . . . . . . 209
dùng 7.5.1 Các quy trình trong cùng một . . . .... . .... . . ... . . . . . . . 209
miền 7.5.2 Trình diễn các quy trình trong cùng một miền ..... . . . . . . 210
7.5.3 Quy trình với các miền riêng lẻ 7.5.4 Trình .... . .... . . ... . . .... . 212
diễn các quy trình với các miền riêng lẻ .. . .... . 212
Đĩa RAM 7.6 ..... . .... . . .... . .... . . .... . .... . . ... . . . . . . . 213

7.6.1 Tạo ảnh đĩa RAM. . .. . . .... . .... . . ... . . . . . . . 215

7.6.2 Trình tải tệp hình ảnh quy .... . . .... . .... . . ... . . . . . . . 216

trình 7.7 Quản lý quy trình . . . . . .... . .... . . .... . .... . . ... . . . . . . . 218
7.7.1 Tạo quy trình . . .... . . ... . . .... . .... . . ... . . . . . . . 218
7.7.2 Kết thúc quy trình ... . . ... . . .... . .... . . ... . . . . . . . 219

7.7.3 Cây gia đình quy trình ... . . ... . . .... . .... . . ... . . . . . . . 220
7.7.4 Chờ kết thúc quy trình con. . .. . .... . . ... . . . . . . . 220
7.7.5 Fork-Exec trong Unix/Linux . . ... . . .... . .... . . ... . . .... . 221

7.7.6 Triển khai Fork 7.7.7 . . . ... . . .... . .... . . ... . . .... . 222

Triển khai Exec 7.7.8 Trình diễn . . . ... . . .... . .... . . ... . . . . . . . 223
Fork-Exec. . . . . .... . .... . . ... . . . . . . . 225

7.7.9 Sh đơn giản để thực thi lệnh . 7.7.10 ... . .... . . ... . . . . . . . 226
vfork . .. . .... . . .... . .... . . .... . .... . . ... . . . . . . . 228
7.7.11 Trình diễn vfork . . . ... . . .... . .... . . ... . . . . . . . 230
7.8 Chủ đề .. . .... . .... . . .... . .... . . .... . .... . . ... . . . . . . . 231
7.8.1 Tạo chủ đề . . .... . . ... . . .... . .... . . ... . . . . . . . 231
7.8.2 Trình diễn chủ đề .... . . .... . .... . . ... . . . . . . . 233

7.8.3 Đồng bộ hóa luồng . . ... . . .... . .... . . ... . . . . . . . 234

7.9 Hệ thống nhúng với phân trang hai cấp. . 7.9.1 ... . .... . . ... . . . . . . . 234
Phân trang 2 cấp tĩnh . . . . ... . . .... . .... . . ... . . . . . . . 236

7.9.2 Trình diễn phân trang 2 cấp tĩnh . . .... . . ... . . . . . . . 237

7.9.3 Phân trang 2 cấp động . . ... . . .... . .... . . ... . . . . . . . 238

7.9.4 Trình diễn phân trang động 2 cấp độ. .. . . .... . . . . . . 240
Machine Translated by Google

xiv Nội dung

7.10 Bản đồ bộ nhớ KMH . . ... . . .... . . ... . . .... ..... . . .... . 241

7.10.1 KMH Sử dụng phân trang tĩnh một cấp . . . .... ..... . . .... . 241

7.10.2 KMH Sử dụng phân trang tĩnh hai cấp . . . .... ..... . . .... . 244

7.10.3 KMH Sử dụng Phân trang Động Hai Cấp. . ... ..... . . . . . . . 245

7.11 Hệ thống tập tin hỗ trợ hệ thống nhúng . .. . . .... ..... . . . . . . . 245

7.11.1 Tạo hình ảnh thẻ SD. . . .... . . ... . . .... ..... . . . . . . . 246

7.11.2 Định dạng phân vùng thẻ SD dưới dạng hệ thống tệp. . . . . . . . . . .... . 247

7.11.3 Tạo thiết bị vòng lặp cho phân vùng thẻ SD. . . . .... . . .... . 247

7.12 Hệ thống nhúng với hệ thống tệp SDC. ... . . .... ..... . . . . . . . 248

7.12.1 Trình điều khiển thẻ SD sử dụng Semaphore. . . . . . .... ..... . . . . . . . 248

7.12.2 Hạt nhân hệ thống sử dụng hệ thống tệp SD . . . . . . . . . . . . . . . . . . . 252

7.12.3 Trình diễn Hệ thống Tệp SDC. .. . . .... ..... . . . . . . . 253

7.13 Hình ảnh hạt nhân khởi động từ SDC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 253


7.13.1 Chương trình khởi động SDC. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254

7.13.2 Trình diễn hạt nhân khởi động từ SDC . . . .... . . . . . . . 258

7.13.3 Khởi động hạt nhân từ SDC bằng phân trang động . ... . . . . . . . 259

7.13.4 Khởi động hai giai đoạn . . . . . .... . . ... . . .... ..... . . . . . . . 260

7.13.5 Trình diễn khả năng khởi động hai giai đoạn . . . . . . . . . . . . . . . . . . . 261

7.14 Tóm tắt . . .... ..... . . ... . . .... . . ... . . .... ..... . . . . . . . 262
Người giới thiệu. . .. . . .... ..... . . ... . . .... . . ... . . .... ..... . . . . . . . 264

8 Hệ điều hành nhúng có mục đích chung . .. . . .... ..... . . . . . . . 265

8.1 Hệ điều hành có mục đích chung . .. . . ... . . .... ..... . . . . . . . 265

8.2 Hệ điều hành có mục đích chung nhúng. . . .... ..... . . . . . . . 265

8.3 Chuyển GPOS hiện có sang Hệ thống nhúng . . . .... ..... . . . . . . . 265

8.4 Phát triển GPOS nhúng cho ARM. . . ... . . .... ..... . . . . . . . 266

8.5 Tổ chức của EOS . .. . . ... . . .... . . ... . . .... ..... . . . . . . . 266
8.5.1 Nền tảng phần cứng 8.5.2 ... . . .... . . ... . . .... ..... . . . . . . . 266
Cây tệp nguồn EOS 8.5.3 Tệp hạt nhân . . . .... . . ... . . .... ..... . . . . . . . 267
EOS .... . . .... . . ... . . .... ..... . . . . . . . 267

8.5.4 Khả năng của EOS. . 8.5.5 . . . .... . . ... . . .... ..... . . . . . . . 268
Trình tự khởi động của EOS. . ... . . ... . . .... ..... . . . . . . . 268

8.5.6 Quản lý quy trình trong EOS . . . . ... . . .... ..... . . . . . . . 269

8.5.7 Mã hội của EOS. . .... . . ... . . .... ..... . . .... . 271
8.5.8 Tệp hạt nhân của EOS . . . . .... . . ... . . .... ..... . . . . . . . 279

8.5.9 Chức năng quản lý quy trình. . . ... . . .... ..... . . . . . . . 282

8.5.10 Ống . .. ..... . . ... . . .... . . ... . . .... ..... . . . . . . . 282

8.5.11 Truyền tin nhắn . . ... . . .... . . ... . . .... ..... . . . . . . . 283

8.5.12 Trình diễn việc truyền tin nhắn . .. . . .... ..... . . . . . . . 285

8.6 Quản lý bộ nhớ trong EOS . . . .... . . ... . . .... ..... . . . . . . . 285

8.6.1 Bản đồ bộ nhớ của EOS . . . .... . . ... . . .... ..... . . . . . . . 285

8.6.2 Không gian địa chỉ ảo . . .... . . ... . . .... ..... . . . . . . . 285

8.6.3 Chế độ hạt nhân Pgdir và Bảng trang 8.6.4 .. . . .... ..... . . . . . . . 285

Bảng trang chế độ người dùng quy trình . . ... . . .... ..... . . . . . . . 286
. . . . . . . . . . . . . . . . . 286
. . .
8.6.5 Chuyển đổi Pgdir trong khi chuyển đổi quy trình
8.6.6 Phân trang động . . ... . . .... . . ... . . .... ..... . . . . . . . 286

8.7 Ngoại lệ và xử lý tín hiệu. . 8.7.1 Xử .... . . ... . . .... ..... . . . . . . . 288
lý tín hiệu trong Unix/Linux . . ... . . .... ..... . . . . . . . 288

8.8 Xử lý tín hiệu trong EOS . 8.8.1 . . . . . .... . . ... . . .... ..... . . . . . . . 289

Tín hiệu trong tài nguyên PROC. . . . . . . . . . . . . . . . . . . . . . . . . . . 289

8.8.2 Nguồn gốc tín hiệu trong EOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289

8.8.3 Cung cấp tín hiệu cho quy trình . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289

8.8.4 Thay đổi bộ xử lý tín hiệu trong hạt nhân . . . . . . . . . . . . . . . . . . . . . . 290


Machine Translated by Google

Nội dung xv

8.8.5 Xử lý tín hiệu trong hạt nhân EOS . . .... . .... . . ... . . . . . . . 290

8.8.6 Bộ thu tín hiệu gửi đi để thực thi ở chế độ người dùng 8.9 .. . . . . . . 291
Trình điều khiển thiết bị ... . . . . . . .... . . ... . . .... . .... . . ... . . . . . . . 291

8.10 Lập kế hoạch quy trình trong EOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 292


8.11 Dịch vụ hẹn giờ trong EOS . . . . .... . . ... . . .... . .... . . ... . . . . . . . 292

8.12 Hệ thống tập tin . . . . . .... . . .... . .... . . .... . .... . . ... . . . . . . . 294

8.12.1 Mức độ hoạt động của tệp . . . . ... . . .... . .... . . ... . . . . . . . 294

8.12.2 Thao tác I/O tệp . ... . . ... . . .... . .... . . ... . . . . . . . 296

8.12.3 Hệ thống tệp EXT2 trong EOS . ... . . .... . .... . . ... . . . . . . . 302

8.12.4 Thực hiện FS cấp 1 . . . .... . .... . . ... . . . . . . . 304

8.12.5 Thực hiện FS cấp 2 . . . .... . .... . . ... . . . . . . . 310

8.12.6 Thực hiện FS cấp 3 . . . .... . .... . . ... . . . . . . . 316

8.13 Chặn bộ đệm I/O của thiết bị . ... . . ... . . .... . .... . . ... . . . . . . . 318

8.14 Thuật toán quản lý bộ đệm I/O . ... . . .... . .... . . ... . . . . . . . 318
8.14.1 Hiệu suất của bộ đệm đệm I/O. . . . .... . . ... . . . . . . . 321
8.15 Giao diện người dùng. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321

8.15.1 Chương trình INIT . ... . . ... . . .... . .... . . ... . . . . . . . 321

8.15.2 Chương trình đăng nhập . . . . . . ... . . .... . .... . . ... . . . . . . . 322

8.15.3 Chương trình sh . . .... . . ... . . .... . .... . . ... . . . . . . . 322


8.16 Trình diễn EOS. . . . .... . . ... . . .... . .... . . ... . . . . . . . 323

8.16.1 Khởi động EOS . . . . . .... . . ... . . .... . .... . . ... . . . . . . . 323

8.16.2 Xử lý lệnh trong EOS . . . . .... . .... . . ... . . . . . . . 323

8.16.3 Xử lý tín hiệu và ngoại lệ trong EOS . . .... . . .... . . . . . . 323

8.17 Tóm tắt . . .... . .... . . .... . .... . . .... . .... . . ... . . . . . . . 326
Người giới thiệu. . ... . .... . .... . . .... . .... . . .... . .... . . ... . . . . . . . 327

9 Đa xử lý trong hệ thống nhúng. . ... . . .... . .... . . ... . . . . . . . 329

9.1 Đa xử lý . . .... . . .... . .... . . .... . .... . . ... . . . . . . . 329

9.2 Yêu cầu hệ thống SMP. ... . . ... . . .... . .... . . ... . . . . . . . 329
9.3 Bộ xử lý ARM MPcore . . .... . . ... . . .... . .... . . ... . . . . . . . 331
Bộ xử lý lõi 9.4 ARM Cortex-A9. . .. . . .... . .... . . ... . . . . . . . 331
9.4.1 Lõi bộ xử lý . . .... . . ... . . .... . .... . . ... . . . . . . . 331

9.4.2 Bộ điều khiển rình mò (SCU) . ... . . .... . .... . . ... . . . . . . . 332

9.4.3 Bộ điều khiển ngắt chung (GIC) . ... . .... . . ... . . . . . . . 332

9.5 Ví dụ lập trình GIC . ... . . ... . . .... . .... . . ... . . . . . . . 332

9.5.1 Định cấu hình GIC để định tuyến các ngắt . . . .... . . .... . . . . . . 332

9.5.2 Giải thích về Mã cấu hình GIC . . . . .... . . . . . . 339

9.5.3 Ưu tiên ngắt và mặt nạ ngắt 9.5.4 Trình ... . .... . . ... . . . . . . . 340

diễn lập trình GIC . .. . .... . . ... . . . . . . . 340

9.6 Trình tự khởi động của ARM MPcores ... . . .... . .... . . ... . . . . . . . 340

9.6.1 Trình tự khởi động thô . . . . ... . . .... . .... . . ... . . . . . . . 341

9.6.2 Trình tự khởi động được hỗ trợ khởi động . . . . . . .... . . ... . . . . . . . 341

9.6.3 Khởi động SMP trên máy ảo 9.7 Ví dụ .... . .... . . .... . . . . . . 341

khởi động ARM SMP. .. . . ... . . .... . .... . . ... . . . . . . . 341

9.7.1 Ví dụ khởi động ARM SMP 1 9.7.2 . . . .... . .... . . ... . . . . . . . 342

Minh họa ví dụ khởi động SMP 1 9.7.3 Ví dụ khởi .... . . .... . . . . . . 346

động ARM SMP 2 9.7.4 Minh họa ví dụ khởi . . . .... . .... . . ... . . . . . . . 347

động ARM SMP 2 Các vùng quan trọng trong SMP . . ..... . . . . . . 348
9,8 .... . . ... . . .... . .... . . ... . . . . . . . 349

9.8.1 Triển khai các vùng quan trọng trong SMP. .. . . .... . . . . . . 349

9.8.2 Những thiếu sót trong hoạt động XCHG/SWAP. .. . . .... . . . . . . 350

9.8.3 Hướng dẫn đồng bộ hóa ARM cho SMP . .. . . ... . . . . . . . 350
Machine Translated by Google

xvi Nội dung

9.9 Nguyên tắc đồng bộ hóa trong SMP . 9.9.1 . . . . . ... . . .... . .... . . . . . . . 351
Khóa xoay . .... . . ... . . .... . . ... . . .... . .... . . . . . . . 351

9.9.2 Ví dụ về Spinlock . ... . . .... . . ... . . .... . .... . . . . . . . 352

9.9.3 Trình diễn khởi động SMP với Spinlock. . . .... . . . . . . . 353
9.9.4 Mutex trong SMP . . . ... . . .... . . ... . . .... . .... . . . . . . . 353

9.9.5 Triển khai Mutex bằng Spinlock . ... . .... . . . . . . . 354

9.9.6 Trình diễn Khởi động SMP bằng Khóa Mutex .... . . . . . . . 355
9.10 Bộ hẹn giờ toàn cầu và cục bộ . . ... . . .... . . ... . . .... . .... . . . . . . . 357
9.10.1 Trình diễn bộ định thời cục bộ trong SMP. . .... . .... . . . . . . . 359

9.11 Semaphores trong SMP . .. . . ... . . .... . . ... . . .... . .... . . . . . . . 359

9.11.1 Ứng dụng của Semaphores trong SMP. . . . .... . .... . . . . . . . 360

9.12 Khóa có điều kiện . .. . . ... . . .... . . ... . . .... . .... . . . . . . . 361

9.12.1 Spinlock có điều kiện . . . . .... . . ... . . .... . .... . . . . . . . 361


9.12.2 Mutex có điều kiện . .. . . .... . . ... . . .... . .... . . . . . . . 361

9.12.3 Hoạt động Semaphore có điều kiện . .. . . .... . .... . . . . . . . 362

9.13 Quản lý bộ nhớ trong SMP . . . .... . . ... . . .... . .... . . . . . . . 362

9.13.1 Mô hình quản lý bộ nhớ trong SMP. . . .... . .... . . . . . . . 363

9.13.2 Không gian VA thống nhất . . . . .... . . ... . . .... . .... . . . . . . . 363

9.13.3 Không gian VA không đồng nhất . . . . . . . . . . . . . . . . . . . . . . . . . . . . 365


9.13.4 Tính toán song song trong không gian VA không đồng nhất . . . . . . . . . . . . 368

9.14 Đa nhiệm trong SMP . .. . . ... . . .... . . ... . . .... . .... . . . . . . . 371

9.15 Hạt nhân SMP để quản lý quy trình . . . . ... . . .... . .... . . . . . . . 372
9.15.1 Tệp ts.s ... . . ... . . .... . . ... . . .... . .... . . . . . . . 373
9.15.2 Tệp tc .... . . ... . . .... . . ... . . .... . .... . . . . . . . 379
9.15.3 Tệp Kernel.c ... . . ... . . .... . . ... . . .... . .... . . . . . . . 381

9.15.4 Trình điều khiển thiết bị và Trình xử lý ngắt trong SMP. .. .. . . . . . . . . . 384

9.15.5 Trình diễn quản lý quy trình trong SMP. .... . . . . . . . 387

9.16 Hệ điều hành SMP Mục đích Chung . . ... . . .... . .... . . . . . . . 389

9.16.1 Tổ chức SMP_EOS. . .. . . ... . . .... . .... . . . . . . . 389

9.16.2 Cây tệp nguồn SMP_EOS 9.16.3 ... . . ... . . .... . .... . . . . . . . 390

Tệp hạt nhân SMP_EOS 9.16.4 Quản . . .... . . ... . . .... . .... . . . . . . . 390

lý quy trình trong SMP_EOS . .. . . .... . .... . . . . . . . 391


9.16.5 Bảo vệ cấu trúc dữ liệu hạt nhân trong SMP . . .... . .... . . . . . . . 392
9.16.6 Phòng chống bế tắc trong SMP . . . . ... . . .... . .... . . . . . . . 394

9.16.7 Điều chỉnh thuật toán UP cho SMP . . . . . . . . .... . .... . . . . . . . 395

9.16.8 Trình điều khiển thiết bị và Trình xử lý ngắt trong SMP. . . . . . . . . . . . . . 396

9.17 Hệ thống trình diễn SMP_EOS. . ... . . ... . . .... . .... . . . . . . . 397

9.17.1 Trình tự khởi động của SMP_EOS . . ... . . .... . .... . . . . . . . 397

9.17.2 Khả năng của SMP_EOS . ... . . ... . . .... . .... . . . . . . . 397

9.17.3 Trình diễn SMP_EOS . . . . ... . . .... . .... . . . . . . . 398

9.18 Tóm tắt . . .... . .... . . ... . . .... . . ... . . .... . .... . . . . . . . 399
Người giới thiệu. . .. . . .... . .... . . ... . . .... . . ... . . .... . .... . . . . . . . 399

10 hệ điều hành nhúng thời gian thực. . . . . ... . . .... . .... . . . . . . . 401

10.1 Khái niệm về RTOS. ... . . ... . . .... . . ... . . .... . .... . . . . . . . 401

10.2 Lập lịch tác vụ trong RTOS . ... . . .... . . ... . . .... . .... . . . . . . . 401

10.2.1 Lập kế hoạch đơn điệu tốc độ (RMS) . .. . . .... . .... . . . . . . . 402

10.2.2 Lập kế hoạch sớm nhất-thời hạn đầu tiên (EDF) . . . . . . .... . . . . . . . 402

10.2.3 Lập lịch trình đơn thời hạn (DMS). . .... . .... . . . . . . . 403

10.3 Đảo ngược ưu tiên . .... . . ... . . .... . . ... . . .... . .... . . . . . . . 403

10.4 Ngăn chặn đảo ngược ưu tiên . . . . .... . . ... . . .... . .... . . . . . . . 404

10.4.1 Mức trần ưu tiên. . . . . . . . .... . . ... . . .... . .... . . . . . . . 404

10.4.2 Kế thừa ưu tiên ... . . .... . . ... . . .... . .... . . . . . . . 404


Machine Translated by Google

Nội dung xvii

10.5 Khảo sát RTOS. . .... . . .... ..... . . .... ..... . . ... . . . . . . . 404
10.5.1 FreeRTOS . ... . . .... . . ... . . .... ..... . . ... . . . . . . . 404

10.5.2 MicroC/OS (µC/OS) . .. . . ... . . .... ..... . . ... . . . . . . . 405


10.5.3 NuttX . . . .... . . .... . . ... . . .... ..... . . ... . . . . . . . 405
10.5.4 VxWorks . .... . . .... . . ... . . .... ..... . . ... . . . . . . . 406

10.5.5 QNX . .. ..... . . .... ..... . . .... ..... . . ... . . . . . . . 407


10.5.6 Linux thời gian thực . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 407

10.5.7 Phê bình RTOS hiện tại . ... . . .... ..... . . ... . . . . . . . 409

10.6 Nguyên tắc thiết kế của RTOS. ... . . ... . . .... ..... . . ... . . .... . 412

10.6.1 Xử lý ngắt . .. . . ... . . .... ..... . . ... . . .... . 412

10.6.2 Quản lý tác vụ. . ... . . ... . . .... ..... . . ... . . .... . 412

10.6.3 Lập lịch tác vụ . . .... . . ... . . .... ..... . . ... . . .... . 412

10.6.4 Công cụ đồng bộ hóa .. . . ... . . .... ..... . . ... . . .... . 412
10.6.5 Giao tiếp tác vụ. . . . . ... . . .... ..... . . ... . . .... . 412

10.6.6 Quản lý bộ nhớ . . . . ... . . .... ..... . . ... . . .... . 412

10.6.7 Hệ thống tập tin . . . . . .... . . ... . . .... ..... . . ... . . .... . 412

10.6.8 Truy tìm và gỡ lỗi . . . ... . . .... ..... . . ... . . . . . . . 413

10.7 RTOS bộ xử lý đơn (UP_RTOS) . . ... . . .... ..... . . ... . . . . . . . 413

10.7.1 Quản lý tác vụ trong UP_RTOS . . .... ..... . . ... . . . . . . . 413

10.7.2 Đồng bộ hóa tác vụ trong UP_RTOS. . .. ..... . . ... . . .... . 414

10.7.3 Lập lịch tác vụ trong UP_RTOS . . . .... ..... . . ... . . .... . 414

10.7.4 Giao tiếp tác vụ trong UP_RTOS . ... ..... . . ... . . .... . 414

10.7.5 Bảo vệ các khu vực quan trọng . . . . .... ..... . . ... . . . . . . . 415

10.7.6 Hệ thống tệp và ghi nhật ký . . . . . . . .... ..... . . ... . . . . . . . 415

10.7.7 UP_RTOS với các tác vụ định kỳ tĩnh

và Lập lịch quay vòng . . . . .... ..... . . ... . . . . . . . 416

10.7.8 UP_RTOS với các tác vụ định kỳ tĩnh

và Lập kế hoạch ưu tiên. .. . . .... ..... . . ... . . . . . . . 426

10.7.9 UP_RTOS với tài nguyên chia sẻ tác vụ động. . . . . . . . . . 432

10.8 RTOS đa bộ xử lý (SMP_RTOS) . .. . . .... ..... . . ... . . . . . . . 440

10.8.1 Hạt nhân SMP_RTOS để quản lý tác vụ. ... . . ... . . . . . . . 440

10.8.2 Thích ứng UP_RTOS với SMP . ... . . .... ..... . . ... . . . . . . . 456

10.8.3 Các ngắt lồng nhau trong SMP_RTOS . .... ..... . . ... . . . . . . . 458

10.8.4 Lập lịch tác vụ ưu tiên trong SMP_RTOS. . . . . . . . . . . . . . . 461

10.8.5 Chuyển tác vụ bằng SGI . .. . . ... . . .... ..... . . ... . . . . . . . 461

10.8.6 Lập lịch tác vụ theo thời gian trong SMP_RTOS . .. . . . . . . . . . . . . 463

10.8.7 Lập lịch tác vụ ưu tiên trong SMP_RTOS. . . . . . . . . . . . . . . 465

10.8.8 Kế thừa ưu tiên 10.8.9 . . . . . . ... . . .... ..... . . ... . . . . . . . 468

Trình diễn hệ thống SMP_RTOS . ... . . . . . . . . . . . . 470

10.9 Tóm tắt . . .... ..... . . .... ..... . . .... ..... . . ... . . .... . 474
Người giới thiệu. . ... ..... ..... . . .... ..... . . .... ..... . . ... . . .... . 474

Mục lục . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 477


Machine Translated by Google

Giới thiệu về tác giả

KC Wang nhận bằng BSEE của Đại học Quốc gia Đài Loan vào
năm 1960 và bằng Tiến sĩ. bằng Kỹ sư Điện của Đại học
Northwestern, Evanston, Ill năm 1965. Ông hiện là Giáo sư
tại Trường Kỹ thuật Điện và Khoa học Máy tính tại Đại học
Bang Washington. Mối quan tâm học thuật của ông là Hệ điều
hành, Hệ thống phân tán và Điện toán song song.

xix
Machine Translated by Google

Giới thiệu
1

1.1 Về cuốn sách này

Cuốn sách này nói về thiết kế và triển khai các hệ điều hành nhúng và thời gian thực (Gajski et al. 1994). Nó bao gồm các
khái niệm và nguyên tắc cơ bản của hệ điều hành (OS) (Silberschatz và cộng sự 2009; Stallings 2011; Tanenbaum và Woodhull
2006; Wang 2015), kiến trúc hệ thống nhúng (ARM Architecture 2016), lập trình hệ thống nhúng (ARM Programming 2016), khái
niệm hệ thống thời gian thực và yêu cầu hệ thống thời gian thực (Dietrich và Walker 2015). Nó cho thấy cách áp dụng lý thuyết
và thực hành về HĐH vào việc thiết kế và triển khai hệ điều hành cho các hệ thống nhúng và thời gian thực.

1.2 Động cơ của cuốn sách này

Trong những ngày đầu, hầu hết các hệ thống nhúng đều được thiết kế cho các ứng dụng đặc biệt. Một hệ thống nhúng thường bao
gồm một bộ vi điều khiển và một vài thiết bị I/O, được thiết kế để giám sát một số cảm biến đầu vào và tạo tín hiệu để điều
khiển các thiết bị bên ngoài, chẳng hạn như bật đèn LED hoặc kích hoạt một số công tắc, v.v. chương trình của các hệ thống
nhúng đời đầu cũng rất đơn giản. Chúng thường được viết dưới dạng siêu vòng lặp hoặc cấu trúc chương trình hướng sự kiện đơn
giản. Tuy nhiên, khi sức mạnh tính toán của các hệ thống nhúng tăng lên trong những năm gần đây, các hệ thống nhúng đã trải
qua một bước nhảy vọt vượt bậc cả về độ phức tạp và lĩnh vực ứng dụng. Kết quả là, các phương pháp tiếp cận truyền thống để
thiết kế phần mềm cho các hệ thống nhúng không còn phù hợp nữa. Để đối phó với độ phức tạp ngày càng tăng của hệ thống và
nhu cầu về chức năng bổ sung, các hệ thống nhúng cần phần mềm mạnh hơn. Cho đến nay, nhiều hệ thống nhúng trên thực tế là
những máy tính có công suất cao với bộ xử lý đa lõi, bộ nhớ gigabyte và thiết bị lưu trữ nhiều gigabyte. Những hệ thống như
vậy nhằm mục đích chạy nhiều chương trình ứng dụng. Để phát huy hết tiềm năng của mình, các hệ thống nhúng hiện đại cần có
sự hỗ trợ của các hệ điều hành đa chức năng. Một ví dụ điển hình là sự phát triển của điện thoại di động trước đây thành điện
thoại thông minh hiện tại. Trong khi cái trước được thiết kế để thực hiện nhiệm vụ đơn giản là chỉ gọi hoặc nhận cuộc gọi
điện thoại thì cái sau có thể sử dụng bộ xử lý đa lõi và chạy các phiên bản Linux thích ứng, chẳng hạn như Android, để thực
hiện đa nhiệm. Xu hướng thiết kế phần mềm hệ thống nhúng hiện nay rõ ràng đang chuyển động theo hướng phát triển hệ điều
hành đa chức năng phù hợp với môi trường di động trong tương lai. Mục đích của cuốn sách này là trình bày cách áp dụng lý
thuyết và thực hành về hệ điều hành để phát triển hệ điều hành cho các hệ thống nhúng và thời gian thực.

1.3 Đối tượng khách quan và dự định

Mục tiêu của cuốn sách này là cung cấp một nền tảng phù hợp cho việc dạy và học lý thuyết cũng như thực hành về hệ điều hành
nhúng và thời gian thực. Nó bao gồm kiến trúc hệ thống nhúng, lập trình hệ thống nhúng, các khái niệm và nguyên tắc cơ bản
của hệ điều hành (OS) và hệ thống thời gian thực. Nó cho thấy cách áp dụng các nguyên tắc và kỹ thuật lập trình này vào việc
thiết kế và triển khai hệ điều hành thực cho cả hệ thống nhúng và thời gian thực. Cuốn sách này dành cho sinh viên khoa học
máy tính và các chuyên gia máy tính, những người muốn nghiên cứu các chi tiết bên trong của hệ điều hành nhúng và thời gian
thực. Nó phù hợp làm sách giáo khoa cho các khóa học về hệ thống nhúng và thời gian thực trong các chương trình giảng dạy
Khoa học/Kỹ thuật Máy tính theo định hướng kỹ thuật nhằm cố gắng cân bằng giữa lý thuyết và thực hành. Cuốn sách mang tính tiến hóa

© Springer International Publishing AG 2017 KC 1

Wang, Hệ điều hành nhúng và thời gian thực, DOI


10.1007/978-3-319-51517-5_1
Machine Translated by Google

2 1. Giới thiệu

phong cách, cùng với mã ví dụ chi tiết và hệ thống mẫu làm việc hoàn chỉnh, khiến nó đặc biệt phù hợp cho việc tự học của những
người đam mê máy tính.
Cuốn sách bao gồm toàn bộ phạm vi thiết kế phần mềm cho các hệ thống nhúng và thời gian thực, từ các chương trình điều khiển
siêu vòng lặp và điều khiển theo hướng sự kiện đơn giản cho các hệ thống bộ xử lý đơn (UP) đến các hệ điều hành Đa xử lý đối
xứng (SMP) hoàn chỉnh trên các hệ thống đa lõi. Nó cũng thích hợp cho nghiên cứu nâng cao về hệ điều hành nhúng và thời gian thực.

1.4 Đặc điểm độc đáo của cuốn sách này

Cuốn sách này có nhiều điểm độc đáo, giúp phân biệt nó với những cuốn sách khác.

1. Cuốn sách này là cuốn sách khép kín. Nó bao gồm tất cả các thông tin cơ bản và nền tảng để nghiên cứu các hệ thống nhúng,
hệ thống thời gian thực và hệ điều hành nói chung. Chúng bao gồm kiến trúc ARM, hướng dẫn ARM và lập trình (ARM architecture
2016; ARM926EJ-S 2008), chuỗi công cụ để phát triển chương trình (ARM toolchain 2016), máy ảo (QEMU Emulators 2010) để triển
khai và thử nghiệm phần mềm, hình ảnh thực thi chương trình, quy ước gọi hàm, cách sử dụng ngăn xếp trong thời gian chạy và
liên kết các chương trình C với mã hợp ngữ.
2. Ngắt và xử lý ngắt là điều cần thiết đối với các hệ thống nhúng. Cuốn sách này trình bày chi tiết về phần cứng ngắt và quá
trình xử lý ngắt. Chúng bao gồm các ngắt không có vectơ, ngắt có vectơ (ARM PL190 2004), ngắt không lồng nhau, ngắt lồng
nhau (Ngắt lồng nhau 2011) và lập trình Bộ điều khiển ngắt chung (GIC) (ARM GIC 2013) trong ARM MPcore (ARM Cortex-A9 MPCore
2012) dựa trên hệ thống. Nó cho thấy cách áp dụng các nguyên tắc xử lý ngắt để phát triển trình điều khiển thiết bị điều
khiển ngắt và hệ thống nhúng hướng sự kiện.
3. Cuốn sách trình bày một khuôn khổ chung để phát triển các trình điều khiển thiết bị điều khiển ngắt, nhấn mạnh vào sự tương
tác và đồng bộ hóa giữa các trình xử lý và quy trình ngắt. Đối với mỗi thiết bị, nó giải thích các nguyên tắc hoạt động và
kỹ thuật lập trình trước khi hiển thị cách triển khai trình điều khiển thực tế và nó trình diễn các trình điều khiển thiết
bị bằng các chương trình mẫu hoạt động hoàn chỉnh.
4. Cuốn sách trình bày cách thiết kế và triển khai hệ điều hành hoàn chỉnh cho các hệ thống nhúng theo từng bước tăng dần. Đầu
tiên, nó phát triển một kernel đa nhiệm đơn giản để hỗ trợ quản lý tiến trình và đồng bộ hóa tiến trình. Sau đó, nó kết hợp
phần cứng Bộ quản lý bộ nhớ (MMU) (ARM MMU 2008) vào hệ thống để cung cấp ánh xạ địa chỉ ảo và mở rộng hạt nhân đơn giản để
hỗ trợ các quy trình chế độ người dùng và lệnh gọi hệ thống. Sau đó, nó bổ sung thêm lịch trình quy trình, xử lý tín hiệu,
hệ thống tệp và giao diện người dùng vào hệ thống, biến nó thành một hệ điều hành hoàn chỉnh. Phong cách tiến triển của cuốn
sách giúp người đọc hiểu rõ hơn về tài liệu.
5. Chương 9 trình bày chi tiết về các hệ thống nhúng Đa xử lý đối xứng (SMP) (Intel 1997) . Đầu tiên, nó giải thích các yêu cầu
của hệ thống SMP và so sánh kiến trúc ARM MPcore (ARM11 2008; ARM Cortex-A9 MPCore 2012) với kiến trúc hệ thống SMP của
Intel. Sau đó, nó mô tả các tính năng SMP của bộ xử lý ARM MPcore, bao gồm SCU và GIC để định tuyến các ngắt cũng như liên
lạc và đồng bộ hóa giữa các bộ xử lý bằng Ngắt do phần mềm tạo ra (SGI). Nó sử dụng một loạt các ví dụ lập trình để chỉ ra
cách khởi động bộ xử lý ARM MPcore và chỉ ra nhu cầu đồng bộ hóa trong môi trường SMP. Sau đó, nó giải thích các hướng dẫn
ARM LDREX/ STREX và các rào cản bộ nhớ, đồng thời sử dụng chúng để triển khai các spinlock, mutexes và semaphores để đồng
bộ hóa quy trình trong hệ thống SMP. Nó trình bày một phương pháp chung để thiết kế hạt nhân SMP và chỉ ra cách áp dụng các
nguyên tắc để điều chỉnh hạt nhân bộ xử lý đơn (UP) cho SMP. Ngoài ra, nó cũng cho thấy cách sử dụng các thuật toán song
song để quản lý quy trình và tài nguyên nhằm cải thiện tính đồng thời và hiệu quả trong các hệ thống SMP.

6. Chương 10 bao gồm các hệ điều hành thời gian thực (RTOS). Nó giới thiệu các khái niệm và yêu cầu của hệ thống thời gian
thực. Nó bao gồm các loại thuật toán lập lịch tác vụ khác nhau trong RTOS và nó cho thấy cách xử lý việc đảo ngược mức độ
ưu tiên và ưu tiên nhiệm vụ trong các hệ thống thời gian thực. Nó bao gồm các nghiên cứu điển hình về một số RTOS phổ biến
và xây dựng một bộ hướng dẫn chung cho thiết kế RTOS. Nó cho thấy thiết kế và triển khai UP_RTOS cho các hệ thống bộ xử lý
đơn (UP). Sau đó, nó mở rộng UP_RTOS thành SMP_RTOS cho SMP, hỗ trợ các ngắt lồng nhau, lập lịch tác vụ ưu tiên, kế thừa ưu
tiên và đồng bộ hóa giữa các bộ xử lý bằng SGI.
7. Xuyên suốt cuốn sách, nó sử dụng các hệ thống mẫu hoàn chỉnh để thể hiện các nguyên tắc thiết kế và kỹ thuật triển khai. Nó
sử dụng chuỗi công cụ ARM trong Ubuntu (15.10) Linux để phát triển phần mềm cho các hệ thống nhúng và nó sử dụng các máy ảo
ARM mô phỏng trong QEMU làm nền tảng để triển khai và thử nghiệm.
Machine Translated by Google

1.5 Nội Dung Sách 3

1.5 Nội Dung Sách

Cuốn sách này được tổ chức như sau.

chương 2 bao gồm kiến trúc ARM, hướng dẫn ARM, lập trình ARM và phát triển các chương trình để thực thi trên máy ảo ARM. Chúng bao gồm các

chế độ bộ xử lý ARM, ngân hàng đăng ký ở các chế độ khác nhau, hướng dẫn và lập trình cơ bản trong lắp ráp ARM. Nó giới thiệu chuỗi công cụ

ARM trong Ubuntu (15.10) Linux và mô phỏng các máy ảo ARM trong QEMU. Nó cho thấy cách sử dụng chuỗi công cụ ARM để phát triển các chương

trình thực thi trên máy ảo ARM Versatilepb bằng một loạt các ví dụ lập trình. Nó giải thích quy ước gọi hàm trong C và chỉ ra cách giao tiếp

mã hợp ngữ với các chương trình C. Sau đó, nó phát triển trình điều khiển UART đơn giản cho I/O trên các cổng nối tiếp và trình điều khiển LCD

để hiển thị cả hình ảnh đồ họa và văn bản. Nó cũng cho thấy sự phát triển của hàm printf() chung để in được định dạng tới các thiết bị đầu ra

hỗ trợ thao tác print char cơ bản.

Chương 3 bao gồm các ngắt và xử lý ngoại lệ. Nó mô tả các chế độ hoạt động của bộ xử lý ARM, các loại ngoại lệ và vectơ ngoại lệ. Nó giải

thích chi tiết các chức năng của bộ điều khiển ngắt và các nguyên tắc xử lý ngắt. Sau đó, nó áp dụng các nguyên tắc xử lý ngắt để thiết kế và

triển khai các trình điều khiển thiết bị điều khiển ngắt. Chúng bao gồm trình điều khiển cho bộ hẹn giờ, bàn phím, UART và thẻ SD (SDC 2016)

và nó thể hiện trình điều khiển thiết bị bằng các chương trình mẫu. Nó giải thích những ưu điểm của ngắt có vectơ so với các ngắt không có

vectơ. Nó cho thấy cách định cấu hình Bộ điều khiển ngắt vectơ (VIC) cho các ngắt có vectơ và trình bày cách xử lý các ngắt có vectơ bằng các

chương trình ví dụ. Nó cũng giải thích các nguyên tắc của các ngắt lồng nhau và trình bày cách xử lý các ngắt lồng nhau bằng các chương trình

ví dụ.

Chương 4 bao gồm các mô hình của hệ thống nhúng. Đầu tiên, nó giải thích và trình diễn mô hình hệ thống siêu vòng lặp đơn giản và chỉ ra

những hạn chế của nó. Sau đó, nó thảo luận về mô hình hướng sự kiện và trình diễn cả hệ thống hướng sự kiện định kỳ và không đồng bộ bằng các

chương trình mẫu. Để vượt xa các mô hình hệ thống siêu vòng lặp và hướng sự kiện đơn giản, nó chứng minh nhu cầu về các quy trình hoặc tác vụ

trong các hệ thống nhúng. Sau đó, nó giới thiệu các loại mô hình quy trình khác nhau, được sử dụng làm mô hình phát triển hệ thống nhúng trong

cuốn sách. Cuối cùng, nó trình bày các phương pháp chính thức để thiết kế hệ thống nhúng. Nó minh họa mô hình Máy trạng thái hữu hạn (FSM)

(Katz và Borriello 2005) bằng một ví dụ thiết kế và triển khai hoàn chỉnh một cách chi tiết.

Chương 5 bao gồm quản lý quy trình. Nó giới thiệu khái niệm quy trình và nguyên tắc cơ bản của đa nhiệm. Nó thể hiện kỹ thuật đa nhiệm

bằng cách chuyển đổi ngữ cảnh. Nó cho thấy cách tạo các quy trình một cách linh hoạt và thảo luận về các mục tiêu, chính sách và thuật toán

lập kế hoạch quy trình. Nó bao gồm việc đồng bộ hóa quy trình và giải thích các loại cơ chế đồng bộ hóa quy trình khác nhau, bao gồm ngủ/thức

dậy, mutexes và semaphores. Nó cho thấy cách sử dụng đồng bộ hóa quy trình để triển khai các hệ thống nhúng theo hướng sự kiện. Nó thảo luận

về các sơ đồ khác nhau để liên lạc giữa các quá trình, bao gồm bộ nhớ dùng chung, đường dẫn và truyền tin nhắn. Nó cho thấy cách tích hợp các

khái niệm và kỹ thuật này để triển khai hạt nhân bộ xử lý đơn (UP) để quản lý quy trình, đồng thời nó trình bày các yêu cầu hệ thống và kỹ

thuật lập trình cho cả việc lập lịch trình quy trình không ưu tiên và ưu tiên. Nhân UP đóng vai trò là nền tảng để phát triển các hệ điều hành

hoàn chỉnh trong các chương sau.

Chương 6 bao gồm đơn vị quản lý bộ nhớ ARM (MMU) và ánh xạ không gian địa chỉ ảo. Nó giải thích chi tiết về ARM MMU và chỉ ra cách định

cấu hình MMU để ánh xạ địa chỉ ảo bằng cách sử dụng cả phân trang một cấp và hai cấp. Ngoài ra, nó cũng giải thích sự khác biệt giữa ánh xạ

không gian VA thấp và ánh xạ không gian VA cao cũng như ý nghĩa của chúng đối với việc triển khai hệ thống. Thay vì chỉ thảo luận về các

nguyên tắc quản lý bộ nhớ, nó còn trình bày các loại sơ đồ ánh xạ địa chỉ ảo khác nhau bằng các chương trình ví dụ hoạt động hoàn chỉnh.

Chương 7 bao gồm các quy trình chế độ người dùng và các cuộc gọi hệ thống. Đầu tiên nó mở rộng kernel UP cơ bản của Chương 5 để hỗ trợ các

chức năng quản lý quy trình bổ sung, bao gồm tạo quy trình động, chấm dứt quy trình, đồng bộ hóa quy trình và chờ kết thúc quy trình con. Sau

đó, nó mở rộng kernel cơ bản để hỗ trợ các tiến trình ở chế độ người dùng. Nó cho thấy cách sử dụng quản lý bộ nhớ để cung cấp cho mỗi quy

trình một không gian địa chỉ ảo ở chế độ người dùng riêng tư, tách biệt với các quy trình khác và được bảo vệ bởi phần cứng MMU. Nó bao gồm và

trình bày các loại sơ đồ quản lý bộ nhớ khác nhau, bao gồm các phần một cấp và phân trang tĩnh và động hai cấp. Nó bao gồm các khái niệm và kỹ

thuật nâng cao về fork, exec, vfork và thread. Ngoài ra, nó còn cho thấy cách sử dụng thẻ SD để lưu trữ cả tệp hình ảnh chế độ người dùng và

hạt nhân trong hệ thống tệp SDC. Nó cũng chỉ ra cách khởi động kernel hệ thống từ các phân vùng SDC. Chương này đóng vai trò là nền tảng cho

việc thiết kế và triển khai hệ điều hành có mục đích chung cho các hệ thống nhúng.

Chương 8 trình bày một hệ điều hành có mục đích chung (GPOS) đầy đủ chức năng, được ký hiệu là EOS, dành cho bộ xử lý đơn (UP) dựa trên ARM

những hệ thống nhúng. Sau đây là bản tóm tắt ngắn gọn về tổ chức và khả năng của hệ thống EOS.
Machine Translated by Google

4 1. Giới thiệu

1. Hình ảnh hệ thống: Hình ảnh hạt nhân có khả năng khởi động và các tệp thực thi ở chế độ Người dùng được chuỗi công cụ ARM tạo ra từ

cây nguồn trong Ubuntu (15.10) Linux và nằm trong hệ thống tệp (EXT2 2001) trên phân vùng SDC. SDC chứa bộ khởi động giai đoạn 1 và

giai đoạn 2 để khởi động hình ảnh hạt nhân từ phân vùng SDC. Sau khi khởi động, hạt nhân EOS sẽ gắn phân vùng SDC làm hệ thống tệp

gốc.

2. Quy trình: Hệ thống hỗ trợ NPROC = 64 quy trình và NTHRED = 128 luồng cho mỗi quy trình, cả hai đều có thể tăng lên nếu cần. Mỗi tiến

trình (ngoại trừ tiến trình nhàn rỗi P0) chạy ở chế độ Kernel hoặc chế độ Người dùng. Việc quản lý bộ nhớ của hình ảnh quá trình được

thực hiện bằng phân trang động 2 cấp độ. Lập kế hoạch quy trình theo mức độ ưu tiên động và thời gian. Nó hỗ trợ giao tiếp giữa các

quá trình bằng đường ống và truyền tin nhắn. Nhân EOS hỗ trợ các chức năng quản lý quy trình fork, exec, vfork, thread, exit và chờ
kết thúc con.

3. Trình điều khiển thiết bị: Nó chứa trình điều khiển thiết bị cho các thiết bị I/O được sử dụng phổ biến nhất, bao gồm màn hình LCD, bộ

hẹn giờ, bàn phím, UART và SDC.

4. Hệ thống tệp: EOS hỗ trợ hệ thống tệp EXT2 hoàn toàn tương thích với Linux. Nó hiển thị các nguyên tắc hoạt động của tệp, đường dẫn

điều khiển và luồng dữ liệu từ không gian người dùng đến không gian kernel cho đến cấp trình điều khiển thiết bị. Nó cho thấy tổ chức

nội bộ của hệ thống tệp và nó mô tả chi tiết việc triển khai một hệ thống tệp hoàn chỉnh.

5. Dịch vụ hẹn giờ, ngoại lệ và xử lý tín hiệu: Nó cung cấp các chức năng dịch vụ hẹn giờ và thống nhất việc xử lý ngoại lệ với xử lý tín

hiệu, cho phép người dùng cài đặt bộ bắt tín hiệu để xử lý các ngoại lệ trong chế độ Người dùng.

6. Giao diện người dùng: Nó hỗ trợ đăng nhập nhiều người dùng vào bảng điều khiển và thiết bị đầu cuối UART. Trình thông dịch lệnh sh hỗ

trợ thực thi các lệnh đơn giản với chuyển hướng I/O, cũng như nhiều lệnh được kết nối bằng đường ống.

7. Chuyển: Hệ thống EOS chạy trên nhiều máy ảo ARM theo QEMU, chủ yếu là để thuận tiện. Nó cũng phải chạy trên bo mạch hệ thống dựa trên

ARM thực sự hỗ trợ các thiết bị I/O phù hợp. Việc chuyển EOS sang một số hệ thống dựa trên ARM phổ biến, ví dụ như Raspberry PI-2 hiện

đang được tiến hành. Kế hoạch là cung cấp cho người đọc tải xuống ngay khi nó sẵn sàng.

Chương 9 bao gồm đa xử lý trong các hệ thống nhúng. Nó giải thích các yêu cầu của hệ thống Đa xử lý đối xứng (SMP) và so sánh cách

tiếp cận SMP của Intel với cách tiếp cận của ARM. Nó liệt kê các bộ xử lý ARM MPcore và mô tả các thành phần cũng như chức năng của bộ xử

lý ARM MPcore hỗ trợ SMP. Tất cả các hệ thống dựa trên ARM MPcore đều phụ thuộc vào Bộ điều khiển ngắt chung (GIC) để định tuyến các ngắt

và liên lạc giữa các bộ xử lý. Nó cho thấy cách cấu hình GIC để định tuyến các ngắt và trình bày cách lập trình GIC bằng các ví dụ. Nó

cho thấy cách khởi động ARM MPcores và chỉ ra nhu cầu đồng bộ hóa trong môi trường SMP. Nó cho thấy cách sử dụng các hướng dẫn kiểm tra

và thiết lập cổ điển hoặc hướng dẫn tương đương để triển khai các bản cập nhật nguyên tử và các vùng quan trọng, đồng thời chỉ ra những

thiếu sót của chúng. Sau đó, nó giải thích các tính năng mới của ARM MPcores hỗ trợ SMP. Chúng bao gồm các hướng dẫn ARM LDRES/STRES và

các rào cản bộ nhớ. Nó cho thấy cách sử dụng các tính năng mới của bộ xử lý ARM MPcore để triển khai spinlock, mutexes và semaphores để

đồng bộ hóa quy trình trong SMP. Nó xác định các spinlock, mutex và semaphor có điều kiện, đồng thời chỉ ra cách sử dụng chúng để ngăn

chặn bế tắc trong hạt nhân SMP. Nó cũng bao gồm các tính năng bổ sung của ARM MMU cho SMP. Nó trình bày một phương pháp chung để điều

chỉnh nhân hệ điều hành đơn bộ xử lý thành SMP. Sau đó, nó áp dụng các nguyên tắc để phát triển một SMP_EOS hoàn chỉnh cho các hệ thống

SMP nhúng và nó thể hiện khả năng của hệ thống SMP_EOS bằng các chương trình mẫu.

Chương 10 bao gồm các hệ điều hành thời gian thực (RTOS). Nó giới thiệu các khái niệm và yêu cầu của hệ thống thời gian thực.

Nó bao gồm các loại thuật toán lập lịch tác vụ khác nhau trong RTOS, bao gồm RMS, EDF và DMS. Nó giải thích vấn đề đảo ngược mức độ ưu

tiên do lập kế hoạch nhiệm vụ ưu tiên và chỉ ra cách xử lý việc đảo ngược mức độ ưu tiên và ưu tiên nhiệm vụ. Nó bao gồm các nghiên cứu

điển hình về một số hệ điều hành thời gian thực phổ biến và trình bày một bộ hướng dẫn chung về thiết kế RTOS. Nó cho thấy thiết kế và

triển khai UP_RTOS cho các hệ thống bộ xử lý đơn (UP). Sau đó, nó mở rộng UP_RTOS thành SMP_RTOS, hỗ trợ các ngắt lồng nhau, lập lịch tác

vụ ưu tiên, kế thừa ưu tiên và đồng bộ hóa giữa các bộ xử lý bằng SGI.

1.6 Sử dụng cuốn sách này làm sách giáo khoa cho hệ thống nhúng

Cuốn sách này phù hợp làm sách giáo khoa cho các khóa học định hướng kỹ thuật về hệ thống nhúng và thời gian thực trong chương trình

giảng dạy Khoa học/Kỹ thuật Máy tính nhằm cố gắng cân bằng giữa lý thuyết và thực hành. Khóa học kéo dài một học kỳ dựa trên cuốn sách

này có thể bao gồm các chủ đề sau.


Machine Translated by Google

1.6 Sử dụng cuốn sách này làm sách giáo khoa cho hệ thống nhúng 5

1. Giới thiệu về hệ thống nhúng, kiến trúc ARM, lập trình ARM cơ bản, chuỗi công cụ ARM và phát triển phần mềm cho hệ thống nhúng, mã lắp

ráp giao diện với các chương trình C, hình ảnh thực thi và cách sử dụng ngăn xếp thời gian chạy, thực thi chương trình trên máy ảo ARM,

thiết bị đơn giản trình điều khiển cho I/O (Chương 2).

2. Các ngoại lệ và ngắt, xử lý ngắt, thiết kế và triển khai trình điều khiển thiết bị điều khiển ngắt (Chương 3).

3. Mô hình hệ thống nhúng; siêu vòng lặp, các hệ thống nhúng hướng sự kiện, các mô hình quy trình và hình thức của các hệ thống nhúng

(Chương 4).

4. Quản lý quy trình và đồng bộ hóa quy trình (Chương 5).

5. Quản lý bộ nhớ, ánh xạ địa chỉ ảo và bảo vệ bộ nhớ (Chương 6).

6. Chế độ hạt nhân và các quy trình ở chế độ người dùng, các lệnh gọi hệ thống (Chương 7).

7. Giới thiệu về hệ thống nhúng thời gian thực (các phần của Chương 10).

8. Giới thiệu về hệ thống nhúng SMP (các phần của Chương 9 và 10).

1.7 Sử dụng cuốn sách này làm sách giáo khoa về hệ điều hành

Cuốn sách này cũng thích hợp làm sách giáo khoa cho các khóa học định hướng kỹ thuật về hệ điều hành có mục đích chung.

Khóa học kéo dài một học kỳ dựa trên cuốn sách này có thể bao gồm các chủ đề sau.

1. Kiến trúc máy tính và lập trình hệ thống: kiến trúc ARM, lệnh và lập trình ARM, chuỗi công cụ ARM, máy ảo, giao diện hợp ngữ với C, hình

ảnh thời gian chạy và cách sử dụng ngăn xếp, trình điều khiển thiết bị đơn giản (Chương 2).

2. Các ngoại lệ và ngắt, xử lý ngắt và trình điều khiển thiết bị điều khiển ngắt (Chương 3).

3. Quản lý quy trình và đồng bộ hóa quy trình (Chương 5).

4. Quản lý bộ nhớ, ánh xạ địa chỉ ảo và bảo vệ bộ nhớ (Chương 6).

5. Chế độ hạt nhân và các quy trình ở chế độ người dùng, các lệnh gọi hệ thống (Chương 7).

6. Nhân hệ điều hành cho mục đích chung, quản lý quy trình, quản lý bộ nhớ, trình điều khiển thiết bị, hệ thống tệp, tín hiệu

xử lý và giao diện người dùng (Chương 8).

7. Giới thiệu về SMP, giao tiếp giữa các bộ xử lý và đồng bộ hóa các bộ xử lý đa lõi, đồng bộ hóa quy trình trong SMP, thiết kế nhân SMP

và hệ điều hành SMP (Chương 9).

8. Giới thiệu về hệ thống thời gian thực (các phần của Chương 10).

Phần vấn đề của mỗi chương bao gồm các câu hỏi được thiết kế để xem xét các khái niệm và nguyên tắc được trình bày trong chương. Trong

khi một số câu hỏi chỉ liên quan đến những sửa đổi đơn giản của các chương trình ví dụ để cho học sinh thử nghiệm các thiết kế và cách triển

khai thay thế, thì nhiều câu hỏi khác lại phù hợp với các dự án lập trình nâng cao.

1.8 Sử dụng cuốn sách này để tự học

Đánh giá từ số lượng lớn các dự án phát triển hệ điều hành và nhiều trang web phổ biến về các hệ thống nhúng và thời gian thực được đăng

trên Internet và những người theo dõi nhiệt tình của họ, có một số lượng lớn những người đam mê máy tính mong muốn tìm hiểu khía cạnh thực

tế của hệ thống nhúng và thực tế. hệ điều hành thời gian. Phong cách tiến hóa của cuốn sách này, cùng với các chương trình hệ thống trình

diễn và mã phong phú, khiến nó đặc biệt phù hợp cho việc tự học. Hy vọng rằng cuốn sách này sẽ hữu ích và mang lại lợi ích cho những độc giả
như vậy.

Người giới thiệu

Kiến trúc ARM 2016: http://www.arm.products/processors/instruction-set-architectures, Trung tâm thông tin ARM, 2016 ARM Cortex-A9
MPCore: Sổ tay tham khảo kỹ thuật Bản sửa đổi: r4p1, Trung tâm thông tin ARM, 2012 ARM GIC: Sổ tay tham khảo
kỹ thuật Bộ điều khiển ngắt chung ARM (PL390), Trung tâm thông tin ARM, 2013 ARM MMU: ARM926EJ-S, ARM946E -S Hướng dẫn
tham khảo kỹ thuật, Trung tâm thông tin ARM, 2008
Machine Translated by Google

6 1. Giới thiệu

Lập trình ARM 2016: "Lập trình ngôn ngữ hội ARM", http://www.peter-cockerell.net/aalp/html/frames.html, 2016 ARM PL190 2004: Bộ
điều khiển ngắt vectơ PrimeCell (PL190), http://infocenter.arm.com/help/topic/com.arm.doc.ddi0181e/DDI0181.pdf,
2004

Chuỗi công cụ ARM 2016: http://gnutoolchains.com/arm-eabi,


2016 ARM11 2008: Hướng dẫn tham khảo kỹ thuật bộ xử lý ARM11 MPcore, r2p0, Trung tâm thông tin ARM, 2008
ARM926EJ-S 2008: "Sổ tay tham khảo kỹ thuật ARM926EJ-S", Trung tâm thông tin ARM, 2008 Dietrich,
S., Walker, D., 2015 "The sự phát triển của Linux thời gian thực", http://www.cse.nd.edu/courses/cse60463/www/amatta2.pdf, 2015 EXT2
2001: www.nongnu.org/ext2-doc/ext2.html, 2001 Gajski,
DD, Vahid, F, Narayan, S, Gong, J, 1994 "Đặc điểm kỹ thuật và thiết kế của hệ thống nhúng", PTR Prentice Hall, 1994 Intel:
Đặc tả đa bộ xử lý, v1.4, Intel, 1997 Katz, RH và
G. Borriello 2005, "Thiết kế logic đương đại", tái bản lần 2, Pearson, 2005.
Ngắt lồng nhau 2011: Ngắt lồng nhau, Trung tâm thông tin ARM, 2011 Trình
mô phỏng QEMU 2010: "Tài liệu người dùng trình mô phỏng QEMU", http://wiki.qemu.org/download/qemu-doc.htm, 2010.
SDC: Thẻ kỹ thuật số an toàn: Tổng quan về tiêu chuẩn SD-Hiệp hội SD https://www.sdcard.org/developers/overview, 2016
Silberschatz, A., PA Galvin, PA, Gagne, G, "Khái niệm hệ điều hành, Phiên bản thứ 8", John Wiley & Sons, Inc. 2009
Stallings, W. "Hệ điều hành: Nội bộ và Nguyên tắc Thiết kế (Ấn bản thứ 7 )", Prentice Hall, 2011
Tanenbaum, AS, Woodhull, AS, 2006 "Hệ điều hành, Thiết kế và Triển khai, Ấn bản thứ ba", Prentice Hall, 2006 Wang, KC, 2015
"Thiết kế và Triển khai Hệ điều hành MTX, Nhà xuất bản Quốc tế Springer AG, 2015
Machine Translated by Google

Kiến trúc và lập trình ARM


2

ARM (ARM Architecture 2016) là một họ bộ vi xử lý Điện toán tập lệnh rút gọn (RISC) được phát triển dành riêng cho môi
trường điện toán di động và nhúng. Do kích thước nhỏ và yêu cầu năng lượng thấp, bộ xử lý ARM đã trở thành bộ xử lý được
sử dụng rộng rãi nhất trong các thiết bị di động, ví dụ như điện thoại thông minh và hệ thống nhúng.
Hiện nay, hầu hết các hệ thống nhúng đều dựa trên bộ xử lý ARM. Trong nhiều trường hợp, lập trình hệ thống nhúng gần như
đồng nghĩa với lập trình bộ xử lý ARM. Vì lý do này, chúng tôi cũng sẽ sử dụng bộ xử lý ARM để thiết kế và triển khai các
hệ thống nhúng trong cuốn sách này. Tùy thuộc vào thời gian phát hành, bộ xử lý ARM có thể được phân loại thành lõi cổ
điển và lõi Cortex mới hơn (kể từ năm 2005). Tùy thuộc vào khả năng và ứng dụng dự định, lõi ARM Cortex có thể được phân
thành ba loại (ARM Cortex 2016).

.Dòng Cortex-M: đây là các bộ xử lý hướng đến vi điều khiển dành cho các ứng dụng Bộ điều khiển vi mô (MCU) và Hệ thống
trên chip (SoC).
.Dòng Cortex-R: đây là các bộ xử lý nhúng dành cho các ứng dụng điều khiển và xử lý tín hiệu theo thời gian thực.
.Dòng Cortex-A: đây là các bộ xử lý ứng dụng dành cho các ứng dụng có mục đích chung, chẳng hạn như hệ thống nhúng với hệ
điều hành đầy đủ tính năng.

Bộ xử lý dòng ARM Cortex-A là bộ xử lý ARM mạnh nhất, bao gồm lõi đơn Cortex-A8 (ARM Cortex-A8 2010) và Cortex A9-MPcore
(ARM Cortex A9 MPcore 2016) với tối đa 4 CPU. Do khả năng tiên tiến của chúng, hầu hết các hệ thống nhúng gần đây đều dựa
trên bộ xử lý dòng ARM Cortex-A. Mặt khác, cũng có một số lượng lớn các hệ thống nhúng dành cho các ứng dụng chuyên dụng
dựa trên lõi ARM cổ điển, được chứng minh là rất tiết kiệm chi phí. Trong cuốn sách này, chúng ta sẽ đề cập đến cả bộ xử
lý dòng ARM-A cổ điển và cổ điển. Cụ thể, chúng ta sẽ sử dụng lõi ARM926EJ-S cổ điển (ARM926EJ-ST 2008, ARM926EJ-ST 2010)
cho các hệ thống CPU đơn và Cortex A9-MPcore cho các hệ thống đa bộ xử lý. Lý do chính để chọn các lõi ARM này là vì chúng
có sẵn dưới dạng máy ảo mô phỏng (VM). Mục tiêu chính của cuốn sách này là trình bày cách thiết kế và triển khai các hệ
thống nhúng theo cách tiếp cận tích hợp. Ngoài việc trình bày lý thuyết và nguyên tắc, nó còn chỉ ra cách áp dụng chúng
vào việc thiết kế và triển khai các hệ thống nhúng bằng các ví dụ lập trình. Vì hầu hết người đọc có thể không có quyền
truy cập vào các hệ thống dựa trên ARM thực, nên chúng tôi sẽ sử dụng các máy ảo ARM được mô phỏng trong QEMU để triển
khai và thử nghiệm. Trong chương này, chúng ta sẽ đề cập đến các chủ đề sau.

. kiến trúc ARM


. Hướng dẫn ARM và lập trình cơ bản trong hợp ngữ ARM
. Mã hợp ngữ giao diện với chương trình C
. Chuỗi công cụ ARM cho các chương trình liên kết biên dịch (chéo)
. Trình giả lập hệ thống ARM và chạy chương trình trên máy ảo ARM

. Phát triển trình điều khiển thiết bị I/O đơn giản. Chúng bao gồm trình điều khiển UART cho cổng nối tiếp và trình điều khiển LCD để hiển thị cả hình ảnh
và văn bản.

© Springer International Publishing AG 2017 7


KC Wang, Hệ điều hành nhúng và thời gian thực, DOI
10.1007/978-3-319-51517-5_2
Machine Translated by Google

số 8
2 Kiến trúc và lập trình ARM

2.1 Chế độ bộ xử lý ARM

Bộ xử lý ARM có bảy (7) chế độ hoạt động, được chỉ định bởi 5 bit chế độ [4:0] trong Thanh ghi trạng thái bộ xử lý hiện tại (CPSR). Bảy chế độ

xử lý ARM là:

Chế độ USR: Chế độ người dùng không có đặc quyền

Chế độ SYS: Chế độ hệ thống sử dụng cùng bộ thanh ghi như chế độ Người dùng

Chế độ FIQ: Chế độ xử lý yêu cầu ngắt nhanh

Chế độ IRQ: chế độ xử lý yêu cầu ngắt bình thường

Chế độ SVC: Chế độ giám sát khi đặt lại hoặc SWI (Ngắt phần mềm)

Chế độ ABT: chế độ hủy bỏ ngoại lệ dữ liệu

Chế độ UND: chế độ ngoại lệ hướng dẫn không xác định

2.2 Thanh ghi CPU ARM

Bộ xử lý ARM có tổng cộng 37 thanh ghi, tất cả đều rộng 32 bit. Bao gồm các

1 bộ đếm chương trình chuyên dụng (PC)

1 thanh ghi trạng thái chương trình hiện tại chuyên dụng

(CPSR) 5 thanh ghi trạng thái chương trình đã lưu chuyên

dụng (SPSR) 30 thanh ghi mục đích chung

Các sổ đăng ký được sắp xếp thành nhiều ngân hàng. Khả năng truy cập của các thanh ghi ngân hàng được điều chỉnh bởi chế độ bộ xử lý.
Ở mỗi chế độ, CPU ARM có thể truy cập

Một bộ thanh ghi R0-R12 cụ thể

Một R13 (con trỏ ngăn xếp), R14 (thanh ghi liên kết) và SPSR (trạng thái chương trình đã lưu) cụ thể

Cùng R15 (bộ đếm chương trình) và CPSR (thanh ghi trạng thái chương trình hiện tại)

2.2.1 Sổ đăng ký chung

Hình 2.1 cho thấy cách tổ chức các thanh ghi chung trong bộ xử lý ARM.

Hình 2.1 Đăng ký ngân hàng trong bộ xử lý ARM


Machine Translated by Google

2.2 Thanh ghi CPU ARM 9

Hình 2.2 Thanh ghi trạng thái của bộ xử lý ARM

Chế độ Người dùng và Hệ thống chia sẻ cùng một bộ thanh ghi. Các thanh ghi R0-R12 giống nhau ở tất cả các chế độ, ngoại
trừ chế độ FIQ, có các thanh ghi R8-R12 riêng. Mỗi chế độ có con trỏ ngăn xếp (R13) và thanh ghi liên kết (R14) riêng. Bộ đếm
chương trình (PC hoặc R15) và Thanh ghi trạng thái hiện tại (CPSR) giống nhau ở tất cả các chế độ. Mỗi chế độ đặc quyền (SVC
đến FIQ) có Thanh ghi trạng thái bộ xử lý đã lưu (SPSR) riêng.

2.2.2 Thanh ghi trạng thái

Trong tất cả các chế độ, bộ xử lý ARM có cùng Thanh ghi trạng thái chương trình hiện tại (CPSR). Hình 2.2 thể hiện nội dung
của thanh ghi CPSR.
Trong thanh ghi CPSR, NZCV là các bit điều kiện, I và F là các bit mặt nạ ngắt IRQ và FIQ, trạng thái T = Thumb và
M[4:0] là các bit chế độ bộ xử lý, xác định chế độ bộ xử lý là

USR: 10000 (0x10)


Câu hỏi thường gặp: 10001 (0x11)

IRQ: 10010 (0x12)


SVC: 10011 (0x13)
ABT: 10111 (0x17)
UND: 11011 (0x1B)
SYS: 11111 (0x1F)

2.2.3 Thay đổi chế độ bộ xử lý ARM

Tất cả các chế độ ARM đều có đặc quyền ngoại trừ chế độ Người dùng không có đặc quyền. Giống như hầu hết các CPU khác, bộ xử
lý ARM thay đổi chế độ để đáp ứng với các ngoại lệ hoặc gián đoạn. Cụ thể, nó chuyển sang chế độ FIQ khi xảy ra ngắt FIQ. Nó
chuyển sang chế độ IRQ khi xảy ra ngắt IRQ. Nó chuyển sang chế độ SVC khi bật nguồn, sau khi đặt lại hoặc thực hiện lệnh SWI.
Nó chuyển sang chế độ Hủy bỏ khi xảy ra ngoại lệ truy cập bộ nhớ và nó chuyển sang chế độ UND khi gặp lệnh không xác định.
Một tính năng khác thường của bộ xử lý ARM là, khi ở chế độ đặc quyền, nó có thể thay đổi chế độ một cách tự do bằng cách
thay đổi các bit chế độ trong CPSR, bằng cách sử dụng các lệnh MSR và MRS. Ví dụ: khi bộ xử lý ARM khởi động hoặc sau khi
thiết lập lại, nó sẽ bắt đầu thực thi ở chế độ SVC. Khi ở chế độ SVC, mã khởi tạo hệ thống phải thiết lập con trỏ ngăn xếp
của các chế độ khác. Để thực hiện những điều này, nó chỉ cần thay đổi bộ xử lý sang một chế độ thích hợp, khởi tạo con trỏ
ngăn xếp (R13_mode) và thanh ghi trạng thái chương trình đã lưu (SPSR) của chế độ đó. Đoạn mã sau đây cho thấy cách chuyển
bộ xử lý sang chế độ IRQ trong khi vẫn bảo toàn các bit khác, ví dụ như bit F và I, trong CPSR.
Machine Translated by Google

10 2 Kiến trúc và lập trình ARM

MRS r0, cpsr // lấy cpsr vào r0

BIC r1, r0, #01F // xóa 5 bit chế độ trong r0

ORR r1, r1, #0x12 // chuyển sang chế độ IRQ

MSR cpsr, r1 // ghi vào cpsr

Nếu chúng ta không quan tâm đến nội dung CPSR ngoài trường chế độ, ví dụ như trong quá trình khởi tạo hệ thống, việc thay đổi sang chế độ IRQ

có thể được thực hiện bằng cách ghi trực tiếp một giá trị vào CPSR, như trong

MSR cpsr, #0x92 // Chế độ IRQ với I bit=1

Cách sử dụng đặc biệt của chế độ SYS là truy cập các thanh ghi chế độ Người dùng, ví dụ R13 (sp), R14 (lr) từ chế độ đặc quyền.

Trong hệ điều hành, các tiến trình thường chạy ở chế độ Người dùng không có đặc quyền. Khi một tiến trình thực hiện lệnh gọi hệ thống

(bằng SWI), nó sẽ đi vào nhân hệ thống ở chế độ SVC. Khi ở chế độ kernel, quy trình có thể cần thao tác ngăn xếp chế độ Người dùng và

trả địa chỉ về hình ảnh chế độ Người dùng. Trong trường hợp này, tiến trình phải có khả năng truy cập vào chế độ Người dùng sp và lr

của nó. Điều này có thể được thực hiện bằng cách chuyển CPU sang chế độ SYS, chế độ này chia sẻ cùng một bộ thanh ghi với chế độ Người

dùng. Tương tự, khi xảy ra ngắt IRQ, bộ xử lý ARM sẽ chuyển sang chế độ IRQ để thực thi quy trình dịch vụ ngắt (ISR) nhằm xử lý ngắt.

Nếu ISR cho phép các ngắt lồng nhau, nó phải chuyển bộ xử lý từ chế độ IRQ sang chế độ đặc quyền khác để xử lý các ngắt lồng nhau.

Chúng ta sẽ chứng minh điều này sau ở Chap. 3 khi chúng ta thảo luận về các trường hợp ngoại lệ và quá trình xử lý gián đoạn trong các

hệ thống dựa trên ARM.

2.3 Quy trình hướng dẫn

Bộ xử lý ARM sử dụng một đường dẫn bên trong để tăng tốc độ dòng lệnh tới bộ xử lý, cho phép thực hiện một số thao tác đồng thời thay

vì tuần tự. Trong hầu hết các bộ xử lý ARM, quy trình hướng dẫn bao gồm 3 giai đoạn, FETCH-DECODE-EXECUTE, như hiển thị bên dưới.

máy tính
FETCH Lấy lệnh từ bộ nhớ

PC-4 DECODE Các thanh ghi giải mã được sử dụng trong lệnh

PC-8 EXECUTE Thực hiện lệnh

Bộ đếm chương trình (PC) thực sự trỏ đến lệnh đang được tìm nạp, thay vì lệnh đang được thực thi.

Điều này có ý nghĩa đối với các lệnh gọi hàm và trình xử lý ngắt. Khi gọi một hàm bằng lệnh BL, địa chỉ trả về thực tế là PC-4, được

điều chỉnh tự động bởi lệnh BL. Khi trở về từ trình xử lý ngắt, địa chỉ trả về cũng là PC-4, địa chỉ này phải được điều chỉnh bởi chính

trình xử lý ngắt, trừ khi ISR được xác định bằng thuộc tính __attribute__((interrupt)), trong trường hợp đó mã được biên dịch sẽ điều

chỉnh liên kết đăng ký tự động. Đối với một số trường hợp ngoại lệ, chẳng hạn như Hủy bỏ, địa chỉ trả về là PC-8, trỏ đến hướng dẫn

ban đầu gây ra ngoại lệ.

2.4 Hướng dẫn ARM

2.4.1 Cờ điều kiện và điều kiện

Trong CPSR của bộ xử lý ARM, 4 bit cao nhất, NZVC, là cờ điều kiện hoặc đơn giản là mã điều kiện, trong đó

N = âm, Z = 0, V = tràn, C = mang bit ra ngoài

Cờ điều kiện được thiết lập bằng các hoạt động so sánh và TST. Theo mặc định, các thao tác xử lý dữ liệu không ảnh hưởng đến cờ

điều kiện. Để làm cho các cờ điều kiện được cập nhật, một lệnh có thể là hậu tố với ký hiệu S, ký hiệu này đặt bit S trong mã hóa lệnh.

Ví dụ: cả hai lệnh sau đây đều cộng hai số, nhưng chỉ có lệnh ADDS ảnh hưởng đến các cờ điều kiện.
Machine Translated by Google

2.4 Hướng dẫn ARM 11

CỘNG r0, r1, r2 ; r0 = r1 + r2

. CỘNG r0, r1, r2 ; r0 = r1 + r2 và đặt cờ điều kiện

Trong mã hóa lệnh 32 bit ARM, 4 bit hàng đầu [31:28] biểu thị các kết hợp khác nhau của điều kiện

các bit cờ, tạo thành trường điều kiện của lệnh (nếu có). Dựa trên sự kết hợp khác nhau của điều kiện

các bit cờ, các điều kiện được xác định theo cách ghi nhớ là EQ, NE, LT, GT, LE, GE, v.v. Phần sau đây trình bày một số điều kiện phổ biến nhất

các điều kiện thường dùng và ý nghĩa của chúng.

0000 : EQ Bằng (bộ Z)

0001 : NE Không bằng (Z rõ ràng)

0010 : Bộ đồ CS (bộ C)

0101 : Bộ tràn VS (bộ V)

1000 : HI Không dấu cao hơn (Bộ C và Z rõ ràng)

1001 : LS Unsigned thấp hơn hoặc tương tự (Bộ C rõ ràng hoặc bộ Z)

1010 : GE Có dấu lớn hơn hoặc bằng (C = V)

1011 : LT Ký ít hơn (C != V)

1100 : GT được ký lớn hơn (Z=0 và N=V)

1101 : LE Ký nhỏ hơn hoặc bằng (Z=1 hoặc N!=V)

1110 : AL Luôn luôn

Một tính năng khá độc đáo của kiến trúc ARM là hầu hết mọi lệnh đều có thể được thực thi có điều kiện. MỘT

lệnh có thể chứa hậu tố điều kiện tùy chọn, ví dụ: EQ, NE, LT, GT, GE, LE, GT, LT, v.v. để xác định xem

CPU sẽ thực thi lệnh dựa trên điều kiện đã chỉ định. Nếu điều kiện không được đáp ứng thì lệnh sẽ không được thực hiện

được thực hiện hoàn toàn mà không có bất kỳ tác dụng phụ nào. Điều này loại bỏ sự cần thiết của nhiều nhánh trong một chương trình, điều này có xu hướng làm gián đoạn

đường dẫn hướng dẫn. Để thực hiện một lệnh có điều kiện, chỉ cần thêm vào sau lệnh đó một điều kiện thích hợp. Ví dụ, một
Lệnh ADD không có điều kiện có dạng:

CỘNG r0, r1, r2 ; r0 = r1 + r2

Để chỉ thực hiện lệnh nếu cờ 0 được đặt, hãy thêm lệnh bằng EQ.

ADDEQ r0, r1, r2 ; Nếu cờ 0 được đặt thì r0 = r1 + r2

Tương tự cho các điều kiện khác.

2.4.2 Hướng dẫn rẽ nhánh

Hướng dẫn phân nhánh có dạng

Nhãn B{<cond>} ; nhánh để dán nhãn

Chương trình con BL{<cond>} ; nhánh tới chương trình con có liên kết

Lệnh Nhánh (B) làm cho nhánh trực tiếp có độ lệch so với PC hiện tại. Chi nhánh có liên kết

Lệnh (BL) dành cho các lệnh gọi chương trình con. Nó ghi PC-4 vào LR của dãy thanh ghi hiện tại và thay thế PC bằng mục nhập

địa chỉ của chương trình con, khiến CPU phải nhập chương trình con đó. Khi chương trình con kết thúc, nó sẽ trả về theo địa chỉ đã lưu

địa chỉ trả về trong thanh ghi liên kết R14. Hầu hết các bộ xử lý khác thực hiện các cuộc gọi chương trình con bằng cách lưu địa chỉ trả về

cây rơm. Thay vì lưu địa chỉ trả về vào ngăn xếp, bộ xử lý ARM chỉ cần sao chép PC-4 vào R14 và phân nhánh tới địa chỉ đó.

gọi là chương trình con. Nếu chương trình con được gọi không gọi các chương trình con khác, nó có thể sử dụng LR để quay về nơi gọi

nhanh chóng. Để trở về từ một chương trình con, chương trình chỉ cần sao chép LR (R14) vào PC (R15), như trong

MOV PC, LR hoặc BX LR


Machine Translated by Google

12 2 Kiến trúc và lập trình ARM

Tuy nhiên, điều này chỉ hoạt động đối với các cuộc gọi chương trình con một cấp. Nếu một chương trình con có ý định thực hiện một cuộc gọi khác, nó phải lưu và

khôi phục thanh ghi LR một cách rõ ràng vì mỗi lệnh gọi chương trình con sẽ thay đổi LR hiện tại. Thay vì lệnh MOV, lệnh
Lệnh MOVS cũng có thể được sử dụng để khôi phục các cờ gốc trong CPSR.

2.4.3 Các phép toán số học

Cú pháp của các phép tính số học là:

<Hoạt động>{<cond>}{S} Rd, Rn, Toán hạng2

Lệnh thực hiện một phép toán số học trên hai toán hạng và đặt kết quả vào thanh ghi đích Rd. Các
toán hạng đầu tiên, Rn, luôn là một thanh ghi. Toán hạng thứ hai có thể là thanh ghi hoặc giá trị tức thời. Trong trường hợp sau,
toán hạng được gửi đến ALU thông qua bộ dịch thùng để tạo ra giá trị thích hợp.
Ví dụ:

CỘNG r0, r1, r2 ; r0 = r1 + r2

SUB r3, r3, #1 ; r3 = r3 - 1

2.4.4 Hoạt động so sánh

CMP: toán hạng1—operand2, nhưng kết quả không được ghi


TST: toán hạng1 VÀ toán hạng2, nhưng kết quả không được ghi
TEQ: toán hạng1 EOR toán hạng2, nhưng kết quả không được ghi

Các phép toán so sánh cập nhật các bit cờ điều kiện trong thanh ghi trạng thái, có thể được sử dụng làm điều kiện trong các lệnh tiếp theo.

hướng dẫn. Ví dụ:

CMP r0, r1 ; đặt các bit điều kiện trong CPSR theo r0-r1

TSTEQ r2, #5 ; kiểm tra r2 và 5 bằng nhau và đặt bit Z trong CPSR

2.4.5 Các thao tác logic

VÀ: toán hạng1 VÀ toán hạng2 ; khôn ngoan một chút VÀ

EOR: toán hạng1 EOR toán hạng2 ; độc quyền bit-khôn ngoan HOẶC

ORR: toán hạng1 HOẶC toán hạng2 ; bit-khôn ngoan HOẶC

BIC: toán hạng1 VÀ (KHÔNG phải toán hạng2) ; bit rõ ràng

Ví dụ:

VÀ r0, r1, r2 ; r0 = r1 & r2

ORR r0, r0, #0x80 ; đặt bit 7 của r0 thành 1

BIC r0, r0, #0xF ; xóa 4 bit thấp của r0

EORS r1, r3, r0 ; r1 = r3 ExOR r0 và đặt các bit điều kiện


Machine Translated by Google

2.4 Hướng dẫn ARM 13

2.4.6 Hoạt động di chuyển dữ liệu

Toán hạng MOV1, toán hạng2

MVN toán hạng1, KHÔNG phải toán hạng2

Ví dụ:

MOV r0, r1 ; r0 = r1 : Luôn thực thi

MOVS r2, #10 ; r2 = 10 và đặt các bit điều kiện Z=0 N=0

MOVNEQ r1, #0 ; r1 = 0 chỉ khi bit Z != 0

2.4.7 Giá trị tức thời và bộ chuyển đổi thùng

Bộ dịch chuyển thùng là một tính năng độc đáo khác của bộ xử lý ARM. Nó được sử dụng để tạo ra các phép dịch chuyển và toán hạng ngay lập tức

bên trong bộ xử lý ARM. Bộ xử lý ARM không có hướng dẫn dịch chuyển thực tế. Thay vào đó, nó có bộ chuyển số thùng, thực hiện chuyển số như

một phần của các hướng dẫn khác. Các thao tác dịch chuyển bao gồm dịch chuyển thông thường sang trái, sang phải và xoay, như trong

MOV r0, r0, LSL #1 ; dịch r0 sang trái 1 bit (nhân r0 với 2)

MOV r1, r1, LSR #2 ; dịch r1 sang phải 2 bit (chia r1 cho 4)

MOV r2, r2, ROR #4 ; hoán đổi 4 bit cao và thấp của r2

Hầu hết các bộ xử lý khác cho phép tải các thanh ghi CPU với các giá trị tức thời, tạo thành các phần của luồng lệnh, làm cho độ dài lệnh

có thể thay đổi. Ngược lại, tất cả các lệnh ARM đều dài 32 bit và chúng không sử dụng luồng lệnh làm dữ liệu. Điều này đưa ra một thách thức

khi sử dụng các giá trị tức thời trong hướng dẫn. Định dạng lệnh xử lý dữ liệu có sẵn 12 bit cho toán hạng2. Nếu được sử dụng trực tiếp, điều

này sẽ chỉ cho phạm vi 0–4095. Thay vào đó, nó được sử dụng để lưu trữ giá trị xoay 4 bit và hằng số 8 bit trong phạm vi 0–255. 8 bit có thể

được xoay sang phải một số vị trí chẵn (tức là ROR bằng 0, 2, 4,…,30). Điều này mang lại phạm vi giá trị lớn hơn nhiều có thể được tải trực

tiếp. Ví dụ: để tải r0 với giá trị tức thời 4096, hãy sử dụng

MOV r0, #0x40, 26 ; tạo 4096 (0x1000) bằng 0x40 ROR 26

Để làm cho tính năng này dễ sử dụng hơn, trình biên dịch mã sẽ chuyển đổi sang dạng này nếu được cung cấp hằng số bắt buộc trong một lệnh,

ví dụ

MOV r0, #4096

Trình biên dịch mã sẽ tạo ra lỗi nếu giá trị đã cho không thể chuyển đổi theo cách này. Thay vì MOV, LDR

lệnh cho phép tải một giá trị 32 bit tùy ý vào một thanh ghi, ví dụ:

LDR thứ, =numeric_constant

Nếu giá trị không đổi có thể được xây dựng bằng cách sử dụng MOV hoặc MVN thì đây sẽ là lệnh thực sự được tạo ra. Nếu không, trình biên

dịch mã sẽ tạo LDR có địa chỉ liên quan đến PC để đọc hằng số từ một nhóm chữ.
Machine Translated by Google

14 2 Kiến trúc và lập trình ARM

2.4.8 Lệnh nhân

MUL{<cond>}{S} Đường, Rm, Rs ; Rd = Rm * Rs

MLA{<cond>}{S} Rd, Rm, Rs,Rn ; Rd = (Rm * Rs) + Rn

2.4.9 Hướng dẫn TẢI và Lưu trữ

Bộ xử lý ARM là Kiến trúc tải/lưu trữ. Dữ liệu phải được nạp vào các thanh ghi trước khi sử dụng. Nó không hỗ trợ bộ nhớ cho các hoạt

động xử lý dữ liệu bộ nhớ. Bộ xử lý ARM có ba bộ hướng dẫn tương tác với bộ nhớ. Đó là:

• Truyền dữ liệu đăng ký đơn (LDR/STR). • Chặn


truyền dữ liệu (LDM/STM). • Hoán đổi dữ

liệu đơn (SWP).

Hướng dẫn tải và lưu trữ cơ bản là: Tải và lưu trữ Word hoặc Byte:

LDR / STR / LDRB / STRB

2.4.10 Thanh ghi cơ sở

Các lệnh tải/lưu trữ có thể sử dụng thanh ghi cơ sở làm chỉ mục để xác định vị trí bộ nhớ cần truy cập. Chỉ mục có thể bao gồm phần

bù trong chế độ đánh địa chỉ trước chỉ mục hoặc sau chỉ mục. Ví dụ về việc sử dụng các thanh ghi chỉ mục là

STR r0, [r1] ; lưu r0 vào vị trí được chỉ bởi r1. ; tải r2 từ bộ

LDR r2, [r1] nhớ được trỏ bởi r1.

STR r0, [r1, #12] ; địa chỉ chỉ mục trước: STR r0 đến [r1+12]

STR r0, [r1], #12 ; Địa chỉ sau chỉ mục: STR r0 đến [r1],r1+12

2.4.11 Chặn truyền dữ liệu

Thanh ghi cơ sở được sử dụng để xác định nơi truy cập bộ nhớ sẽ xảy ra. Bốn chế độ địa chỉ khác nhau cho phép tăng và giảm bao gồm

hoặc loại trừ vị trí thanh ghi cơ sở. Thanh ghi cơ sở có thể được cập nhật tùy chọn sau khi truyền dữ liệu bằng cách thêm nó vào '!'

biểu tượng. Các hướng dẫn này rất hiệu quả để lưu và khôi phục bối cảnh thực thi, ví dụ như sử dụng vùng bộ nhớ làm ngăn xếp hoặc di

chuyển các khối dữ liệu lớn trong bộ nhớ. Điều đáng lưu ý là, khi sử dụng các lệnh này để lưu/khôi phục nhiều thanh ghi CPU vào/từ bộ

nhớ, thứ tự thanh ghi trong lệnh không thành vấn đề.

Các thanh ghi được đánh số thấp hơn luôn được chuyển đến/từ các địa chỉ thấp hơn trong bộ nhớ.

2.4.12 Hoạt động ngăn xếp

Ngăn xếp là vùng bộ nhớ tăng lên khi dữ liệu mới được "đẩy" lên "trên cùng" của ngăn xếp và co lại khi dữ liệu được "bật" ra khỏi

đầu ngăn xếp. Hai con trỏ được sử dụng để xác định giới hạn hiện tại của ngăn xếp.

• Con trỏ cơ sở: dùng để trỏ tới “đáy” của ngăn xếp (vị trí đầu tiên). • Con trỏ ngăn

xếp: dùng để trỏ đến "đỉnh" hiện tại của ngăn xếp.
Machine Translated by Google

2.4 Hướng dẫn ARM 15

Một ngăn xếp được gọi là giảm dần nếu nó tăng dần xuống trong bộ nhớ, tức là giá trị được đẩy cuối cùng nằm ở địa chỉ thấp nhất. Một ngăn xếp

được gọi là tăng dần nếu nó lớn lên trong bộ nhớ. Bộ xử lý ARM hỗ trợ cả ngăn xếp giảm dần và tăng dần. Ngoài ra, nó còn cho phép con trỏ ngăn xếp

trỏ đến địa chỉ bị chiếm giữ cuối cùng (Ngăn xếp đầy đủ) hoặc tới địa chỉ bị chiếm đóng tiếp theo (Ngăn xếp trống). Trong ARM, các thao tác ngăn

xếp được thực hiện bằng các lệnh STM/LDM. Loại ngăn xếp được xác định bởi tiền tố trong hướng dẫn STM/LDM:

• STMFD/LDMFD: Ngăn xếp giảm dần đầy đủ • STMFA/LDMFA:

Ngăn xếp tăng dần đầy đủ • STMED/LDMED: Ngăn xếp

giảm dần trống • STMEA/LDMEA: Ngăn xếp tăng dần trống

Trình biên dịch C luôn sử dụng ngăn xếp giảm dần đầy đủ. Các dạng ngăn xếp khác rất hiếm và hầu như không bao giờ được sử dụng trong thực tế. Vì

vì lý do này, chúng tôi sẽ chỉ sử dụng các ngăn xếp Giảm dần đầy đủ trong suốt cuốn sách này.

2.4.13 Ngăn xếp và chương trình con

Cách sử dụng phổ biến của ngăn xếp là tạo không gian làm việc tạm thời cho các chương trình con. Khi một chương trình con bắt đầu, bất kỳ thanh ghi

nào cần được bảo tồn đều có thể được đẩy vào ngăn xếp. Khi chương trình con kết thúc, nó sẽ khôi phục các thanh ghi đã lưu bằng cách lấy chúng ra

khỏi ngăn xếp trước khi quay lại trình gọi. Đoạn mã sau đây thể hiện mẫu chung của một chương trình con.

STMFD sp!, {r0-r12, lr} ; lưu tất cả các thanh ghi và địa chỉ trả về

{ Mã chương trình con } ; mã chương trình con

LDMFD sp!, {r0-r12, pc} ; khôi phục các thanh ghi đã lưu và trả về bằng lr

Nếu lệnh pop có tập hợp bit 'S' (bằng ký hiệu '^'), thì việc chuyển thanh ghi PC khi ở chế độ đặc quyền cũng sao chép SPSR đã lưu sang chế độ

CPSR trước đó, khiến quay trở lại chế độ trước đó trước khi thực hiện ngoại lệ (bằng SWI hoặc IRQ).

2.4.14 Ngắt phần mềm (SWI)

Trong ARM, lệnh SWI được sử dụng để tạo ra ngắt phần mềm. Sau khi thực hiện lệnh SWI, bộ xử lý ARM chuyển sang chế độ SVC và thực thi từ địa chỉ

vectơ SVC 0x08, khiến nó thực thi trình xử lý SWI, thường là điểm vào của các lệnh gọi hệ thống tới nhân hệ điều hành. Chúng ta sẽ chứng minh các

cuộc gọi hệ thống trong Chương. 5.

2.4.15 Hướng dẫn chuyển PSR

Các lệnh MRS và MSR cho phép nội dung của CPSR/SPSR được chuyển từ thanh ghi trạng thái thích hợp sang thanh ghi mục đích chung. Toàn bộ thanh ghi

trạng thái hoặc chỉ có thể chuyển các bit cờ. Các hướng dẫn này được sử dụng chủ yếu để thay đổi chế độ bộ xử lý khi ở chế độ đặc quyền.

MRS{<cond>} Rd, <psr> ; Rd = <psr>

MSR{<cond>} <psr>, Rm ; <psr> = Rm

2.4.16 Hướng dẫn bộ đồng xử lý

Kiến trúc ARM xử lý nhiều thành phần phần cứng, ví dụ: Đơn vị quản lý bộ nhớ (MMU) dưới dạng bộ đồng xử lý, được truy cập bằng các hướng dẫn bộ

đồng xử lý đặc biệt. Chúng tôi sẽ đề cập đến bộ đồng xử lý trong Chap. 6 và các chương sau.
Machine Translated by Google

16 2 Kiến trúc và lập trình ARM

Hình 2.3 Các thành phần của Toolchain

Chuỗi công cụ 2.5 ARM

Chuỗi công cụ là tập hợp các công cụ lập trình để phát triển chương trình, từ mã nguồn đến các tệp thực thi nhị phân.

Chuỗi công cụ thường bao gồm trình biên dịch mã, trình biên dịch, trình liên kết, một số chương trình tiện ích, ví dụ như objcopy, để

chuyển đổi tệp và trình gỡ lỗi. Hình 2.3 mô tả các thành phần và luồng dữ liệu của một toolchan điển hình.

Chuỗi công cụ chạy trên máy chủ và tạo mã cho máy mục tiêu. Nếu kiến trúc máy chủ và đích khác nhau thì chuỗi công cụ này được gọi là

chuỗi công cụ chéo hoặc đơn giản là trình biên dịch chéo. Thông thường, chuỗi công cụ được sử dụng để phát triển hệ thống nhúng là chuỗi

công cụ chéo. Trên thực tế, đây là cách phát triển phần mềm tiêu chuẩn cho các hệ thống nhúng. Nếu chúng tôi phát triển mã trên máy Linux

dựa trên kiến trúc Intel x86 nhưng mã dành cho máy mục tiêu ARM thì chúng tôi cần trình biên dịch chéo nhắm mục tiêu ARM dựa trên Linux.

Có nhiều phiên bản khác nhau của chuỗi công cụ dựa trên Linux dành cho kiến trúc ARM (ARM toolchains 2016). Trong cuốn sách này, chúng ta

sẽ sử dụng chuỗi công cụ arm-none-eabi trong Ubuntu Linux Phiên bản 14.04/15.10. Người đọc có thể lấy và cài đặt chuỗi công cụ cũng như

qemu-system-arm cho máy ảo ARM trên Ubuntu Linux như sau.

sudo apt-get cài đặt gcc-arm-none-eabi


sudo apt-get cài đặt qemu-system-arm

Trong các phần sau, chúng tôi sẽ trình bày cách sử dụng chuỗi công cụ ARM và máy ảo ARM trong QEMU

bằng các ví dụ lập trình.

2.6 Trình mô phỏng hệ thống ARM

QEMU hỗ trợ nhiều máy ARM giả lập (QEMU Emulator 2010). Chúng bao gồm bo mạch ARM Integrator/CP, bo mạch chủ đa năng ARM, bo mạch chủ ARM

RealView và một số bo mạch khác. Các CPU ARM được hỗ trợ bao gồm ARM926E, ARM1026E, ARM946E, ARM1136 hoặc Cortex-A8. Tất cả đều là hệ

thống bộ xử lý đơn (UP) hoặc CPU đơn.

Để bắt đầu, chúng ta sẽ chỉ xem xét các hệ thống đơn bộ xử lý (UP). Hệ thống đa bộ xử lý (MP) sẽ được đề cập ở phần sau của Chương. 9.

Trong số các máy ảo ARM được mô phỏng, chúng tôi sẽ chọn ván chân tường ARM Versatilepb (ARM926EJ-S 2016) làm nền tảng để triển khai và

thử nghiệm vì những lý do sau.


Machine Translated by Google

2.6 Trình mô phỏng hệ thống ARM 17

(1). Nó hỗ trợ nhiều thiết bị ngoại vi. Theo hướng dẫn sử dụng QEMU, ván chân tường ARM Versatilepb được mô phỏng với các thiết bị sau: •

CPU ARM926E, ARM1136 hoặc


Cortex-A8. • Bộ điều khiển ngắt Vector PL190. • Bốn

UART PL011.

• Bộ điều khiển LCD PL110. •

PL050 KMI với bàn phím và chuột PS/2. • Giao diện thẻ
đa phương tiện PL181 với thẻ SD. • Bộ chuyển đổi Ethernet

SMC 91c111. • Cầu nối máy chủ PCI (có

giới hạn). • Bộ điều khiển USB PCI OHCI. •


Bộ điều hợp Bus chủ PCI SCSI

LSI53C895A với các thiết bị đĩa cứng và CD-ROM.

(2). Kiến trúc bo mạch đa năng ARM được ghi lại đầy đủ bằng các bài viết trực tuyến trong Trung tâm thông tin ARM.

(3). QEMU có thể khởi động trực tiếp máy ảo ARM Versatilepb được mô phỏng. Ví dụ: chúng tôi có thể tạo tệp thực thi nhị phân, t.bin và chạy

tệp đó trên ARM VersatilepbVM được mô phỏng bằng cách

qemu-system-arm –M linh hoạtpb –m 128M –kernel t.bin –serial mon:stdio

QEMU sẽ tải tệp t.bin về 0x10000 trong RAM và thực thi trực tiếp. Điều này rất thuận tiện vì nó giúp loại bỏ

cần lưu trữ hình ảnh hệ thống trong bộ nhớ flash và dựa vào bộ khởi động chuyên dụng để khởi động hình ảnh hệ thống.

2.7 Lập trình ARM

2.7.1 Ví dụ lập trình hợp ngữ ARM 1

Chúng tôi bắt đầu lập trình ARM bằng một loạt chương trình mẫu. Để dễ tham khảo, chúng ta sẽ gắn nhãn các chương trình mẫu bằng C2.x, trong

đó C2 biểu thị số chương và x biểu thị số chương trình. Chương trình ví dụ đầu tiên, C2.1, bao gồm tệp ts.s trong tập hợp ARM. Phần sau đây

trình bày các bước phát triển và chạy chương trình ví dụ.

(1). tập tin ts.s của C2.1

/************ tệp ts.s của C2.1 *************/

.chữ

.global bắt đầu

bắt đầu:

chuyển r0, #1 @r0 = 1

MOV R1, #2 @r1 = 2

CỘNG R1, R1, R0 // r1 = r1 + r0

ldr r2, =kết quả // r2 = &kết quả

str r1, [r2] /*kết quả = r1 */

dừng lại: b dừng lại


.dữ liệu

kết quả: .word 0 /* vị trí của một từ */

Mã chương trình tải các thanh ghi CPU r0 với giá trị 1, r1 với giá trị 2. Sau đó, nó thêm r0 vào r1 và lưu trữ

kết quả vào vị trí bộ nhớ có nhãn kết quả.

Trước khi tiếp tục, cần lưu ý những điều sau. Đầu tiên, trong một chương trình mã hợp ngữ, các lệnh không phân biệt chữ hoa chữ thường.

Một lệnh có thể sử dụng chữ hoa, chữ thường hoặc thậm chí là các trường hợp hỗn hợp. Để dễ đọc hơn, kiểu mã hóa phải nhất quán, tất cả đều

là chữ thường hoặc toàn chữ hoa. Tuy nhiên, các ký hiệu khác, ví dụ như vị trí bộ nhớ, phân biệt chữ hoa chữ thường. Thứ hai, như được hiển

thị trong chương trình, chúng ta có thể sử dụng ký hiệu @ hoặc // để bắt đầu một dòng chú thích hoặc đưa các chú thích vào các cặp /* phù hợp
Machine Translated by Google

18 2 Kiến trúc và lập trình ARM

Và */. Việc sử dụng loại dòng bình luận nào là vấn đề sở thích cá nhân. Trong cuốn sách này, chúng ta sẽ sử dụng // cho một

các dòng chú thích và sử dụng các cặp /* và */ phù hợp cho các khối chú thích có thể kéo dài trên nhiều dòng, áp dụng cho

cả mã hợp ngữ và chương trình C.

(2). Tệp tập lệnh mk:

Tập lệnh sh, mk, được sử dụng để (chéo) biên dịch liên kết ts.s thành tệp ELF. Sau đó, nó sử dụng objcopy để chuyển đổi tệp ELF thành

hình ảnh thực thi nhị phân có tên t.bin.

arm-none-eabi-as –o ts.o ts.s # tập hợp ts.s thành ts.o

arm-none-eabi-ld –T t.ld –o t.elf ts.o # liên kết ts.o tới tập tin t.elf

arm-none-eabi-nm t.elf # hiển thị ký hiệu trong t.elf

arm-none-eabi-objcopy –O nhị phân t.elf t.bin # objcopy t.elf sang t.bin

(3). tập lệnh liên kết:

Trong bước liên kết, tệp tập lệnh liên kết, t.ld, được sử dụng để chỉ định điểm vào và bố cục của các phần chương trình.

NHẬP(bắt đầu) /* xác định điểm bắt đầu là địa chỉ mục nhập */

PHẦN /*các phần của chương trình */

. = 0x10000; /* địa chỉ tải, do QEMU yêu cầu */

.chữ : { *(.text) } /* tất cả văn bản trong phần .text */

.dữ liệu : { *(.data) } /* tất cả dữ liệu trong phần .data */

.bss : { *(.bss) } /* tất cả bss trong phần .bss */

. =CĂN CHỈNH(8);

. =. + 0x1000; /* Dung lượng ngăn xếp 4 KB */

stack_top =.; /* stack_top là ký hiệu được xuất bởi trình liên kết */

Trình liên kết tạo ra một tệp ELF. Nếu muốn, người đọc có thể xem nội dung tệp ELF bằng cách

arm-none-eabi-readelf -a t.elf # hiển thị toàn bộ thông tin của t.elf

arm-none-eabi-objdump -d t.elf # tháo rời tập tin t.elf

Tệp ELF chưa được thực thi. Để thực thi, nó phải được chuyển đổi thành tệp thực thi nhị phân bằng objcopy, như trong

arm-none-eabi-objcopy –O nhị phân t.elf t.bin # chuyển đổi t.elf thành t.bin

(3) Chạy tệp thực thi nhị phân: Để chạy t.bin trên máy ảo ARM Versatilepb, nhập lệnh

qemu-system-arm –M multiplepb –kernel t.bin –nographic –serial /dev/null

Người đọc có thể bao gồm tất cả các lệnh trên trong tập lệnh mk, tập lệnh này sẽ biên dịch liên kết và chạy tệp thực thi nhị phân bằng cách

một lệnh script duy nhất.


Machine Translated by Google

2.7 Lập trình ARM 19

Hình 2.4 Đăng ký nội dung chương trình C2.1

(4). Kiểm tra kết quả: Để kiểm tra kết quả chạy chương trình, nhập lệnh giám sát QEMU:

đăng ký thông tin : hiển thị các thanh ghi CPU

xp /wd [địa chỉ] : hiển thị nội dung bộ nhớ bằng các từ 32 bit

Hình 2.4 thể hiện nội dung thanh ghi chạy chương trình C2.1. Như hình vẽ cho thấy, thanh ghi R2 chứa
0x0001001C, là địa chỉ của kết quả. Ngoài ra, dòng lệnh arm-none-eabi-nm t.elf trong mk
script cũng hiển thị vị trí của các ký hiệu trong chương trình. Người đọc có thể nhập lệnh giám sát QEMU

xp /wd 0x1001C

để hiển thị nội dung của kết quả, giá trị này phải là 3. Để thoát QEMU, hãy nhập Control-a x hoặc Control-C để chấm dứt QEMU
quá trình.

2.7.2 Ví dụ lập trình hợp ngữ ARM 2

Chương trình ví dụ tiếp theo, được ký hiệu là C2.2, sử dụng mã hợp ngữ ARM để tính tổng của một mảng số nguyên. Nó cho thấy
cách sử dụng ngăn xếp để gọi chương trình con. Nó cũng trình bày các chế độ đánh địa chỉ gián tiếp và sau chỉ mục của các lệnh ARM.
Để cho ngắn gọn, chúng tôi chỉ hiển thị tệp ts.s. Tất cả các tệp khác giống như trong chương trình C2.1.
C2.2: tập tin ts.s:

.chữ

.global bắt đầu

start: ldr sp, =stack_top // đặt con trỏ ngăn xếp

tổng số tiền // gọi tổng

dừng lại: b dừng lại // vòng lặp

sum:// int sum(): tính tổng của một mảng int trong Kết quả

stmfd sp!, {r0-r4, lr} // lưu r0-r4, lr vào ngăn xếp

chuyển r0, #0 // r0 = 0

ldr r1, =Mảng // r1 = &Mảng

ldr r2, =N // r2 = &N

ldr r2, [r2] // r2 = N

vòng lặp: ldr r3, [r1], #4 // r3 = *(r1++)

thêm r0, r0, r3 // r0 += r3

phụ r2, r2, #1 // r2—

cmp r2, #0 // nếu (r2 != 0 )

vòng lặp tốt // vòng lặp goto;

ldr r4, =Kết quả // r4 = &Kết quả

str r0, [r4] // Kết quả = r0


Machine Translated by Google

20 2 Kiến trúc và lập trình ARM

Hình 2.5 Đăng ký nội dung chương trình C2.2

ldmfd sp!, {r0-r4, pc} // bật ngăn xếp, quay lại người gọi

.dữ liệu

N: .word 10 // số phần tử mảng

Mảng: .word 1,2,3,4,5,6,7,8,9,10

Kết quả: .word 0

Chương trình tính tổng của một mảng số nguyên. Số phần tử mảng (10) được xác định trong vị trí bộ nhớ có nhãn N và các phần tử mảng

được xác định trong vùng bộ nhớ có nhãn Array. Tổng được tính bằng R0, được lưu vào vị trí bộ nhớ có nhãn Kết quả. Như trước đây, hãy chạy

tập lệnh mk để tạo t.bin có thể thực thi nhị phân. Sau đó chạy t.bin trong QEMU. Khi chương trình dừng lại, hãy sử dụng thông tin lệnh

giám sát và xp để kiểm tra kết quả. Hình 2.5 thể hiện kết quả chạy chương trình C2.2. Như hình minh họa, thanh ghi R0 chứa kết quả tính

toán là 0x37 (55 ở dạng thập phân). Bạn đọc có thể sử dụng lệnh

arm-none-eabi-nm t.elf

để hiển thị các ký hiệu trong một tệp mã đối tượng. Nó liệt kê các vị trí bộ nhớ của các ký hiệu chung trong tệp t.elf, chẳng hạn như

0001004C N

Mảng 00010050

00010078 Kết quả

Sau đó, sử dụng xp/wd 0x10078 để xem nội dung của Kết quả, giá trị này phải là 55 ở dạng thập phân.

2.7.3 Kết hợp lắp ráp với lập trình C

Lập trình hội là không thể thiếu, ví dụ như khi truy cập và thao tác với các thanh ghi CPU, nhưng nó cũng rất tẻ nhạt. Trong lập trình hệ

thống, mã hợp ngữ nên được sử dụng như một công cụ để truy cập và điều khiển phần cứng cấp thấp, thay vì như một phương tiện lập trình

chung. Trong cuốn sách này, chúng ta sẽ chỉ sử dụng mã hợp ngữ nếu thực sự cần thiết. Bất cứ khi nào có thể, chúng ta sẽ triển khai mã

chương trình bằng ngôn ngữ cấp cao C. Để tích hợp hợp ngữ và mã C trong cùng một chương trình, điều cần thiết là phải hiểu các hình ảnh

thực thi chương trình và quy ước gọi của C.

2.7.3.1 Hình ảnh thực thi Một

hình ảnh (tệp) thực thi được tạo bởi trình liên kết trình biên dịch bao gồm ba phần logic.

Phần văn bản: còn gọi là Phần mã chứa mã thực thi Phần dữ liệu: các biến toàn

cục và tĩnh được khởi tạo, các hằng số tĩnh Phần BSS: các biến toàn cục và

tĩnh chưa được khởi tạo. (BSS không có trong file ảnh)

Trong quá trình thực thi, hình ảnh thực thi được tải vào bộ nhớ để tạo hình ảnh thời gian chạy, trông giống như sau.
Machine Translated by Google

2.7 Lập trình ARM 21

---------------------------------------------

(Địa chỉ thấp) | Mã | Dữ liệu | BSS | Đống | Ngăn xếp | (Địa chỉ cao)
---------------------------------------------

Một hình ảnh thời gian chạy bao gồm 5 phần liền kề nhau (một cách hợp lý). Phần Mã và Dữ liệu được tải trực tiếp từ
tệp thực thi. Phần BSS được tạo bởi kích thước phần BSS trong tiêu đề tệp thực thi. Nội dung của nó thường bị xóa về 0.
Trong ảnh thực thi, các phần Code, Data và BSS được cố định và không thay đổi. Vùng Heap dùng để phân bổ bộ nhớ động
trong ảnh thực thi. Ngăn xếp dành cho các lệnh gọi hàm trong khi thực thi. Về mặt logic, nó nằm ở đầu (địa chỉ) cao của
hình ảnh thực thi và phát triển xuống dưới, tức là từ địa chỉ cao đến địa chỉ thấp.

2.7.3.2 Quy ước gọi hàm trong C Quy ước gọi

hàm của C bao gồm các bước sau giữa hàm gọi (người gọi) và hàm được gọi (gọi).

---------------------------------- Người gọi --------------- -------------------

(1). tải 4 tham số đầu tiên trong r0-r3; Đẩy bất kỳ tham số bổ sung nào vào ngăn xếp

(2). chuyển quyền điều khiển cho callee bởi BL callee

---------------------------------- Callee --------------- -------------------

(3). lưu LR, FP(r12) vào ngăn xếp, thiết lập khung ngăn xếp (điểm FP tại LR đã lưu)

(4). dịch SP xuống để phân bổ các biến cục bộ và không gian tạm thời trên ngăn xếp

(4). sử dụng các tham số, cục bộ (và toàn cục) để thực hiện tác vụ chức năng

(5). tính toán và tải giá trị trả về trong R0, pop stack để trả lại quyền điều khiển cho người gọi

---------------------------------- Người gọi --------------- -------------------

(6). nhận giá trị trả về từ R0

(7). Dọn dẹp ngăn xếp bằng cách loại bỏ các tham số bổ sung, nếu có.

Quy ước gọi hàm có thể được minh họa rõ nhất bằng một ví dụ. Tệp tc sau chứa hàm C func(), hàm này gọi một hàm g()
khác.

/****************** tập tin tc **********************/

extern int g(int x, in y); // một hàm bên ngoài

int func(int a, int b, int c, int d, int e, int f)

int x, y, z; // biến cục bộ

x = 1; y =2; z = 3; // truy cập địa phương

g(x,y); // gọi g(x,y)

trả lại a + e; // giá trị trả về

Sử dụng trình biên dịch chéo ARM để tạo tệp mã lắp ráp có tên ts bởi

arm-none-eabi-gcc –S –mcpu=arm926ej-s tc

Sau đây cho thấy mã hợp ngữ được tạo bởi trình biên dịch ARM GCC, trong đó ký hiệu sp là ngăn xếp
con trỏ (R13) và fp là con trỏ khung ngăn xếp (R12).
Machine Translated by Google

22 2 Kiến trúc và lập trình ARM

.global chức năng // xuất func dưới dạng ký hiệu toàn cục

chức năng:

(1). Thiết lập khung ngăn xếp

stmfd sp!, {fp, lr} // lưu lr, fp vào ngăn xếp

thêm fp, sp, #4 // Điểm FP tại LR đã lưu

(2). Chuyển SP xuống 8 khe (4 byte) cho cục bộ và tạm thời

phụ sp, sp, #32

(3). Lưu r0-r3 (tham số a,b,c,d) vào ngăn xếp tại –offsets(fp)

str r0, [fp, #-24] str r1, // lưu r0 a

[fp, #-28] str r2, [fp, #-32] // lưu r1 b

str r3, [fp, #-36] // lưu r2 c

// lưu r3 d

(4). Thực hiện x=1; y=2; z=3; hiển thị vị trí của họ trên ngăn xếp

mov r3, #1

str r3, [fp, #-8] // x=1 tại -8(fp);

mov r3, #2

str r3, [fp, #-12] // y=2 tại -12(fp)

mov r3, #3

str r3, [fp, #-16] // z=3 tại -16(fp)

(5). Chuẩn bị gọi g(x,y)

ldr r0, [fp, #-8] // r0 = x

ldr r1, [fp, #-12] // r1 = y

bl g // gọi g(x,y)

(6). Tính a+e làm giá trị trả về trong r0

ldr r2, [fp, #-24] // r2 = a (được lưu ở -24(fp)

ldr r3, [fp, #4] // r3 = e ở mức +4(fp)

thêm r3, r2, r3 // r3 = a+e

di chuyển r0, r3 // r0 = giá trị trả về trong r0

(7). Quay lại người gọi

phụ sp, fp, #4 ldmfd // sp=fp-4 (trỏ vào FP đã lưu)

sp!, {fp, pc} // quay lại người gọi

Khi gọi hàm func(), người gọi phải truyền (6) tham số (a, b, c, d, e, f) cho hàm được gọi. 4 cái đầu tiên
các tham số (a, b, c, d) được truyền vào các thanh ghi r0–r3. Bất kỳ tham số bổ sung nào đều được truyền qua ngăn xếp. Khi điều khiển đi vào

được gọi là hàm, đỉnh ngăn xếp chứa các tham số bổ sung (theo thứ tự ngược lại). Trong ví dụ này, đỉnh ngăn xếp chứa 2
tham số bổ sung e và f. Ngăn xếp ban đầu trông như sau.

Cao SP thấp

----|---|---|-------------------

|f|e|

----|exParam|-------------------

(1). Khi vào, hàm được gọi trước tiên sẽ thiết lập khung ngăn xếp bằng cách đẩy LR, FP vào ngăn xếp và để FP (r12) trỏ vào
LR đã lưu.

(2). Sau đó nó dịch chuyển SP xuống (về phía địa chỉ thấp) để phân bổ không gian cho các biến cục bộ và vùng làm việc tạm thời, v.v.
Ngăn xếp trở thành
Machine Translated by Google

2.7 Lập trình ARM 23

SP SP

Cao |-push->|---- trừ SP bằng #32 ------>|---- Thấp


---|+8 |+4 | 0 |-4 |-8 |-12|-16|-20|-24|-28|-32|-36|
| f | e |LR |FP | | | | | | | | |

---|exParam|-|-|---| biến cục bộ, không gian làm việc|-----


FP

Trong sơ đồ, độ lệch (byte) có liên quan đến vị trí được chỉ ra bởi thanh ghi FP. Trong khi việc thực thi nằm trong một hàm,

các tham số phụ (nếu có) nằm ở [fp, +offset], các biến cục bộ và tham số đã lưu ở [fp, –offset], tất cả đều được tham chiếu bởi

sử dụng FP làm thanh ghi cơ sở. Từ dòng mã hội (3) và (4), lưu 4 tham số đầu tiên được truyền trong r0-r3 và

gán giá trị cho các biến cục bộ x, y, z, chúng ta có thể thấy nội dung ngăn xếp trở thành như trong sơ đồ tiếp theo.

Cao SP thấp

----|+8 |+4 | 0 |-4 |-8 |-12|-16|-20|-24|-28|-32|-36|---


| f | e |LR |FP | x | y | z | ? | một | b | c | d |
----|exParam|-|-|---|- người dân địa phương --|---| đã lưu r0-r3 --|---
FP

|< ----------- khung ngăn xếp của func ----------- >|

Mặc dù ngăn xếp là một phần của bộ nhớ liền kề, nhưng về mặt logic, mỗi chức năng chỉ có thể truy cập vào một vùng giới hạn của ngăn xếp.

Vùng ngăn xếp hiển thị cho một hàm được gọi là khung ngăn xếp của hàm và FP (r12) được gọi là con trỏ khung ngăn xếp.

Tại dòng mã hợp ngữ (5), hàm gọi g(x, y) chỉ với hai tham số. Nó tải x vào r0, y vào r1 và sau đó

BL đến g.

Tại dòng mã hợp ngữ (6), nó tính a + e là giá trị trả về trong r0.

Tại các dòng mã hợp ngữ (7), nó giải phóng không gian trong ngăn xếp, đưa FP và LR đã lưu vào PC, gây ra sự thực thi
quay lại với người gọi.

Mã được tạo bởi trình biên dịch ARM C chỉ sử dụng r0–r3. Nếu một hàm xác định bất kỳ biến đăng ký nào, chúng sẽ được gán

các thanh ghi r4–r11, được lưu trong ngăn xếp trước và được khôi phục sau khi hàm trả về. Nếu một chức năng không gọi ra,

không cần phải lưu/khôi phục thanh ghi liên kết LR. Trong trường hợp đó, mã được tạo bởi trình biên dịch ARM C không lưu và

khôi phục thanh ghi liên kết LR, cho phép vào/ra lệnh gọi hàm nhanh hơn.

2.7.3.3 Nhảy xa
Trong một chuỗi các lệnh gọi hàm, chẳng hạn như

main() –> A() –> B()–>C();

khi một hàm được gọi kết thúc, nó thường quay trở lại hàm gọi, ví dụ C() trả về B(), trả về A(), v.v.

cũng có thể quay lại trực tiếp chức năng trước đó trong chuỗi gọi bằng một bước nhảy dài. Chương trình sau

thể hiện bước nhảy xa trong Unix/Linux.

/***** longjump.c thể hiện bước nhảy xa trong Linux *****/

#include <stdio.h>

#include <setjmp.h>
jmp_buf env; chủ // để lưu môi trường longjmp

yếu()

{
int r, a=100;
printf("gọi setjmp để lưu môi trường\n");

if ((r = setjmp(env)) == 0){


MỘT();

printf("trở về bình thường\n");

}
khác{
Machine Translated by Google

24 2 Kiến trúc và lập trình ARM

printf("trở lại main() thông qua bước nhảy dài, r=%da=%d\n", r, a);

int A()

{ printf("nhập A()\n");

B();

printf("thoát A()\n");

int B()

printf("nhập B()\n"); printf("nhảy

xa? (y|n) "); nếu (getchar()=='y')

longjmp(env, 1234);

printf("thoát B()\n");

Trong chương trình longjump.c ở trên, trước tiên, hàm main() gọi setjmp(), lưu môi trường thực thi hiện tại trong cấu
trúc jmp_buf và trả về 0. Sau đó, nó tiếp tục gọi A(), gọi B(). Trong khi ở hàm B(), nếu người dùng chọn không quay lại bằng
cách nhảy xa, các hàm sẽ hiển thị trình tự trả về bình thường. Nếu người dùng chọn quay lại theo longjmp(env, 1234), việc
thực thi sẽ trở về môi trường đã lưu cuối cùng với giá trị khác 0. Trong trường hợp này, nó khiến B() quay trở lại main()
trực tiếp, bỏ qua A().
Nguyên tắc nhảy xa rất đơn giản. Khi một hàm kết thúc, nó sẽ trả về bằng (callerLR, callerFP) trong hiện tại
khung ngăn xếp, như thể hiện trong sơ đồ sau.

--------------------------------------------

|params|người gọiLR|người gọiFP|…………..|

------------|---------------------------------|---
CPU.FP CPU.SP

Nếu chúng ta thay thế (callerLR, callerFP) bằng (savedLR, saveFP) của một hàm trước đó trong chuỗi gọi, thì việc thực
thi sẽ quay trở lại trực tiếp với hàm đó. Ví dụ: chúng ta có thể triển khai setjmp(int env[2]) và longjmp(int env[2], int
value) trong tập hợp như sau.

.global setjmp, longjmp

setjmp:// int setjmp(int env[2]); lưu LR, FP trong env[2]; trở về 0

stmfd sp!, {fp, lr}

thêm fp, sp, #4

ldr r1, [fp] // LR trả về của người gọi

str r1, [r0] // lưu LR vào env[0]

ldr r1, [fp, #-4] // FP của người gọi

str r1, [r0, #4] // lưu FP vào env[1]

chuyển r0, #0 // trả về 0 cho người gọi

phụ sp, fp, #4

ldmfd sp!, {fp, pc}

longjmp:// int longjmp(int env[2], giá trị int)

stmfd sp!, {fp, lr}

thêm fp, sp, #4

ldr r2, [r0] // trả về LR của hàm

str r2, [fp] // thay thế LR đã lưu trong khung ngăn xếp

ldr r2, [r0, #4] // trả về FP của hàm

str r2, [fp, #-4] // thay thế FP đã lưu trong khung ngăn xếp

chuyển động r0, r1 // giá trị trả về

phụ sp, fp, #4


Machine Translated by Google

2.7 Lập trình ARM 25

ldmfd sp!, {fp, pc} // trả về thông qua REPLACED LR và FP

Nhảy xa có thể được sử dụng để hủy bỏ một chức năng trong chuỗi gọi, khiến việc thực thi tiếp tục trở lại môi trường đã biết
được lưu trước đó. Ngoài (savedLR, saveFP), setjmp() cũng có thể lưu các thanh ghi CPU khác và SP của người gọi, cho phép
longjmp() để khôi phục môi trường thực thi hoàn chỉnh của hàm ban đầu. Mặc dù hiếm khi được sử dụng trong chế độ người dùng
chương trình, nhảy xa là một kỹ thuật phổ biến trong lập trình hệ thống. Ví dụ: nó có thể được sử dụng trong bộ thu tín hiệu để
bỏ qua chức năng chế độ người dùng gây ra lỗi ngoại lệ hoặc bẫy. Chúng ta sẽ chứng minh kỹ thuật này sau trong Chap. số 8 TRÊN
tín hiệu và xử lý tín hiệu.

2.7.3.4 Gọi hàm hợp ngữ từ C


Chương trình ví dụ tiếp theo C2.3 cho thấy cách gọi hàm hợp ngữ từ C. Hàm main() trong C gọi hàm hợp ngữ
hàm sum() với 6 tham số, trả về tổng của tất cả các tham số. Theo quy ước gọi của
C, hàm main() truyền 4 tham số đầu tiên a, b, c, d trong r0–r3 và các tham số còn lại e, f, trên ngăn xếp. Khi nhập cảnh
đối với hàm được gọi, đỉnh ngăn xếp chứa các tham số e, f, theo thứ tự địa chỉ tăng dần. Hàm được gọi đầu tiên
thiết lập khung ngăn xếp bằng cách lưu LR, FP vào ngăn xếp và để FP (r12) trỏ vào LR lưu. Các tham số e và f bây giờ là
tại FP + 4 và FP + 8 tương ứng. Hàm sum chỉ cần cộng tất cả các tham số trong r0 và trả về cho người gọi.

(1)./************ file tc của Chương trình C2.3 *************/

int g; // toàn cục chưa được khởi tạo

int chính()

int a, b, c, d, e, f; // biến cục bộ

a = b = c =d =e = f = 1; // giá trị không quan trọng

g = tổng(a,b,c,d,e,f); // gọi sum(), truyền a,b,c,d,e,f

(2)./************ file ts.s của Chương trình C2.3 **********/

.global bắt đầu, tính tổng

bắt đầu: ldr sp, =stack_top

bl chính // gọi hàm main() trong C

dừng lại: b dừng lại

tổng: // int sum(int a,b,c,d,e,f){ return a+b+e+d+e+f;}

// khi vào, đỉnh ngăn xếp chứa e, f, được truyền bởi main() trong C

// Thiết lập khung ngăn xếp

stmfd sp!, {fp, lr} // đẩy fp, lr

thêm fp, sp, #4 // fp -> đã lưu lr vào ngăn xếp

// Tính tổng tất cả (6) tham số

thêm r0, r0, r1 // 4 tham số đầu tiên nằm trong r0-r3

thêm r0, r0, r2

thêm r0, r0, r3

ldr r3, [fp, #4] // nạp e vào r3

thêm r0, r0, r3 // cộng vào tổng trong r0

ldr r3, [fp, #8] // nạp f vào r3

thêm r0, r0, r3 // cộng vào tổng trong r0

// Quay lại người gọi

phụ sp, fp, #4 // sp=fp-4 (trỏ vào FP đã lưu)

ldmfd sp!, {fp, pc} // quay lại người gọi

Cần lưu ý rằng trong chương trình C2.3, hàm sum() không lưu r0–r3 mà sử dụng chúng trực tiếp. Vì vậy, mã
sẽ hiệu quả hơn mức được tạo bởi trình biên dịch ARM GCC. Điều này có nghĩa là chúng ta nên viết tất cả các chương trình trong
cuộc họp? Câu trả lời tất nhiên là KHÔNG. Người đọc sẽ dễ dàng tìm ra lý do.
Machine Translated by Google

26 2 Kiến trúc và lập trình ARM

(3). Biên dịch liên kết tc và ts.s thành một tệp thực thi

arm-none-eabi-as –o ts.o ts.s # tập hợp ts.s

arm-none-eabi-gcc –c tc # biên dịch tc thành

arm-none-eabi-ld –T t.ld –o t.elf to ts.o # liên kết tới tập tin t.elf

arm-none-eabi-objcopy –O nhị phân t.elf t.bin # chuyển đổi t.elf thành t.bin

(4). Chạy t.bin và kiểm tra kết quả trên máy ảo ARM như trước.

2.7.3.5 Gọi hàm C từ hợp ngữ Gọi hàm C với các


tham số từ hợp ngữ cũng dễ dàng nếu chúng ta tuân theo quy ước gọi của C. Chương trình C2.4 sau đây trình bày cách gọi một hàm
C từ hợp ngữ.

/*********** tc file của Chương trình 2.4 ****************/ int sum(int x, int y){ return

x + y; } // tập tin tc

/************ tệp ts.s của Chương trình C2.4 *************/

.chữ

.global bắt đầu, tính tổng

bắt đầu:

ldr sp, = stack_top // cần một ngăn xếp để thực hiện cuộc gọi

ldr r2, =a

ldr r0, [r2] // r0 = a

ldr r2, =b

ldr r1, [r2] // r1 = b

tổng số tiền // c = tổng(a,b)

ldr r2, =c

str r0, [r2] // lưu trữ giá trị trả về trong c

dừng lại: b dừng lại

.dữ liệu

Một: .word 1

b: .word 2

c: .word 0

2.7.3.6 Hợp ngữ nội tuyến


Trong các ví dụ trên, chúng ta đã viết mã hợp ngữ trong một tệp riêng biệt. Hầu hết các chuỗi công cụ ARM đều dựa trên GCC.
Trình biên dịch GCC hỗ trợ lắp ráp nội tuyến, thường được sử dụng trong mã C để thuận tiện. Định dạng cơ bản của lắp ráp nội tuyến là

__asm__("mã hợp ngữ"); hoặc đơn giản là asm("mã hợp ngữ");

Nếu mã hợp ngữ có nhiều hơn một dòng, các câu lệnh sẽ được phân tách bằng \n\t; như trong

asm("mov %r0, %r1\n\t; thêm %r0,#10,r0\n\t");

Mã tập hợp nội tuyến cũng có thể chỉ định toán hạng. Mẫu mã lắp ráp nội tuyến như vậy là

asm (mẫu trình biên dịch mã

: toán hạng đầu ra

: toán hạng đầu vào

: danh sách các thanh ghi bị ghi đè

);
Machine Translated by Google

2.7 Lập trình ARM 27

Các câu lệnh hợp ngữ có thể chỉ định các toán hạng đầu ra và đầu vào, được tham chiếu là %0, %1. Ví dụ, trong

đoạn mã sau,

int a, b=10;

asm("mov %1,%%r0; mov %%r0,%0;" // sử dụng %%REG cho các thanh ghi
:"=r"(a) // đầu ra PHẢI có =
:"r"(b) // đầu vào
:"%r0" // thanh ghi bị ghi đè
);

Trong đoạn mã trên, %0 tham chiếu đến a, %1 tham chiếu đến b, %%r0 tham chiếu đến thanh ghi r0. Toán tử ràng buộc "r" có nghĩa là

sử dụng một thanh ghi cho toán hạng. Nó cũng cho trình biên dịch GCC biết rằng thanh ghi r0 sẽ bị mã nội tuyến ghi đè.

Mặc dù chúng ta có thể chèn mã hợp ngữ nội tuyến khá phức tạp vào chương trình C, nhưng việc lạm dụng quá mức có thể ảnh hưởng đến khả

năng đọc của chương trình. Trong thực tế, chỉ nên sử dụng tập hợp nội tuyến nếu mã rất ngắn, ví dụ: một lệnh tập hợp đơn lẻ hoặc hoạt

động dự định liên quan đến thanh ghi điều khiển CPU. Trong những trường hợp như vậy, mã hợp ngữ nội tuyến không chỉ rõ ràng mà còn hiệu

quả hơn so với việc gọi một hàm hợp ngữ.

2.8 Trình điều khiển thiết bị

Bảng mạch ARM Versatilepb được mô phỏng là một máy ảo. Nó hoạt động giống như một hệ thống phần cứng thực sự nhưng không có trình điều

khiển cho các thiết bị ngoại vi được mô phỏng. Để thực hiện bất kỳ chương trình có ý nghĩa nào, dù trên hệ thống thực hay ảo, chúng ta

phải triển khai trình điều khiển thiết bị để hỗ trợ các hoạt động I/O cơ bản. Trong cuốn sách này, chúng ta sẽ phát triển trình điều

khiển cho các thiết bị ngoại vi được sử dụng phổ biến nhất bằng một loạt các ví dụ lập trình. Chúng bao gồm trình điều khiển cho cổng

nối tiếp UART, bộ hẹn giờ, màn hình LCD, bàn phím và thẻ SD đa phương tiện, sau này sẽ được sử dụng làm thiết bị lưu trữ cho hệ thống tệp.

Trình điều khiển thiết bị thực tế nên sử dụng các ngắt. Chúng ta sẽ trình bày các trình điều khiển thiết bị điều khiển ngắt trong

Chương. 3 khi chúng ta thảo luận về ngắt và xử lý ngắt. Sau đây, chúng tôi sẽ trình bày trình điều khiển UART đơn giản bằng cách thăm

dò và trình điều khiển LCD không sử dụng các ngắt. Để làm được những điều này, cần phải biết kiến trúc hệ thống ARM Versatile.

2.8.1 Bản đồ bộ nhớ hệ thống

Kiến trúc hệ thống ARM sử dụng I/O được ánh xạ bộ nhớ. Mỗi thiết bị I/O được gán một khối bộ nhớ liền kề trong bản đồ bộ nhớ hệ thống.

Các thanh ghi nội bộ của mỗi thiết bị I/O được truy cập dưới dạng phần bù từ địa chỉ cơ sở của thiết bị. Bảng 2.1 thể hiện sơ đồ bộ

nhớ (rút gọn) của bo mạch ARM Versatile/926EJ-S (ARM 926EJ-S 2016). Trong bản đồ bộ nhớ, các thiết bị I/O chiếm diện tích 2 MB bắt đầu

từ 256 MB.

2.8.2 Lập trình GPIO

Hầu hết các bo mạch hệ thống dựa trên ARM đều cung cấp các chân Đầu vào-Đầu ra Mục đích Chung (GPIO) làm giao diện I/O cho hệ thống.

Một số chân GPIO có thể được cấu hình cho đầu vào. Các chân khác có thể được cấu hình cho đầu ra. Trong nhiều khóa học về hệ thống

nhúng cấp độ sơ cấp, các bài tập lập trình và dự án khóa học thường là lập trình các chân GPIO của một bo mạch hệ thống nhúng nhỏ để

giao tiếp với một số thiết bị thực, chẳng hạn như công tắc, cảm biến, đèn LED và rơle, v.v. /O thiết bị, việc lập trình GPIO tương đối

đơn giản. Giao diện GPIO, ví dụ như LPC2129 GPIO MCU được sử dụng trong nhiều bo mạch hệ thống nhúng đời đầu, bao gồm bốn thanh ghi 32

bit.

GPIODIR: đặt hướng pin; 0 cho đầu vào, 1 cho đầu ra GPIOSET:

đặt mức điện áp chân ở mức cao (3,3 V)

GPIOCLR: đặt mức điện áp chân ở mức thấp (0 V)

GPIOPIN: đọc thanh ghi này trả về trạng thái của tất cả các chân
Machine Translated by Google

28 2 Kiến trúc và lập trình ARM

Bảng 2.1 Sơ đồ bộ nhớ của ARM đa năng/ARM926EJ-S

Chip MPMC Chọn 0, SRAM 128 MB 0x00000000 128 MB

Chip MPMC Chọn 1, SRAM mở rộng 128 MB 0x08000000 128 MB

Đăng ký hệ thống 0x10000000 4 KB

Bộ điều khiển ngắt thứ cấp (SIC) 0x10003000 4 KB

Giao diện thẻ đa phương tiện 0 (MMCID) 0x10005000 4 KB

Giao diện bàn phím/chuột 0 (bàn phím) 0x10006000 4 KB

Dành riêng (Giao diện UART3) 0x10009000 4 KB

Giao diện Ethernet 0x10010000 64 KB

Giao diện USB 0x10020000 64 KB

Bộ điều khiển LCD màu 0x10120000 64 KB

Bộ điều khiển DMA 0x10130000 64 KB

Bộ điều khiển ngắt Vector (PIC) 0x10140000 64 KB

Bộ điều khiển hệ thống OxlOlEOOOO 4 KB

Giao diện cơ quan giám sát OxlOlElOOO 4 KB

Giao diện mô-đun hẹn giờ 0 và 1 0xl01E2000 4 KB

(Bộ định thời 1 lúc 0xl01E2020) 0xl01E2FFF

Giao diện mô-đun hẹn giờ 2 và 3 0X101E3000 4 KB

(Bộ hẹn giờ 3 ở 0xl01E3020) 0X101E3FFF

Giao diện GPIO (cổng 0) 0X101E4000 4 KB

Giao diện GPIO (cổng 1) 0xl01E5000 4 KB

Giao diện GPIO (cổng 2) 0X101E6000 4 KB

Giao diện UART 0 OxlOlElOOO 4 KB

Giao diện UART 1 0X101E2000 4 KB

Giao diện UART 2 0xl01F3000 4 KB

Bộ nhớ mở rộng tĩnh SSMC 0x20000000 256 MB

Các thanh ghi GPIO có thể được truy cập dưới dạng các offset từ từ một địa chỉ cơ sở (được ánh xạ bộ nhớ). Trong các thanh ghi GPIO, mỗi

bit tương ứng với chân GPIO. Tùy thuộc vào cài đặt hướng trong IODIR, mỗi chân có thể được kết nối với một chân thích hợp
Thiết bị vào/ra.

Như một ví dụ cụ thể, giả sử rằng chúng tôi muốn sử dụng GPIO pin0 cho đầu vào, được kết nối với một (công tắc loại bỏ),

và chân 1 cho đầu ra, được kết nối với (mặt đất) của đèn LED có nguồn điện áp +3,3 V riêng và một

điện trở hạn chế dòng điện. Chúng ta có thể lập trình các thanh ghi GPIO như sau.

GPIODIR: bit0=0 (đầu vào), bit1=1 (đầu ra);

GPIOSET: tất cả các bit=0 (không có chân nào được đặt ở mức cao);

GPIOCLR: bit1=1 (đặt ở mức THẤP hoặc nối đất);

GPIOPIN: đọc trạng thái pin, kiểm tra pin0 xem có đầu vào nào không.

Tương tự, chúng ta có thể lập trình các chân khác cho các chức năng I/O mong muốn. Việc lập trình các thanh ghi GPIO có thể được thực hiện bằng

mã hợp ngữ hoặc C. Với địa chỉ cơ sở GPIO và độ lệch thanh ghi, việc viết một điều khiển GPIO khá dễ dàng

chương trình mà

• bật đèn LED nếu nhấn hoặc đóng công tắc đầu vào, và

• tắt đèn LED nếu công tắc đầu vào được nhả hoặc mở.

Chúng tôi coi trường hợp này và các trường hợp lập trình GPIO khác dưới dạng bài tập trong phần Vấn đề. Trong một số hệ thống, GPIO

giao diện có thể phức tạp hơn nhưng nguyên tắc lập trình vẫn giữ nguyên. Ví dụ: trên ARM

Bảng mạch PB đa năng, các giao diện GPIO được sắp xếp thành các nhóm riêng biệt gọi là cổng (Port0 đến Port2), nằm ở phần đế
Machine Translated by Google

2.8 Trình điều khiển thiết bị 29

địa chỉ 0x101E4000-0x101E6000. Mỗi cổng cung cấp 8 chân GPIO, được điều khiển bởi thanh ghi GPIODIR (8 bit) và thanh ghi GPIODATA
(8 bit). Thay vì kiểm tra trạng thái chân đầu vào, đầu vào GPIO có thể sử dụng các ngắt. Mặc dù thú vị và truyền cảm hứng cho sinh
viên nhưng việc lập trình GPIO chỉ có thể được thực hiện trên các hệ thống phần cứng thực. Do các máy ảo ARM mô phỏng không có chân
GPIO nên chúng tôi chỉ có thể mô tả các nguyên tắc chung của lập trình GPIO. Tuy nhiên, tất cả các máy ảo ARM đều hỗ trợ nhiều loại
thiết bị I/O khác. Trong các phần sau, chúng tôi sẽ trình bày cách phát triển trình điều khiển cho các thiết bị đó.

2.8.3 Trình điều khiển UART cho I/O nối tiếp

Việc dựa vào các lệnh giám sát QEMU để hiển thị nội dung thanh ghi và bộ nhớ là rất tẻ nhạt. Sẽ tốt hơn nhiều nếu chúng ta có thể
phát triển trình điều khiển thiết bị để thực hiện I/O trực tiếp. Trong chương trình ví dụ tiếp theo, chúng ta sẽ viết trình điều
khiển UART đơn giản cho I/O trên các thiết bị đầu cuối nối tiếp được mô phỏng. Bo mạch đa năng ARM hỗ trợ bốn thiết bị PL011 UART
cho I/O nối tiếp (ARM PL011 2016). Mỗi thiết bị UART có một địa chỉ cơ sở trong bản đồ bộ nhớ hệ thống. Địa chỉ cơ sở của 4 UART là

UART0: 0x101F1000

UART1: 0x101F2000

UART2: 0x101F3000

UART3: 0x10090000

Mỗi UART có một số thanh ghi, là các byte lệch khỏi địa chỉ cơ sở. Sau đây liệt kê nhiều nhất
thanh ghi UART quan trọng.

0x00 UARTDR Thanh ghi dữ liệu: để đọc/ghi ký tự 0x18 Thanh ghi cờ UARTFR:

TxEmpty, RxFull, v.v. 0x24 UARIBRD Thanh ghi tốc độ truyền: đặt tốc độ truyền

0x2C Thanh ghi điều khiển dòng UARTLCR: số bit trên mỗi ký tự, tính chẵn

lẻ, v.v.

0x38 UARTIMIS Thanh ghi mặt nạ ngắt cho các ngắt TX và RX

Nói chung, UART phải được khởi tạo theo các bước sau.

(1). Viết giá trị chia vào thanh ghi tốc độ baud để có tốc độ truyền mong muốn. Hướng dẫn tham khảo kỹ thuật ARM PL011 liệt kê các
giá trị ước số nguyên sau (dựa trên đồng hồ UART 7,38 MHz) cho tốc độ truyền thường được sử dụng:

0x4 = 1152000, 0xC = 38400, 0x18 = 192000, 0x20 = 14400, 0x30 = 9600

(2). Ghi vào thanh ghi Line Control để chỉ định số bit trên mỗi char và chẵn lẻ, ví dụ 8 bit trên mỗi char không có chẵn lẻ.
(3). Ghi vào thanh ghi Mặt nạ ngắt để bật/tắt các ngắt RX và TX

Khi sử dụng bo mạch ARM Versatilepb mô phỏng, có vẻ như QEMU tự động sử dụng các giá trị mặc định cho cả tham số điều khiển
đường truyền và tốc độ truyền, khiến các bước (1) và (2) trở thành tùy chọn hoặc không cần thiết. Trên thực tế, người ta thấy rằng
việc ghi bất kỳ giá trị nào vào thanh ghi số nguyên (0x24) sẽ hoạt động nhưng đây không phải là tiêu chuẩn cho UART trong các hệ
thống thực. Đối với bo mạch Versatilepb được mô phỏng, tất cả những gì chúng ta cần làm là lập trình thanh ghi Mặt nạ ngắt (nếu sử
dụng các ngắt) và kiểm tra thanh ghi Cờ trong quá trình I/O nối tiếp. Để bắt đầu, chúng ta sẽ triển khai I/O UART bằng cách thăm
dò, chỉ kiểm tra thanh ghi trạng thái Cờ. Trình điều khiển thiết bị điều khiển ngắt sẽ được đề cập ở phần sau của Chương. 3 khi
chúng ta thảo luận về ngắt và xử lý ngắt. Khi phát triển trình điều khiển thiết bị, chúng ta có thể cần mã lắp ráp để truy cập vào
các thanh ghi CPU và phần cứng giao diện. Tuy nhiên, chúng ta sẽ chỉ sử dụng mã hợp ngữ nếu thực sự cần thiết. Bất cứ khi nào có
thể, chúng tôi sẽ triển khai mã trình điều khiển bằng C, do đó giữ số lượng mã hợp ngữ ở mức tối thiểu. Trình điều khiển UART và
chương trình kiểm tra, C2.5, bao gồm các thành phần sau.

(1). ts.s: khi CPU ARM khởi động, nó ở chế độ Giám sát hoặc SVC. Tệp ts.s đặt con trỏ ngăn xếp chế độ SVC và gọi main() trong C.
Machine Translated by Google

30 2 Kiến trúc và lập trình ARM

.global start, stack_top // stack_top được xác định trong t.ld

bắt đầu:

ldr sp, =stack_top // đặt con trỏ ngăn xếp chế độ SVC

bl chính // gọi hàm main() trong C


b. // nếu main() trả về, chỉ cần lặp

(2). tc: tệp này chứa hàm main(), hàm này khởi tạo UART và sử dụng UART0 cho I/O trên cổng nối tiếp.

/*************** tệp tc của C2.5 ****************/

int v[] = {1,2,3,4,5,6,7,8,9,10}; // mảng dữ liệu

tổng số nguyên;

#include "string.c" // chứa strlen(), strcmp(), v.v.

#include "uart.c" // tập tin mã trình điều khiển UART

int chính()

int tôi;

chuỗi char[64];

UART *lên;

uart_init(); // khởi tạo UART

lên = &uart[0]; // kiểm tra UART0

uprints(up, "Nhập dòng từ thiết bị đầu cuối nối tiếp 0\n\r");

trong khi(1){

ugets(lên, chuỗi);

uprints(up, " ");

uprints(up, string);

uprints(lên, "\n\r");

if (strcmp(string, "end")==0)

phá vỡ;

uprints(up, "Tính tổng của mảng:\n\r");

tổng = 0;

cho (i=0; i<10; i++)

tổng += v[i];

uprints(up, "sum = ");

uputc(up, (sum/10)+'0'); uputc(up, (sum%10)+'0');

uprints(up, "\n\rEND OF RUN\n\r");

(3). Tệp uart.c: tệp này triển khai trình điều khiển UART đơn giản. Trình điều khiển sử dụng thanh ghi dữ liệu UART cho các ký tự đầu vào/đầu ra,

và nó kiểm tra thanh ghi cờ xem thiết bị đã sẵn sàng chưa. Sau đây liệt kê ý nghĩa của nội dung thanh ghi UART.

Thanh ghi dữ liệu (offset 0x00): dữ liệu vào (READ)/data out (WRITE)

Thanh ghi cờ (offset 0x18): trạng thái của cổng UART

76 543 2 1 0

- -
| TXFE RXFF TXFF RXFE BẬN - |

trong đó TXFE=Bộ đệm Tx trống, Bộ đệm RXFF=Rx đầy, TXFF=Bộ đệm Tx đầy,

RXFE=Bộ đệm Rx trống, BẬN=thiết bị bận.


Machine Translated by Google

2.8 Trình điều khiển thiết bị 31

Một cách tiêu chuẩn để truy cập các bit riêng lẻ của một thanh ghi là định nghĩa chúng dưới dạng các hằng số ký hiệu, như trong

#define TXFE 0x80

#define RXFF 0x40

#define TXFF 0x20

#define RXFE 0x10

#define BẬN 0x08

Sau đó sử dụng chúng làm mặt nạ bit để kiểm tra các bit khác nhau của thanh ghi cờ. Phần sau đây hiển thị mã trình điều khiển UART.

/******** file uart.c của C2.5 : Mã trình điều khiển UART ********/

/*** độ lệch byte của thanh ghi UART từ char *base ***/

#xác định UDR 0x00

#xác định UFR 0x18

typedef cấu trúc dễ bay hơi uart {

char *cơ sở; // địa chỉ cơ sở; như ký tự *

int n; // số uart 0-3

}UART;

UART uart[4]; // 4 cấu trúc UART

int uart_init() // Hàm khởi tạo UART

int tôi; UART *lên;

for (i=0; i<4; i++){ // uart0 đến uart2 liền kề

lên = &uart[i];

up->base = (char *)(0x101F1000 + i*0x1000);

lên->n = tôi;

uart[3].base = (char *)(0x10009000); // uart3 tại 0x10009000

int ugetc(UART *up) // nhập một char từ UART trỏ lên

while (*(up->base+UFR) & RXFE); // vòng lặp nếu UFR là REFE

trả về *(lên->cơ sở+UDR); // trả về một ký tự trong UDR

int uputc(UART *up, charc) // xuất một char sang UART trỏ lên

while (*(up->base+UFR) & TXFF); // vòng lặp nếu UFR là TXFF

*(up->base+UDR) = c; // ghi char vào thanh ghi dữ liệu

int upgets(UART *up, char *s) // nhập một chuỗi ký tự

while ((*s = ugect(up)) != '\r') { uputc(up, *s);

s++;

*s = 0;

int up(UART *up, char *s) // xuất ra một chuỗi ký tự

trong khi (*s)

uputc(up, *s++);

}
Machine Translated by Google

32 2 Kiến trúc và lập trình ARM

(4). file link-script: Tệp script liên kết, t.ld, giống như trong chương trình C2.2. (5). mk

và chạy file script: File mk script cũng giống như trong C2.2. Đối với một cổng nối tiếp, tập lệnh chạy là

qemu-system-arm -M multiplenpb -m 128M -kernel t.bin -serial mon:stdio

Để có nhiều cổng nối tiếp hơn, hãy thêm –serial /dev/pts/1 –serial /dev/pts/2, v.v. vào dòng lệnh. Trong Linux, hãy mở (các) xterm dưới

dạng thiết bị đầu cuối giả. Nhập lệnh ps Linux để xem số pts/n của thiết bị đầu cuối giả, số này phải khớp với số pts/n trong tùy chọn –

serial /dev/pts/n của QEMU. Trên mỗi thiết bị đầu cuối giả, có một tiến trình sh Linux đang chạy, quá trình này sẽ lấy tất cả đầu vào của

thiết bị đầu cuối. Để sử dụng thiết bị đầu cuối giả làm cổng nối tiếp, tiến trình sh Linux phải được đặt ở trạng thái không hoạt động.

Điều này có thể được thực hiện bằng cách nhập lệnh Linux sh

ngủ 1000000

cho phép quá trình Linux sh ngủ trong một số lượng lớn giây. Sau đó, thiết bị đầu cuối giả có thể được sử dụng làm cổng nối tiếp của QEMU.

2.8.3.1 Trình diễn trình điều khiển UART Trong tệp

uart.c, mỗi thiết bị UART được biểu thị bằng cấu trúc dữ liệu UART. Hiện tại, cấu trúc UART chỉ chứa địa chỉ cơ sở và số ID đơn vị. Trong

quá trình khởi tạo UART, địa chỉ cơ sở của từng cấu trúc UART được đặt thành địa chỉ vật lý của thiết bị UART. Các thanh ghi UART được

truy cập dưới dạng *(up->base+OFFSET) trong C. Trình điều khiển bao gồm 2 hàm I/O cơ bản, ugetc() và uputc().

(1). int ugetc(UART *up): hàm này trả về một char từ cổng UART. Nó lặp cho đến khi thanh ghi cờ UART không còn là RXFE, cho biết có một

ký tự trong thanh ghi dữ liệu. Sau đó, nó đọc thanh ghi dữ liệu, xóa bit RXFF và đặt bit RXFE trong FR và trả về char. (2). int uputc(UART
*up, c): hàm này xuất ra một char tới cổng

UART. Nó lặp cho đến khi thanh ghi cờ của UART không còn là TXFF, cho biết UART đã sẵn sàng truyền một char khác. Sau đó nó ghi char vào

thanh ghi dữ liệu để truyền ra ngoài.

Các hàm ugets() và uprints() dành cho I/O của chuỗi hoặc dòng. Chúng dựa trên ugec() và uputc(). Đây là cách điển hình để phát triển

các chức năng I/O. Ví dụ: với get(), chúng ta có thể triển khai hàm int itoa(char *s) để chuyển đổi một chuỗi các chữ số thành số nguyên.

Tương tự, với putc(), chúng ta có thể triển khai hàm printf() để in có định dạng, v.v. Chúng ta sẽ phát triển và trình diễn hàm printf()

trong phần tiếp theo về trình điều khiển LCD. Hình 2.6 cho thấy kết quả đầu ra khi chạy chương trình C2.5, minh họa trình điều khiển UART.

2.8.3.2 Sử dụng Phiên Telnet TCP/IP làm Cổng UART Ngoài các

thiết bị đầu cuối giả, QEMU cũng hỗ trợ các phiên telnet TCP/IP làm cổng nối tiếp. Đầu tiên chạy chương trình như

qemu-system-arm -M linh hoạtpb -m 128M -kernel t.bin \

-telnet nối tiếp: localhost: 1234, máy chủ

Khi QEMU khởi động, nó sẽ đợi cho đến khi kết nối telnet được thực hiện. Từ một thiết bị đầu cuối (X-window) khác, nhập telnet

localhost 1234 để kết nối. Sau đó, nhập các dòng từ thiết bị đầu cuối telnet.

Hình 2.6 Trình diễn chương trình điều khiển UART


Machine Translated by Google

2.8 Trình điều khiển thiết bị 33

Trình điều khiển màn hình LCD màu 2.8.4

Bo mạch đa năng ARM hỗ trợ màn hình LCD màu, sử dụng bộ điều khiển LCD màu ARM PL110 (Bộ điều khiển LCD màu ARM PrimeCell PL110, Bảng cơ sở

ứng dụng đa năng ARM cho ARM926EF-S). Trên bảng Đa năng, bộ điều khiển LCD ở địa chỉ cơ sở 0x10120000. Nó có một số thanh ghi thời gian và

điều khiển, có thể được lập trình để cung cấp các chế độ và độ phân giải hiển thị khác nhau. Để sử dụng màn hình LCD, các thanh ghi điều

khiển và định thời của bộ điều khiển phải được thiết lập đúng cách. Hướng dẫn sử dụng Bảng chân đế ứng dụng đa năng của ARM cung cấp các

cài đặt thanh ghi thời gian sau cho chế độ VGA và SVGA.

Chế độ Độ phân giải OSC1 thời gianReg0 thời gianReg1 thời gianReg2
-------------------------------------------------- ----------

VGA 640x480 0x02C77 0x3F1F3F9C 0x090B61DF 0x067F1800

SVGA 800x600 0x02CAC 0x1313A4C4 0x0505F6F7 0x071F1800

-------------------------------------------------- ----------

Thanh ghi địa chỉ bộ đệm khung của LCD phải trỏ đến bộ đệm khung trong bộ nhớ. Với 24 bit cho mỗi pixel, mỗi pixel được biểu thị bằng

một số nguyên 32 bit, trong đó 3 byte thấp là giá trị BGR của pixel. Đối với chế độ VGA, kích thước bộ đệm khung cần thiết là 1220 KB byte.

Đối với chế độ SVGA, kích thước bộ đệm khung cần thiết là 1895 KB. Để hỗ trợ cả hai chế độ VGA và SVGA, chúng tôi sẽ phân bổ kích thước bộ

đệm khung là 2 MB. Giả sử rằng chương trình điều khiển hệ thống chạy trong bộ nhớ vật lý 1 MB thấp nhất, chúng ta sẽ phân bổ vùng bộ nhớ từ

2 đến 4 MB cho bộ đệm khung. Trong thanh ghi Điều khiển LCD (0x1010001C), bit0 được bật LCD và bit11 đang bật nguồn, cả hai phải được đặt

thành 1. Các bit khác dành cho thứ tự byte, số bit trên mỗi pixel, chế độ đơn sắc hoặc màu, v.v. Trong LCD driver, bit3–1 được đặt thành

101 cho 24 bit trên mỗi pixel, theo mặc định, tất cả các bit khác là 0 đối với thứ tự byte cuối nhỏ. Người đọc có thể tham khảo sổ tay kỹ

thuật LCD để biết ý nghĩa của các bit khác nhau. Cần lưu ý rằng, mặc dù hướng dẫn sử dụng ARM liệt kê thanh ghi Điều khiển LCD ở 0x1C, nhưng

thực tế nó là 0x18 trên bảng Versatilepb mô phỏng của QEMU. Lý do cho sự khác biệt này vẫn chưa được biết.

2.8.4.1 Hiển thị các tập tin hình ảnh

Là một thiết bị hiển thị được ánh xạ bộ nhớ, màn hình LCD có thể hiển thị cả hình ảnh và văn bản. Thực sự việc hiển thị hình ảnh dễ dàng

hơn nhiều so với văn bản. Nguyên tắc hiển thị hình ảnh khá đơn giản. Một hình ảnh bao gồm H (chiều cao) x W (chiều rộng) pixel, trong đó

H<=480 và W<=640 (đối với chế độ VGA). Mỗi pixel được chỉ định bởi giá trị màu RGB 3 byte. Để hiển thị hình ảnh, chỉ cần trích xuất các giá

trị RGB của từng pixel và ghi chúng vào vị trí pixel tương ứng trong bộ đệm khung hiển thị. Có nhiều định dạng tệp hình ảnh khác nhau, chẳng

hạn như BMP, JPG, PNG, v.v. Các ứng dụng dành cho Microsoft Windows thường sử dụng hình ảnh BMP. Hình ảnh JPG rất phổ biến trên các trang

Web trên Internet do kích thước nhỏ hơn của chúng. Mỗi tệp hình ảnh có một tiêu đề chứa thông tin của hình ảnh. Về nguyên tắc, việc đọc

tiêu đề tệp và sau đó trích xuất các pixel của tệp hình ảnh là khá dễ dàng. Tuy nhiên, nhiều tệp hình ảnh thường ở định dạng nén, ví dụ:

tệp JPG, trước tiên phải được giải nén.

Vì mục đích của chúng tôi ở đây là hiển thị trình điều khiển màn hình LCD, thay vì thao tác với các tệp hình ảnh, chúng tôi sẽ chỉ sử dụng

các tệp BMP màu 24-bit do định dạng hình ảnh đơn giản của chúng. Bảng 2.2 cho thấy định dạng của file BMP.

Tệp hình ảnh BMP màu 24 bit không bị nén. Nó bắt đầu bằng tiêu đề tệp 14 byte, trong đó hai byte đầu tiên là chữ ký tệp BMP 'M' và 'B',

cho biết đó là tệp BMP. Theo sau tiêu đề tệp là tiêu đề hình ảnh 40 byte, chứa chiều rộng (W) và chiều cao (H) của hình ảnh theo số pixel ở

độ lệch byte lần lượt là 18 và 22. Tiêu đề hình ảnh cũng chứa các thông tin khác, có thể bỏ qua đối với các tệp BMP đơn giản. Ngay sau tiêu

đề hình ảnh là các giá trị BGR 3 byte của các pixel hình ảnh được sắp xếp thành hàng H. Trong tệp BMP, hình ảnh được lưu trữ lộn ngược.

Hàng đầu tiên trong tệp hình ảnh thực sự là hàng dưới cùng của hình ảnh. Mỗi hàng chứa (W*3) được nâng lên bội số của 4 byte. Chương trình

ví dụ đọc hình ảnh BMP và hiển thị chúng trên màn hình LCD. Vì màn hình LCD chỉ có thể hiển thị 640x480 pixel ở chế độ VGA nên hình ảnh lớn

hơn có thể được hiển thị ở kích thước giảm, ví dụ: 1/2 hoặc 1/4 kích thước ban đầu của chúng.

2.8.4.2 Bao gồm các phần dữ liệu nhị phân Các tệp

hình ảnh thô có thể được bao gồm dưới dạng các phần dữ liệu nhị phân trong một hình ảnh thực thi. Giả sử rằng IMAGE là một tệp hình ảnh thô.

Các bước sau đây cho biết cách đưa nó vào dưới dạng phần dữ liệu nhị phân trong hình ảnh thực thi.

(1). Chuyển đổi dữ liệu thô thành mã đối tượng bằng objcopy

arm-none-eabi-objcopy –I nhị phân –O elf32-littlearm –B cánh tay HÌNH ẢNH HÌNH ẢNH.o
Machine Translated by Google

34 2 Kiến trúc và lập trình ARM

Bảng 2.2 Định dạng file BMP

Bù lại Kích cỡ Sự miêu tả

--------------- Tiêu đề tệp 14 byte ---------

0 2 Chữ ký (′MB′)

2 4 Kích thước của tệp BMP tính bằng byte

6 2 Đặt trước (0)

số 8 2 Đặt trước (0)

10 4 Offset để bắt đầu dữ liệu hình ảnh theo byte

---------------- Tiêu đề hình ảnh 40 byte -----------------

14 4 Kích thước tiêu đề hình ảnh (40)

18 4 Chiều rộng hình ảnh tính bằng pixel

22 4 Chiều cao hình ảnh tính bằng pixel

26 2 Số lượng mặt phẳng hình ảnh (1)

28 2 Số bit trên mỗi pixel (24)

--------------- Các lĩnh vực khác -------------------------

50 4 Số lượng màu quan trọng (0)

----------------- Hàng hình ảnh -----------------

54 đến cuối tập tin: hàng hình ảnh

(2). Bao gồm mã đối tượng dưới dạng phần dữ liệu nhị phân trong tập lệnh liên kết

#---- tập lệnh liên kết t.ld -------

NHẬP(reset_start)

PHẦN

{ . = 0x10000;

.chữ : { ts.o *( .text) }

.dữ liệu : { *(.dữ liệu) }


.bss : { *(.bss) }

.data : { *(image.o) } /* bao gồm image.o làm phần dữ liệu */

/* vùng ngăn xếp */

2.8.4.3 Ví dụ lập trình C2.6: Trình điều khiển LCD


Chương trình ví dụ C2.6 triển khai trình điều khiển LCD hiển thị các tệp hình ảnh thô. Chương trình bao gồm các

các thành phần sau.

(1). Tệp ts.s: Vì chương trình trình điều khiển không sử dụng các ngắt cũng như không cố gắng xử lý bất kỳ trường hợp ngoại lệ nào nên không cần phải

cài đặt các vectơ ngoại lệ. Khi vào, nó thiết lập ngăn xếp chế độ SVC và gọi main() trong C.

/*********** tệp ts.s của C2.6 **********/

.global reset_start

đặt lại_start:

LDR sp, =stack_top // đặt con trỏ ngăn xếp SVC

BL chính

B .
Machine Translated by Google

2.8 Trình điều khiển thiết bị 35

(2). Tệp vid.c: Đây là trình điều khiển LCD. Nó khởi tạo các thanh ghi LCD ở chế độ VGA có độ phân giải 640x480 và đặt bộ đệm
khung ở mức 2 MB. Nó cũng bao gồm mã cho chế độ SVGA với độ phân giải 800x600 nhưng chúng đã bị loại bỏ.

/************** vid.c file của C2.6 *******************/

int dễ bay hơi *fb;

int CHIỀU RỘNG = 640; // mặc định ở chế độ VGA cho 640x480

int fbuf_init(chế độ int)

fb = (int *)(0x200000); // ở mức 2 MB đến 4 MB

//****************** cho VGA 640x480 ****************/

*(dễ bay hơi unsigned int *)(0x1000001c) = 0x2C77;

*(dễ bay hơi unsigned int *)(0x10120000) = 0x3F1F3F9C;

*(dễ bay hơi unsigned int *)(0x10120004) = 0x090B61DF;

*(dễ bay hơi unsigned int *)(0x10120008) = 0x067F1800;

/****************** cho 800X600 SVGA ****************

*(dễ bay hơi unsigned int *)(0x1000001c) = 0x2CAC; *(dễ bay hơi unsigned

int *)(0x10120000) = 0x1313A4C4; *(dễ bay hơi unsigned int *)(0x10120004) =

0x0505F6F7; *(dễ bay hơi unsigned int *)(0x10120008) = 0x071F1800;

**************************************************** **/

*(dễ bay hơi unsigned int *)(0x10120010) = 0x200000; // fbuf *(dễ bay hơi unsigned int *)

(0x10120018) = 0x82B;

(3). Tệp uart.c: Đây là trình điều khiển UART tương tự trong Ví dụ C2.5, ngoại trừ việc nó sử dụng hàm uputc() cơ bản để triển
khai hàm uprintf() cho việc in được định dạng.
(4). Tệp tc: Tệp này chứa hàm main() gọi hàm show_bmp() để hiển thị hình ảnh. Trong tập lệnh liên kết, hai tệp hình ảnh, image1
và image2, được đưa vào dưới dạng phần dữ liệu nhị phân trong hình ảnh thực thi. Vị trí bắt đầu của tệp hình ảnh có thể được truy
cập bằng các ký hiệu _binary_imageI_start do trình liên kết tạo ra.

/*************** file tc của chương trình C2.6 *******************/

#include "defines.h" // địa chỉ cơ sở thiết bị, v.v.

#include "vid.c" // Trình điều khiển LCD

#include "uart.c" // trình điều khiển UART

char bên ngoài _binary_image1_start, _binary_image2_start;

#xác định chiều rộng 640

int show_bmp(char *p, int start_row, int start_col)

int h, w, pixel, rsize, i, j; char không dấu

r, g, b;

char *pp;

int *q = (int *)(p+14); w = *(q+1); // bỏ qua tiêu đề tệp 14 byte // chiều rộng hình

h = *(q+2); p += ảnh tính bằng pixel // chiều cao hình

54; ảnh tính bằng pixel // p-> pixel trong

hình ảnh

// Ảnh BMP bị lộn ngược, mỗi hàng là bội số của 4 byte

rsize = 4*((3*w + 3)/4); // bội số của 4

p += (h-1)*rsize; // hàng pixel cuối cùng

for (i=start_row; i<start_row + h; i++){

pp = p;

cho (j=start_col; j<start_col + w; j++){


Machine Translated by Google

36 2 Kiến trúc và lập trình ARM

b = *pp; g = *(pp+1); r = *(pp+2); // giá trị BRG

điểm ảnh = (b<<16) | (g<<8) | r; // giá trị pixel fb[i*WIDTH + j] = pixel; //

ghi vào bộ đệm khung // chuyển pp sang pixel tiếp theo

trang += 3;

p -= rsize; // về hàng trước

uprintf(\nBMP chiều cao ảnh=%d chiều rộng=%d\n", h, w);

int chính()

ký tự c,* p;

uart_init(); // khởi tạo UART

lên = upp[0]; // sử dụng UART0

fbuf_init(); // mặc định ở chế độ VGA

trong khi(1){

p = &_binary_image1_start;

show_bmp(p, 0, 80); // hiển thị hình ảnh1

uprintf("nhập key từ UART này: ");

ugetc(lên);

p = &_binary_image2_start;

show_bmp(p,120, 0); // hiển thị hình ảnh2

trong khi(1); // vòng lặp ở đây

(5). Tệp tập lệnh mk: Tập lệnh mk tạo mã đối tượng cho các tệp hình ảnh, được bao gồm dưới dạng các phần dữ liệu nhị phân trong hình

ảnh thực thi. # mk và

chạy file script của C2.6: Điểm mới duy nhất là chuyển đổi hình ảnh thành file đối tượng.

arm-none-eabi-objcopy -I nhị phân -O elf32-littlearm -B cánh tay image1 image1.o

arm-none-eabi-objcopy -I nhị phân -O elf32-littlearm -B cánh tay image2 image2.o

arm-none-eabi-as -mcpu=arm926ej-s ts.s -o ts.o arm-none-eabi-gcc -c

-mcpu=arm926ej-s tc -o to

arm-none-eabi-ld -T t.ld ts.o to -o t.elf

arm-none-eabi-objcopy -O nhị phân t.elf t.bin echo đã sẵn sàng chưa?

đọc giả

qemu-system-arm -M multiplenpb -m 128 M -kernel t.bin -serial mon:stdio

2.8.4.4 Trình diễn hình ảnh hiển thị trên LCD Hình 2.7 cho thấy
kết quả đầu ra mẫu khi chạy chương trình C2.6 ở chế độ VGA.

Phần trên cùng của Hình 2.7 hiển thị cổng I/O của UART. Phần dưới cùng của hình hiển thị màn hình LCD. Khi chương trình bắt đầu,

đầu tiên nó hiển thị image1 thành (row = 0, col = 80) trên màn hình LCD và nó cũng in kích thước hình ảnh thành UART0.

Nhập khóa đầu vào từ UART0 sẽ cho phép nó hiển thị image2 đến (row = 120, col = 0), v.v. Việc hiển thị các tệp hình ảnh có thể rất

thú vị, đó là cơ sở của hoạt hình máy tính. Các biến thể của chương trình hiển thị hình ảnh được liệt kê dưới dạng bài tập trong phần
Vấn đề dành cho bạn đọc quan tâm.

2.8.4.5 Hiển thị văn bản


Để hiển thị văn bản, chúng ta cần một tệp phông chữ, tệp này chỉ định phông chữ hoặc mẫu bit của ký tự ASCII. Tệp phông chữ, font.bin,

là một bitmap thô gồm 128 ký tự ASCII, trong đó mỗi char được biểu thị bằng một bitmap 8x16, tức là mỗi char được biểu thị bằng 16

byte, mỗi byte chỉ định các pixel của một dòng quét của char. Để hiển thị một ký tự, hãy sử dụng giá trị mã ASCII của ký tự đó (nhân với
Machine Translated by Google

2.8 Trình điều khiển thiết bị 37

bằng 16) làm phần bù để truy cập các byte của nó trong bitmap. Sau đó quét các bit trong mỗi byte. Với mỗi bit 0, ghi BGR = 0x000000

(màu đen) vào pixel tương ứng. Với mỗi 1 bit, hãy ghi BRG = 0x111111 (màu trắng) vào pixel. Thay vì đen trắng, mỗi ký tự cũng có thể

được hiển thị bằng màu bằng cách ghi các giá trị RGB khác nhau vào các pixel.

Giống như tệp hình ảnh, tệp phông chữ thô có thể được đưa vào dưới dạng phần dữ liệu nhị phân trong hình ảnh thực thi. Một cách

khác là chuyển đổi bitmap thành bản đồ char trước tiên. Ví dụ: chương trình sau đây, bitmap2charmap.c, chuyển đổi bitmap phông chữ

thành bản đồ char.

/**** bitmap2charmap.c: chạy dưới dạng a.out font.bin > font ****/

#include <stdio.h>

main(int argc, char *argv[ ])

int tôi, n; u8 buf[16];

TỆP *fp = fopen(argv[1], "r"); // tập tin fopen cho ĐỌC

while((n = fread(buf, 1, 16, fp)){ // đọc 16 byte

for (i=0; i<n; i++) // ghi mỗi byte dưới dạng 2 hex

printf("0x%2x ", buf[i]);

printf("\n");

Hình 2.7 Trình diễn hình ảnh hiển thị trên LCD
Machine Translated by Google

38 2 Kiến trúc và lập trình ARM

Không giống như các tệp bitmap thô, trước tiên phải được chuyển đổi thành tệp đối tượng, tệp bản đồ char có kích thước lớn hơn nhưng

chúng có thể được đưa trực tiếp vào mã C.

2.8.4.6 Chương trình trình điều khiển màn hình LCD màu

Chương trình ví dụ C2.7 trình bày trình điều khiển màn hình LCD để hiển thị văn bản. Chương trình bao gồm các thành phần sau.

(1). Tệp ts.s: Tệp ts.s giống như trong chương trình ví dụ C2.6. (2). tệp vid.c:

Tệp vid.c triển khai trình điều khiển cho màn hình LCD ARM PL110 [ARM PL110, 2016]. Trên bảng Versatilepb, địa chỉ cơ sở của LCD màu là

0x10120000. Các thanh ghi khác là phần bù (u32) từ địa chỉ cơ sở.

/********* tệp vid.c : Trình điều khiển LCD ********/

00 thời gian0

04 thời gian1

08 thời gian2

thời gian 0C3

10 UpperPanelframeBaseAddressRegister // sử dụng bảng phía trên

14 lowPanelFrameBaseAddressRegister // KHÔNG sử dụng bảng phía dưới

18 controlRegister // LƯU Ý: PL110 CR được mô phỏng QEMU ở mức 0x18

**************************************************** *******/

#xác định ĐỎ 0

#xác định XANH 1

#xác định XANH 2

#xác định TRẮNG 3

char bên ngoài _binary_font_start; màu int;

con trỏ u8;

int dễ bay hơi *fb;

hàng int, col, cuộn_row; phông chữ char *

không dấu;

int CHIỀU RỘNG = 640; // độ rộng dòng quét, mặc định là 640

int fbuf_init()

int tôi;

fb = (int *)0x200000; phông chữ = // bộ đệm khung ở mức 2MB-4MB

&_binary_font_start; // bitmap phông chữ

/********* dành cho chế độ VGA 640x480 *******************/

*(dễ bay hơi unsigned int *)(0x1000001c) = 0x2C77; *(dễ bay hơi unsigned int *)

(0x10120000) = 0x3F1F3F9C; *(dễ bay hơi unsigned int *)(0x10120004) = 0x090B61DF; *(dễ

bay hơi unsigned int *)(0x10120008) = 0x067F1800; *(dễ bay hơi unsigned int *)(0x10120010)

= 0x200000; // ở mức 2MB *(dễ bay hơi unsigned int *)(0x10120018) = 0x82B;

/************ dành cho chế độ SVGA 800X600 *******************

*(dễ bay hơi unsigned int *)(0x1000001c) = 0x2CAC; // 800x600

*(dễ bay hơi unsigned int *)(0x10120000) = 0x1313A4C4;

*(dễ bay hơi unsigned int *)(0x10120004) = 0x0505F6F7;

*(dễ bay hơi unsigned int *)(0x10120008) = 0x071F1800;

*(dễ bay hơi unsigned int *)(0x10120010) = 0x200000;

*(dễ bay hơi unsigned int *)(0x10120018) = 0x82B;

**********/

con trỏ = 127; // con trỏ = hàng 127 trong phông chữ bitmap

}
Machine Translated by Google

2.8 Trình điều khiển thiết bị 39

int clrpix(int x, int y) // xóa pixel tại (x,y)

int pix = y*640 + x; fb[pix]

= 0x00000000;

int setpix(int x, int y) // đặt pixel tại (x,y)

int pix = y*640 + x;

nếu (màu==ĐỎ)

fb[pix] = 0x000000FF;

nếu (màu==XANH)

fb[pix] = 0x00FF0000;

nếu (màu==XANH)

fb[pix] = 0x0000FF00;

int dchar(unsigned char c, int x, int y) // hiển thị char tại (x,y)

int r, bit;

ký tự không dấu * cadress, byte;

cadress = phông chữ + c*16;

cho (r=0; r<16; r++){

byte = *(địa chỉ + r);

cho (bit=0; bit<8; bit++){

nếu (byte & (1<<bit))

setpix(x+bit, y+r);

int undchar(unsigned char c, int x, int y) // xóa char tại (x,y)

hàng int, bit;

ký tự không dấu * cadress, byte;

cadress = phông chữ + c*16;

cho (hàng=0; hàng<16; hàng++){

byte = *(địa chỉ + hàng);

cho (bit=0; bit<8; bit++){

nếu (byte & (1<<bit))

clrpix(x+bit, y+hàng);

int cuộn() // vuốt LÊN một dòng (một cách khó khăn)

int tôi;

cho (i=64*640; i<640*480; i++){

fb[i] = fb[i + 640*16];

int kpchar(char c, int ro, int co) // in char tại (row, col)

int x, y;

x = co*8;

y = ro*16;

dchar(c, x, y);

}
Machine Translated by Google

40 2 Kiến trúc và lập trình ARM

int unkpchar(char c, int ro, int co) // xóa char tại (row, col)

int x, y;

x = co*8;

y = ro*16;

undchar(c, x, y);

int erachar() // xóa char tại (row,col)

int r, bit, x, y; ký tự

không dấu * cadress, byte;

x = col*8;

y = hàng*16;

cho (r=0; r<16; r++){

cho (bit=0; bit<8; bit++){

clrpix(x+bit, y+r);

int clrcursor() // xóa con trỏ tại (row, col)

unkpchar(127, row, col);

int putcursor(unsigned char c) // đặt con trỏ tại (row, col)

kpchar(c, row, col);

int kputc(char c) // in char tại vị trí con trỏ

clrcursor();

nếu (c=='\r'){ // trả về khóa

col=0;

con trỏ(con trỏ);

trở lại;

nếu (c=='\n'){ // khóa dòng mới

hàng++;

nếu (hàng>=25){

hàng = 24;

cuộn();

con trỏ(con trỏ);

trở lại;

nếu (c=='\b'){ // Phím xóa

nếu (col>0){

col--;

tẩy xóa();

con trỏ(con trỏ);

trở lại;

// c là trường hợp char thông thường

kpchar(c, row, col);


Machine Translated by Google

2.8 Trình điều khiển thiết bị 41

col++;

nếu (col>=80){

col = 0;

hàng++;

nếu (hàng >= 25){

hàng = 24;

cuộn();

con trỏ(con trỏ);

// Phần sau triển khai kprintf() để in có định dạng int kprints(char *s)

trong khi(*s){

kputc(*s);

s++;

int krpx(int x)

ký tự c;

nếu (x){

c = tab[x % 16];

krpx(x / 16);

kputc(c);

int kprintx(int x)

kputc('0'); kputc('x');

nếu (x==0)

kputc('0');

khác

krpx(x);

kputc(' ');

int krpu(int x)

ký tự c;

nếu (x){

c = tab[x % 10];

krpu(x / 10);

kputc(c);

int kprintu(int x)

nếu (x==0)

kputc('0');

khác

krpu(x);

kputc(' ');

}
Machine Translated by Google

42 2 Kiến trúc và lập trình ARM

int kprinti(int x)

nếu (x<0){

kputc('-');

x = -x;

kprintu(x);

int kprintf(char *fmt,…)

int *ip;

char *cp; cp

= fmt;

ip = (int *)&fmt + 1;

while(*cp){ if

(*cp != '%'){

kputc(*cp); if

(*cp=='\n') kputc('\r');

cp++;

Tiếp tục;

cp++;

chuyển đổi(*cp){

trường hợp 'c': kputc((char)*ip); phá vỡ;

case 's': kprints((char *)*ip); phá vỡ;

trường hợp 'd': kprinti(*ip); phá vỡ;

trường hợp 'u': kprintu(*ip); phá vỡ;

trường hợp 'x': kprintx(*ip); phá vỡ;

cp++; ip++;

2.8.4.7 Giải thích về Mã trình điều khiển LCD Màn hình LCD

có thể được coi là một hộp hình chữ nhật có kích thước 480x640 pixel. Mỗi pixel có tọa độ (x, y) trên màn hình. Tương ứng, bộ đệm khung,

u32 fbuf[ ], là vùng bộ nhớ chứa 480*640 u32 số nguyên, trong đó 24 bit thấp của mỗi số nguyên biểu thị giá trị BGR của pixel. Địa chỉ

tuyến tính hoặc chỉ mục của một pixel trong fbuf[ ] tại tọa độ (x,y) = (cột, hàng) được đưa ra bởi thuật toán của Mailman (Chương 2, Wang

2015).

chỉ số pixel ¼ x þ y640;

Các chức năng hiển thị cơ bản của trình điều khiển LCD là

(1). setpix(x,y): đặt pixel ở giá trị (x, y) thành BGR (theo biến màu toàn cục). (2).

clrpix(x,y): xóa pixel tại (x, y) bằng cách đặt BGR thành màu nền (đen). (3). dchar(char, x, y):

hiển thị char tại tọa độ (x, y). Mỗi char được biểu thị bằng một bitmap 8x16. Đối với một giá trị char nhất định (0 đến 127), dchar() tìm

nạp 16 byte của char từ bitmap. Đối với mỗi bit trong một byte, nó gọi clrpix(x+bitNum, y +byteNum) để xóa pixel trước. Việc này sẽ xóa

char cũ, nếu có, tại (x, y). Nếu không, nó sẽ hiển thị các mẫu bit tổng hợp của các ký tự, khiến nó không thể đọc được. Sau đó, nó gọi

setpix(x+bitNum, y+byteNum) để đặt pixel nếu bit là 1. (4). eraschar(): xóa char tại (x, y). Đối với thiết bị hiển thị được ánh xạ bộ

nhớ, khi một char được ghi vào bộ đệm khung, nó sẽ vẫn ở đó (và do đó được hiển thị trên màn hình) cho đến khi bị xóa. Đối với các ký tự

thông thường, dchar() sẽ tự động xóa


Machine Translated by Google

2.8 Trình điều khiển thiết bị 43

char gốc. Đối với các ký tự đặc biệt như con trỏ, cần xóa nó trước khi di chuyển sang vị trí khác. Điều này được thực hiện
bằng thao tác eraschar(). (5).
kputc(char c): hiển thị ký tự tại dòng (hàng, col) hiện tại và di chuyển con trỏ, điều này có thể khiến màn hình bị cuộn
lên. (6). Scroll(): cuộn màn hình lên hoặc xuống 1 dòng.
(7). Con trỏ: Khi hiển thị văn bản, con trỏ cho phép người dùng xem ký tự tiếp theo sẽ được hiển thị ở đâu. Bộ điều khiển
ARM LCD không có bộ tạo con trỏ. Trong trình điều khiển LCD, con trỏ được mô phỏng bằng một ký tự đặc biệt (mã ASCII 127)
trong đó tất cả các pixel là 1, xác định một hộp hình chữ nhật đặc làm con trỏ. Con trỏ có thể nhấp nháy nếu nó được bật/tắt
định kỳ, ví dụ cứ sau 0,5 giây, việc này cần có bộ hẹn giờ. Chúng ta sẽ hiển thị con trỏ nhấp nháy ở phần sau của Chương. 3
khi chúng tôi triển khai bộ hẹn giờ với các ngắt hẹn giờ. Hàm putcursor() vẽ con trỏ tại vị trí (hàng, cột) hiện tại trên
màn hình và hàm eracursor() sẽ xóa con trỏ khỏi vị trí hiện tại của nó.
(số 8). Hàm printf()

Đối với bất kỳ thiết bị đầu ra nào hỗ trợ thao tác in char cơ bản, chúng ta có thể triển khai hàm printf() để in được
định dạng. Phần sau đây cho thấy cách phát triển hàm printf() chung như vậy, có thể được sử dụng cho cả UART và màn hình
LCD. Đầu tiên, chúng ta triển khai hàm printu() để in các số nguyên không dấu.

char *ctable = "0123456789ABCDEF";

int CƠ SỞ = 10; // cho số thập phân

int rpu(u32 x)

ký tự c; // biến cục bộ

nếu (x){

c = ctable[x % BASE];

rpu(x / BASE);

đặt (c);

int printu(u32 x)

(x==0)? putc('0') : rpu(x);

putc(' ');

Hàm rpu(x) tạo ra các chữ số của x % 10 trong ASCII theo cách đệ quy và in chúng trên đường dẫn trả về. Ví dụ: nếu x=123,
các chữ số được tạo theo thứ tự '3', '2', '1', được in thành '1', '2', '3' nếu cần. Với printu(), việc viết hàm printd() để
in các số nguyên có dấu trở nên đơn giản. Bằng cách đặt BASE thành 16, chúng ta có thể in ở dạng hex. Giả sử rằng chúng ta
đã triển khai print(), printd(), printu() và printx(). Sau đó chúng ta có thể viết một

int printf(char *fmt,…) // LƯU Ý dấu 3 chấm ở tiêu đề hàm

hàm để in được định dạng, trong đó fmt là một chuỗi định dạng chứa các ký hiệu chuyển đổi %c, %s, %u, %d, %x.

int printf(char *fmt, …) // hầu hết các trình biên dịch C đều yêu cầu dấu 3 chấm

char *cp = fmt; int *ip // cp trỏ tới chuỗi fmt // ip trỏ tới mục đầu

= (int *)&fmt +1; tiên trong ngăn xếp

trong khi (*cp){ // quét chuỗi định dạng

nếu (*cp != '%'){ // nhổ ra các ký tự thông thường

putc(*cp);

nếu (*cp=='\n') // cho mỗi '\n'

putc('\r'); // in một '\r'

cp++;

Tiếp tục;

cp++; // cp trỏ vào ký hiệu chuyển đổi


Machine Translated by Google

44 2 Kiến trúc và lập trình ARM

switch(*cp){ // in mục bằng ký hiệu %FORMAT

trường hợp 'c' : putc((char )*ip); phá vỡ; case 's' :

print((char *)*ip); phá vỡ; trường hợp 'u' : printu((u32 )*ip);

phá vỡ; trường hợp 'd' : printd((int )*ip); phá vỡ; trường

hợp 'x' : printx((u32 )*ip); phá vỡ;

cp++; ip++; // con trỏ tiến

(5). Tệp tc: Tệp tc chứa hàm main() và hàm show_bmp(). Đầu tiên nó khởi tạo cả trình điều khiển UART và LCD. Trình điều khiển
UART được sử dụng cho I/O từ cổng nối tiếp. Với mục đích trình diễn, nó hiển thị kết quả đầu ra cho cả cổng nối tiếp và màn
hình LCD. Khi chương trình khởi động, nó sẽ hiển thị một hình ảnh logo nhỏ ở đầu màn hình. Giới hạn cuộn trên được đặt thành
một dòng bên dưới hình ảnh logo để logo sẽ vẫn còn trên màn hình khi màn hình được cuộn lên trên.

/*************** tệp tc của C2.7 *************/

#include "định nghĩa.h"

#include "uart.c"

#include "vid.c"

char bên ngoài _binary_panda1_start;

int show_bmp(char *p, int startRow, int startCol){// CÙNG như trước}

int chính()

dòng char[64];

fbuf_init(); char

*p = &_binary_panda1_start; show_bmp(p, 0, 0); //

hiển thị logo

uart_init();

UART *up = upp[0];

trong khi(1){

màu = XANH;

kprintf("nhập một dòng từ cổng UART: ");

uprintf("nhập dòng từ UART: ");

ugets(lên, dòng);

uprintf("line=%s\n", line);

màu = ĐỎ;

kprintf("line=%s\n", line);

(6). Tệp t.ld: Tập lệnh liên kết bao gồm các tệp đối tượng của phông chữ và hình ảnh dưới dạng các phần dữ liệu nhị phân,
tương tự như chương
trình ví dụ C2.6. (7). mk và chạy file script: Tương tự như C2.6.

2.8.4.8 Trình diễn chương trình điều khiển LCD Hình 2.8
cho thấy kết quả đầu ra mẫu khi chạy chương trình ví dụ C2.7. Nó sử dụng trình điều khiển LCD để hiển thị cả hình ảnh và văn
bản trên màn hình LCD. Ngoài ra, nó còn sử dụng trình điều khiển UART để thực hiện I/O từ cổng nối tiếp.
Machine Translated by Google

2.9 Tóm tắt 45

Hình 2.8 Trình diễn văn bản hiển thị trên LCD

2.9 Tóm tắt

Chương này đề cập đến kiến trúc ARM, hướng dẫn ARM, lập trình trong tập hợp ARM và phát triển các chương trình để thực thi trên máy

ảo ARM. Chúng bao gồm các chế độ bộ xử lý ARM, các thanh ghi ngân hàng ở các chế độ khác nhau, các hướng dẫn và lập trình cơ bản trong

tập hợp ARM. Vì hầu hết phần mềm dành cho hệ thống nhúng được phát triển bằng cách biên dịch chéo nên nó giới thiệu chuỗi công cụ ARM,

cho phép chúng tôi phát triển các chương trình để thực thi trên các máy ảo ARM mô phỏng. Chúng tôi chọn Linux Ubuntu (14.04/15.0) làm

nền tảng phát triển chương trình vì nó hỗ trợ các chuỗi công cụ ARM hoàn chỉnh nhất. Trong số các máy ảo ARM, chúng tôi chọn bo mạch

ARM Versatilepb mô phỏng trong QEMU vì nó hỗ trợ nhiều thiết bị ngoại vi thường được sử dụng trong các hệ thống dựa trên ARM thực. Sau

đó, nó cho thấy cách sử dụng chuỗi công cụ ARM để phát triển các chương trình thực thi trên máy ảo ARM Versatilepb bằng một loạt các

ví dụ về lập trình. Nó giải thích quy ước gọi hàm trong C và chỉ ra cách giao tiếp mã hợp ngữ với các chương trình C.

Sau đó, nó phát triển trình điều khiển UART đơn giản cho I/O trên các cổng nối tiếp và trình điều khiển LCD để hiển thị cả hình ảnh đồ

họa và văn bản. Nó cũng cho thấy sự phát triển của hàm printf() chung để in được định dạng tới các thiết bị đầu ra hỗ trợ thao tác

print char cơ bản.

Danh sách các chương trình mẫu

C2.1: Lập trình hợp ngữ ARM C2.2: Tổng

mảng số nguyên trong hợp ngữ C2.3: Gọi hàm

hợp ngữ từ C C2.4: Gọi hàm C từ hợp ngữ

C2.5: Trình điều khiển UART

C2.6: Driver LCD hiển thị hình ảnh C2.7:

Driver LCD hiển thị văn bản

Các vấn đề

1. Chương trình ví dụ C2.2 chứa 3 lệnh được tô sáng // r2--


phụ r2, r2, #1

cmp r2, #0 // nếu (r2 != 0)

vòng lặp tốt // vòng lặp goto;

(1). Nếu bạn xóa lệnh cmp thì chương trình sẽ không hoạt động. Giải thích vì sao?

(2). Tuy nhiên thay 3 dòng bằng sub r2, r2,


#1

vòng lặp tốt

sẽ có tác dụng. Giải thích vì sao?

2. Trong mã hợp ngữ của chương trình C2.4, a, b, c liền kề nhau. Sửa đổi mã lắp ráp bằng cách cho R2 trỏ vào đầu tiên
từ a. Sau đó
Machine Translated by Google

46 2 Kiến trúc và lập trình ARM

(1). Truy cập a, b, c bằng cách sử dụng R2 làm thanh ghi cơ sở có offset.

(b). Truy cập a, b, c bằng cách sử dụng R2 làm thanh ghi cơ sở với địa chỉ được lập chỉ mục sau.

3. Trong chương trình ví dụ C2.4, thay vì xác định a, b, c trong mã hợp ngữ, hãy xác định chúng dưới dạng toàn cục được khởi tạo trong t.
tập tin c:

int a ¼ 1; b ¼ 2; c ¼ 0;

Khai báo chúng dưới dạng ký hiệu chung trong tệp ts.s. Biên dịch và chạy lại chương trình.

4. Lập trình GPIO: Giả sử BASE là địa chỉ cơ sở của GPIO và các thanh ghi GPIO nằm ở các địa chỉ offset IODIR, IOSET, IOCLR, IOPIN. Phần sau đây

cho thấy cách xác định chúng dưới dạng hằng số trong mã hợp ngữ ARM và C.

.set CƠ SỞ, 0x101E4000 // #define CƠ SỞ 0x101E4000

.set IODIR, 0x000 // #define IODIR 0x000

.set IOSET, 0x004 // #define IOSET 0x004

.set IOCLR, 0x008 // #define IOCLR 0x008

.set IOPIN, 0x00C // #define IOPIN 0x00C

Viết chương trình điều khiển GPIO bằng cả hợp ngữ và C để thực hiện các tác vụ sau.

(1). Lập trình các chân GPIO như được chỉ định trong Mục. 2.8.1.

(2). Xác định trạng thái của các chân GPIO.

(3). Sửa đổi chương trình điều khiển để làm cho đèn LED nhấp nháy khi công tắc đầu vào đóng.

5. Chương trình ví dụ C2.6 giả định rằng mọi kích thước hình ảnh là h<=640 và chiều rộng<=480 pixel. Sửa đổi chương trình để xử lý ảnh BMP có kích

thước lớn hơn bằng (1). Cắt xén: hiển thị tối

đa 480x640 pixel.

(2). Thu nhỏ: giảm kích thước hình ảnh theo hệ số, ví dụ 2 nhưng vẫn giữ nguyên tỷ lệ khung hình 4:3.

6. Sửa đổi chương trình ví dụ C2.6 để hiển thị một chuỗi các hình ảnh hơi khác nhau để làm hoạt ảnh.

7. Nhiều tệp hình ảnh, ví dụ: hình ảnh JPG, được nén, trước tiên phải được giải nén. Sửa đổi chương trình ví dụ C2.6 để hiển thị hình ảnh (được nén)

ở định dạng khác, ví dụ: tệp hình ảnh JPG.

8. Trong chương trình điều khiển màn hình LCD C2.7, xác định tab_size = 8. Mỗi phím tab (\t) mở rộng thành 8 khoảng trắng. Sửa đổi trình điều khiển

LCD để hỗ trợ các phím tab. Kiểm tra trình điều khiển LCD đã sửa đổi bằng cách đưa \t vào lệnh gọi printf().

9. Trong trình điều khiển LCD của chương trình C2.7, việc cuộn lên một dòng được thực hiện bằng cách sao chép toàn bộ bộ đệm khung. Nghĩ ra cách

hiệu quả hơn để thực hiện thao tác cuộn. GỢI Ý: Bộ nhớ hiển thị có thể được coi là bộ đệm tròn. Để cuộn lên một dòng, chỉ cần tăng con trỏ bộ

đệm khung theo kích thước dòng.

10. Sửa đổi chương trình ví dụ C2.7 để hiển thị văn bản với các phông chữ khác nhau.

11. Sửa đổi chương trình ví dụ C2.8 để thực hiện nhảy xa bằng cách sử dụng mã của Môn phái. 2.7.3.3. Sử dụng UART0 để có được người dùng

đầu vào nhưng hiển thị đầu ra cho màn hình LCD. Xác minh rằng nhảy xa hoạt động.

12. Trong trình điều khiển LCD, hàm printf() chung được định nghĩa là

int printf(char *fmt, …); // lưu ý dấu 3 chấm

Khi triển khai printf(), nó giả định rằng tất cả các tham số đều nằm liền kề trên ngăn xếp, do đó chúng có thể được truy cập tuyến tính. Điều

này dường như không phù hợp với quy ước gọi của ARM C, quy ước này chuyển 4 tham số đầu tiên trong r0–r3 và các tham số bổ sung, nếu có, trên ngăn

xếp. Biên dịch mã hàm printf() để tạo tệp mã hợp ngữ. Kiểm tra mã hợp ngữ để xác minh rằng các tham số thực sự nằm liền kề trên ngăn xếp.

Người giới thiệu

Kiến trúc ARM: http://www.arm.products/processors/instruction-set-architectures, Trung tâm thông tin ARM, 2016 ARM
Cortex-A8: "Sổ tay tham khảo kỹ thuật ARM Cortex-A8", Trung tâm thông tin ARM, 2010 ARM Cortex A9
MPcore: "Sổ tay tham khảo kỹ thuật Cortex A9 MPcore", Trung tâm thông tin ARM, 2016 ARM926EJ-ST: "ARM926EJ
-S Sổ tay tham khảo kỹ thuật", Trung tâm thông tin ARM, 2008 ARM926EJ-ST: "Hướng dẫn sử dụng
ván chân tường ứng dụng đa năng cho ARM926EJ-S", Trung tâm thông tin ARM, 2010 ARM PL011: "Sổ tay tham khảo kỹ
thuật PrimeCell UART (PL011)", Trung tâm thông tin ARM , 2016 Bộ điều khiển LCD màu ARM PrimeCell PL110:
"Bảng nền ứng dụng đa năng ARM cho ARM926EF-S", Trung tâm thông tin ARM, 2016 Lập trình ARM: "Lập trình ngôn ngữ hội ARM", http://www.peter-
cockerell.net/aalp/html /frames.html Chuỗi công cụ ARM: http://gnutoolchains.com/arm-eabi, Trình giả lập QEMU 2016:
"Tài liệu người dùng Trình giả lập QEMU", http://
wiki.qemu.org/download/qemu-doc.htm, 2010
Machine Translated by Google

Xử lý ngắt và ngoại lệ
3

Trong mọi hệ thống máy tính, CPU được thiết kế để liên tục thực hiện các lệnh. Ngoại lệ là một sự kiện được CPU nhận ra, làm chuyển hướng

CPU khỏi các hoạt động thực thi thông thường của nó để thực hiện một việc khác, gọi là xử lý ngoại lệ. Ngắt là một sự kiện bên ngoài làm

chuyển hướng CPU khỏi các hoạt động thực thi thông thường của nó để thực hiện xử lý ngắt. Theo nghĩa rộng hơn, ngắt là những loại ngoại lệ

đặc biệt. Sự khác biệt duy nhất giữa các ngoại lệ và ngắt là cái trước có thể bắt nguồn từ chính CPU nhưng cái sau luôn bắt nguồn từ các

nguồn bên ngoài. Ngắt là điều cần thiết cho mọi hệ thống máy tính. Nếu không có ngắt, hệ thống máy tính sẽ không thể phản hồi các sự kiện

bên ngoài, chẳng hạn như đầu vào của người dùng, sự kiện hẹn giờ và yêu cầu dịch vụ từ các thiết bị I/O, v.v. Hầu hết các hệ thống nhúng

được thiết kế để phản hồi các sự kiện bên ngoài và xử lý các sự kiện đó khi chúng xảy ra. Vì lý do này, ngắt và xử lý ngắt đặc biệt quan

trọng đối với các hệ thống nhúng. Trong chương này, chúng ta sẽ thảo luận về các trường hợp ngoại lệ, ngắt và xử lý ngắt trong các hệ

thống dựa trên ARM.

Trong chương. 2, chúng tôi đã phát triển các trình điều khiển đơn giản cho màn hình LCD và UART. LCD là một thiết bị ánh xạ bộ nhớ,

không sử dụng các ngắt. UART hỗ trợ các ngắt nhưng trình điều khiển UART đơn giản sử dụng tính năng thăm dò chứ không phải ngắt cho I/O.

Nhược điểm chính của I/O bằng cách thăm dò là nó không sử dụng CPU một cách hiệu quả. Trong khi CPU đang thực hiện I/O bằng cách thăm dò,

nó liên tục bận và không thể làm gì khác. Trong hệ thống máy tính, I/O phải được thực hiện bằng các ngắt bất cứ khi nào có thể. Trong

Chương này, chúng tôi sẽ trình bày cách áp dụng nguyên tắc xử lý ngắt để thiết kế và triển khai các trình điều khiển thiết bị điều khiển ngắt.

3.1 Ngoại lệ ARM

3.1.1 Chế độ bộ xử lý ARM

Bộ xử lý ARM có bảy chế độ hoạt động khác nhau, được xác định bởi 5 bit chế độ [4:0] trong thanh ghi trạng thái bộ xử lý hiện tại (CSPR)

(Kiến trúc ARM 2016; Kiến trúc bộ xử lý ARM 2016). Bảng 3.1 cho thấy bảy chế độ của bộ xử lý ARM.

Trong số bảy chế độ, chỉ có chế độ Người dùng là không có đặc quyền. Tất cả các chế độ khác đều có đặc quyền. Một đặc điểm khác thường

của kiến trúc ARM là khi CPU ở chế độ đặc quyền, nó có thể thay đổi sang bất kỳ chế độ nào khác bằng cách thay đổi các bit chế độ trong

CPSR. Khi CPU ở chế độ Người dùng không có đặc quyền, cách duy nhất để thay đổi sang chế độ đặc quyền là thông qua các ngoại lệ, ngắt hoặc

lệnh SWI. Mỗi chế độ đặc quyền có các thanh ghi riêng, ngoại trừ chế độ Hệ thống, chia sẻ cùng một bộ thanh ghi với chế độ Người dùng, ví

dụ: chúng có cùng một con trỏ ngăn xếp (R13) và cùng một thanh ghi liên kết (R14).

3.1.2 Ngoại lệ ARM

Ngoại lệ là một sự kiện được bộ xử lý nhận ra, làm chuyển hướng bộ xử lý khỏi các hoạt động thực thi thông thường của nó để xử lý ngoại lệ

đó. Theo nghĩa chung, ngắt cũng là trường hợp ngoại lệ. Trong ARM, có bảy loại ngoại lệ (không bao gồm loại Dự trữ) (ARM CPU Architecture

2016), được trình bày trong Bảng 3.2.

Khi xảy ra ngoại lệ, bộ xử lý ARM sẽ thực hiện như sau.

© Springer International Publishing AG 2017 47


KC Wang, Hệ điều hành nhúng và thời gian thực, DOI
10.1007/978-3-319-51517-5_3
Machine Translated by Google

48 3 Xử lý ngắt và ngoại lệ

Bảng 3.1 Các chế độ của bộ xử lý ARM

Bảng 3.2 Ngoại lệ ARM

(1). Sao chép CPSR vào SPSR cho chế độ xử lý ngoại lệ.
(2). Thay đổi các bit chế độ CPSR sang chế độ thích hợp, ánh xạ vào các thanh ghi được xếp dãy và vô hiệu hóa các ngắt. IRQ luôn
bị tắt, FIQ chỉ bị tắt khi FIQ xảy ra và được đặt lại.
(3). Đặt thanh ghi LR_mode thành địa chỉ trả về.
(4). Đặt Bộ đếm chương trình (PC) thành địa chỉ vectơ của ngoại lệ. Điều này buộc một nhánh phải xử lý ngoại lệ thích hợp.

Nếu nhiều ngoại lệ xảy ra đồng thời, chúng sẽ được xử lý theo thứ tự ưu tiên như trong Bảng 3.2.
Sau đây là danh sách các sự kiện ngoại lệ và cách chúng được bộ xử lý ARM xử lý.

• Sự kiện Reset xảy ra khi bộ xử lý được cấp nguồn. Đây là sự kiện có mức độ ưu tiên cao nhất và sẽ được thực hiện bất cứ khi nào
nó được báo hiệu. Khi vào bộ xử lý đặt lại, CPSR ở chế độ SVC và cả hai ngắt IRQ và FIQ đều bị che đi.
Nhiệm vụ của trình xử lý đặt lại là khởi tạo hệ thống. Điều này bao gồm việc thiết lập các chế độ khác nhau, định cấu hình bộ
nhớ và khởi tạo trình điều khiển thiết bị, v.v.
• Sự kiện Hủy bỏ dữ liệu (DAB) xảy ra khi bộ điều khiển bộ nhớ hoặc MMU chỉ ra rằng địa chỉ bộ nhớ không hợp lệ đã được truy cập.
Ví dụ: nếu không có bộ nhớ vật lý cho một địa chỉ hoặc bộ xử lý không có quyền truy cập vào vùng bộ nhớ thì ngoại lệ hủy bỏ
dữ liệu sẽ được đưa ra. Việc hủy bỏ dữ liệu có mức độ ưu tiên cao thứ hai. Điều này có nghĩa là bộ xử lý sẽ xử lý các ngoại
lệ hủy bỏ dữ liệu trước khi xử lý bất kỳ ngắt nào.
• Ngắt FIQ xảy ra khi một thiết bị ngoại vi bên ngoài đặt chân FIQ thành nFIQ. Ngắt FIQ là ngắt có mức ưu tiên cao nhất. Khi vào
bộ xử lý FIQ, cả ngắt IRQ và FIQ đều bị vô hiệu hóa. Điều này có nghĩa là trong khi xử lý ngắt FIQ, không có ngắt nào khác có
thể xảy ra trừ khi chúng được phần mềm kích hoạt rõ ràng. Trong các hệ thống dựa trên ARM, FIQ thường được sử dụng để xử lý
các ngắt từ một nguồn ngắt duy nhất cực kỳ khẩn cấp. Cho phép nhiều nguồn FIQ sẽ làm mất đi mục đích của FIQ.
Machine Translated by Google

3.1 Ngoại lệ ARM 49

• Ngắt IRQ xảy ra khi một thiết bị ngoại vi bên ngoài đặt chân IRQ. Ngắt IRQ là ngắt có mức ưu tiên cao thứ hai. Bộ xử lý sẽ xử lý ngắt IRQ

nếu không có ngắt FIQ hoặc ngoại lệ hủy bỏ dữ liệu. Khi truy cập vào bộ xử lý IRQ, các ngắt IRQ sẽ bị che đi. Bit I của CSPR sẽ được giữ

nguyên cho đến khi nguồn ngắt hiện tại bị xóa.

• Sự kiện Hủy bỏ tìm nạp trước (PFA) xảy ra khi nỗ lực tải lệnh dẫn đến lỗi bộ nhớ. Ngoại lệ xảy ra nếu lệnh đạt đến giai đoạn thực thi của

đường ống và không có ngoại lệ/ngắt cao hơn nào được đưa ra. Khi truy cập vào trình xử lý PFA, IRQ bị vô hiệu hóa nhưng FIQ vẫn được bật,

do đó mọi ngắt FIQ sẽ được thực hiện ngay lập tức trong khi xử lý ngoại lệ PFA.

• Ngắt SWI xảy ra khi lệnh SWI đã được tìm nạp và giải mã thành công và không có ngoại lệ/ngắt có mức ưu tiên cao hơn nào khác được đưa ra. Khi

vào trình xử lý SWI, CPSR được đặt ở chế độ SVC. Ngắt SWI thường được sử dụng để thực hiện các cuộc gọi hệ thống từ chế độ Người dùng đến

nhân hệ điều hành ở chế độ SVC.


• Sự kiện Lệnh không xác định xảy ra khi một lệnh không có trong tập lệnh ARM/Thumb đã được tìm nạp và

được giải mã thành công và không có ngoại lệ/ngắt nào khác được gắn cờ. Trong hệ thống dựa trên ARM có bộ đồng xử lý, bộ xử lý ARM sẽ thăm

dò các bộ đồng xử lý để xem liệu chúng có thể xử lý lệnh hay không. Nếu không có bộ đồng xử lý nào yêu cầu lệnh thì ngoại lệ lệnh không xác

định sẽ xuất hiện. SWI và Lệnh không xác định có cùng mức độ ưu tiên vì chúng không thể xảy ra cùng lúc. Nói cách khác, lệnh đang được thực

thi không thể vừa là SWI vừa là lệnh không xác định. Trong thực tế, các hướng dẫn không xác định có thể được sử dụng để cung cấp các điểm

dừng phần mềm khi gỡ lỗi các chương trình ARM.

3.1.3 Bảng vectơ ngoại lệ

Bộ xử lý ARM sử dụng bảng vectơ để xử lý các ngoại lệ và ngắt. Bảng 3.3 thể hiện bảng vector ARM
nội dung.

Bảng vectơ xác định điểm vào của các trình xử lý ngoại lệ và ngắt. Bảng vectơ được đặt tại địa chỉ vật lý 0. Nhiều hệ thống dựa trên ARM

bắt đầu thực thi từ bộ nhớ flash hoặc ROM, có thể được ánh xạ lại thành 0xFFFF0000 trong khi khởi động. Nếu bảng vectơ ban đầu không ở mức 0

trong SRAM, thì nó phải được sao chép sang SRAM trước khi ánh xạ lại thành 0x00000000. Điều này thường được thực hiện trong quá trình khởi tạo

hệ thống.

3.1.4 Trình xử lý ngoại lệ

Mỗi mục trong bảng vectơ chứa một lệnh ARM (B, BL hoặc LDR), khiến bộ xử lý tải PC với địa chỉ mục nhập của quy trình xử lý ngoại lệ. Đoạn mã

sau đây hiển thị nội dung bảng vectơ điển hình, trong đó mỗi lệnh LDR tải PC với địa chỉ mục nhập của hàm xử lý ngoại lệ. Đối với mục nhập

vectơ dành riêng (0x14), vòng lặp nhánh tới chính nó là đủ vì ngoại lệ không bao giờ có thể xảy ra.

Bảng 3.3 Bảng Vector ARM


Machine Translated by Google

50 3 Xử lý ngắt và ngoại lệ

Máy tính LDR 0x00, reset_handler_addr

Máy tính LDR 0x04, undef_handler_addr

Máy tính LDR 0x08, swi_handler_addr

PC LDR 0x0C, prefetch_abort_handler_addr


Máy tính LDR 0x10, data_abort_handler_addr
0x14B .

Máy tính LDR 0x18, irq_handler_addr

Máy tính LDR 0x1C, fiq_handler_addr


đặt lại_handler_addr: .word reset_handler

undef_handler_addr: .word undef_handler

swi_handler_addr: .word swi_handler

prefetch_abort_handler_addr: .word prefetch_abort_handler


dữ liệu_abort_handler_addr: .word data_abort_handler

irq_handler_addr: .word irq_handler .word

fiq_handler_addr: fiq_handler

Khi một vectơ sử dụng LDR để tải địa chỉ mục nhập của trình xử lý, trình xử lý sẽ được gọi gián tiếp. LDR phải tải một
hằng số nằm trong phạm vi 4 kB từ bảng vectơ nhưng nó có thể phân nhánh thành địa chỉ 32 bit đầy đủ. Lệnh AB (nhánh) sẽ đi
trực tiếp đến trình xử lý, nhưng nó chỉ có thể phân nhánh đến địa chỉ 24 bit. Vì vectơ FIQ là mục cuối cùng trong bảng
vectơ nên mã xử lý FIQ có thể được đặt trực tiếp tại vị trí vectơ FIQ, cho phép trình xử lý FIQ được thực thi nhanh chóng.

3.1.5 Trả về từ Trình xử lý ngoại lệ

Giả sử rằng các trình xử lý ngoại lệ/ngắt được nhập bằng các lệnh LDR PC, như trong

PC LDR, handler_entry_address

Khi vào một quy trình xử lý ngoại lệ/ngắt, bộ xử lý sẽ tự động lưu trữ địa chỉ trả về trong thanh ghi liên kết r14 của
chế độ hiện tại. Do đường dẫn lệnh trong bộ xử lý ARM, địa chỉ trả về được lưu trong thanh ghi liên kết bao gồm một phần
bù, phải được trừ khỏi thanh ghi liên kết để trở về đúng vị trí trước ngoại lệ hoặc ngắt. Bảng 3.4 cho thấy độ lệch bộ đếm
chương trình của các ngoại lệ khác nhau.
Đối với Data Abort, địa chỉ trả về là PC-8, trỏ đến hướng dẫn ban đầu gây ra ngoại lệ. Đối với các ngắt và Hủy bỏ tìm
nạp trước, địa chỉ trả về là PC-4 do lệnh được thực thi trước khi ngoại lệ nằm ở PC-4 hiện tại. Đối với lệnh SWI và lệnh
Không xác định, địa chỉ trả về là LR hiện tại vì bộ xử lý ARM đã thực thi lệnh SWI hoặc lệnh không xác định. Đối với hầu
hết những người mới bắt đầu, việc xử lý không thống nhất các địa chỉ trả lại IRQ và SWI thường là nguyên nhân gây nhầm lẫn.
Hậu tố 'S' ở cuối lệnh MOV chỉ định rằng, nếu các thanh ghi đích liên quan đến việc tải PC thì CPSR cũng sẽ được khôi phục
từ SPSR đã lưu.
Một phương thức trả về điển hình từ trình xử lý ngắt là thực thi lệnh sau ở cuối trình xử lý ngắt

Bảng 3.4 Độ lệch bộ đếm chương trình


Machine Translated by Google

3.1 Ngoại lệ ARM 51

Máy tính SUBS, r14_irq, #4

tải PC với r14_irq – 4, giả sử rằng r14_irq chưa bị thay đổi trong trình xử lý ngắt. Ngoài ra, thanh ghi liên kết có thể được điều chỉnh ở đầu

trình xử lý ngắt, như trong.

SUB lr, lr, #4

<mã xử lý>

Máy tính MOVS, LR

Một phương pháp được sử dụng rộng rãi hơn như sau.

SUB lr, lr, #4 // trừ 4 dạng LR

STMFD sp_irq!,{r0-r12,lr} // đẩy r0-r12,LR

<mã xử lý>

LDMFD sp_irq!,{r0-r12,pc}^ // pop r0-r12,PC,SPSR

Đầu tiên, trình xử lý ngắt trừ 4 từ thanh ghi liên kết, lưu nó vào ngăn xếp, sau đó thực thi mã xử lý. Khi trình xử lý kết thúc, nó quay

trở lại điểm bị gián đoạn bằng lệnh LDMFD với ký hiệu ^, tải PC với LR đã lưu và khôi phục SPSR, khiến quay trở lại chế độ trước đó trước khi

bị gián đoạn. Thay vì trừ 4 từ thanh ghi liên kết theo cách thủ công, trình xử lý ngắt có thể được viết bằng C với thuộc tính ngắt, như trong

void __attribute__((ngắt))handler()

{
// mã xử lý thực tế

Trong trường hợp đó, mã do trình biên dịch tạo ra sẽ tự động điều chỉnh thanh ghi liên kết.

3.2 Ngắt và xử lý ngắt

3.2.1 Các loại ngắt

Bộ xử lý ARM chỉ chấp nhận hai yêu cầu ngắt bên ngoài là FIQ và IRQ. Cả hai đều là tín hiệu mức thấp hoạt động nhạy cảm với bộ xử lý. Để CPU

chấp nhận một ngắt, bit mặt nạ ngắt (I hoặc F) thích hợp trong CPSR phải được xóa về 0. FIQ có mức ưu tiên cao hơn IRQ, do đó FIQ sẽ được xử

lý trước khi xảy ra nhiều ngắt. Việc xử lý FIQ khiến IRQ và FIQ tiếp theo bị vô hiệu hóa, ngăn không cho chúng được lấy cho đến khi trình xử

lý FIQ thoát hoặc kích hoạt chúng một cách rõ ràng. Điều này thường được thực hiện bằng cách khôi phục CPSR từ SPSR ở cuối trình xử lý FIQ.

Vectơ FIQ là mục cuối cùng trong bảng vectơ. Mã xử lý FIQ có thể được đặt trực tiếp tại vị trí vectơ và chạy tuần tự từ địa chỉ đó. Điều

này tránh được lệnh rẽ nhánh và độ trễ liên quan của nó. Nếu hệ thống có bộ nhớ đệm, bảng vectơ và trình xử lý FIQ đều có thể bị khóa trong

một khối trong bộ đệm. Điều này rất quan trọng vì FIQ được thiết kế để xử lý ngắt càng nhanh càng tốt. Mỗi chế độ đặc quyền có các thanh ghi

ngân hàng r13, r14 và SPSR riêng. Chế độ FIQ có thêm 5 thanh ghi ngân hàng (r8–r12), có thể được sử dụng để chứa thông tin giữa các lệnh gọi

trong trình xử lý FIQ, giúp tăng thêm tốc độ thực thi của trình xử lý FIQ.

3.2.2 Bộ điều khiển ngắt

Hệ thống dựa trên ARM thường chỉ hỗ trợ một nguồn ngắt FIQ nhưng nó có thể hỗ trợ nhiều yêu cầu IRQ từ các nguồn khác nhau. Để hỗ trợ nhiều

ngắt IRQ, cần có bộ điều khiển ngắt để phân loại các nguồn IRQ khác nhau và chỉ đưa ra một yêu cầu IRQ cho CPU. Hầu hết các bo mạch ARM đều

bao gồm Bộ điều khiển ngắt Vectored (VIC), là ARM PL190 hoặc PL192 (ARM PL190, PL192 2016). Một VIC cung cấp các chức năng sau.
Machine Translated by Google

52 3 Xử lý ngắt và ngoại lệ

. Ưu tiên các nguồn ngắt

. Hỗ trợ ngắt vectơ

3.2.2.1 Bộ điều khiển ngắt ARM PL190/192 Hình 3.1


thể hiện sơ đồ khối của PL190 VIC. Nó hỗ trợ 16 ngắt có vectơ. PL192 VIC tương tự nhưng nó hỗ trợ 32 ngắt có vectơ.

3.2.2.2 IRQ có vectơ và không có vectơ VIC


nhận tất cả các yêu cầu ngắt từ các nguồn khác nhau và sắp xếp chúng thành ba loại, FIQ, IRQ có vectơ và IRQ không
có vectơ. FIQ có mức độ ưu tiên cao nhất. Trong trường hợp PL190 VIC, nó nhận 16 yêu cầu ngắt và ưu tiên chúng
trong 16 khe IRQ có vectơ, ký hiệu là IRQ0-IRQ15. Mỗi ngắt có vectơ có một địa chỉ vectơ, được ký hiệu là VectAddr0-
VectAddr15.

3.2.2.3 Ưu tiên ngắt Trong số


các ngắt có vectơ, IRQ0 có mức ưu tiên cao nhất và IRQ15 có mức ưu tiên thấp nhất. IRQ không có vectơ có mức ưu
tiên thấp nhất. VIC OR là các yêu cầu từ cả IRQ có vectơ và không có vectơ để tạo tín hiệu IRQ đến lõi ARM, được
hiển thị dưới dạng dòng nVICIRQ trong Hình 3.1 .

Hình 3.1 ARM PL190 VIC


Machine Translated by Google

3.2 Ngắt và xử lý ngắt 53

3.2.3 Bộ điều khiển ngắt chính và phụ

Một số bo mạch ARM có thể chứa nhiều hơn một VIC. Ví dụ: bo mạch ARM926EJ-S có hai VIC, một VIC chính (PIC) và một VIC phụ
(SIC), được hiển thị trong Hình 3.2. Hầu hết các đầu vào của PIC được dành riêng cho các ngắt có mức ưu tiên cao, chẳng
hạn như bộ định thời, GPIO và UART. Các nguồn ngắt có mức ưu tiên thấp, chẳng hạn như USB, Ethernet, bàn phím và chuột,
được đưa vào SIC. Một số ngắt này có thể được định tuyến tới PIC ở IRQ 21 đến 26. Các ngắt có mức ưu tiên thấp hơn, chẳng
hạn như màn hình cảm ứng, bàn phím và chuột được định tuyến chung đến IRQ 31 của PIC.

Hình 3.2 VIC trong bo mạch ARM926EJ-S


Machine Translated by Google

54 3 Xử lý ngắt và ngoại lệ

3.3 Xử lý ngắt

3.3.1 Nội dung bảng vectơ

Các vectơ ngắt nằm trong bảng vectơ ngoại lệ. Mỗi vị trí vectơ ngắt chứa một lệnh tải PC với địa chỉ đầu vào của trình xử lý
ngắt. Đối với các ngắt FIQ và IRQ, nội dung vectơ là

0x18: PC LDR, irq_handler_addr


0x1C: PC LDR, fiq_handler_addr
irq_handler_addr: .word irq_handler
fiq_handler_addr: .word fiq_handler

3.3.2 Trình tự ngắt phần cứng

Khi xảy ra ngắt FIQ, bộ xử lý sẽ thực hiện như sau.

1. LR_fiq = địa chỉ của lệnh tiếp theo sẽ được thực thi + 4.
2. SPSR_fiq = CPSR.
3. CPSR[4:0] = 0x11 (chế độ FIQ).
4. CPSR[5] = 0 (Thực thi ở trạng thái ARM).
5. CPSR[7-6] = 11 (Vô hiệu hóa các ngắt IRQ và FIQ).
6. PC = 0x1C (thực thi trình xử lý FIQ).

Tương tự, khi xảy ra ngắt IRQ, bộ xử lý sẽ thực hiện như sau.

1. LR_irq = địa chỉ của lệnh tiếp theo sẽ được thực thi + 4.
2. SPSR_irq = CPSR.
3. CPSR[4:0] = 0x12 (chế độ IRQ).
4. CPSR[5] = 0 (Thực thi ở trạng thái ARM).
5. CPSR[7-6] = 01 (Tắt IRQ nhưng vẫn bật FIQ).
6. PC = 0x18 (thực thi trình xử lý IRQ).

3.3.3 Điều khiển ngắt trong phần mềm

Khi thảo luận về ngắt và xử lý ngắt, có một số thuật ngữ thường được sử dụng cần được làm rõ.

3.3.3.1 Bật/Tắt ngắt Mỗi thiết bị có một


thanh ghi điều khiển hoặc, trong một số trường hợp, một thanh ghi điều khiển ngắt riêng biệt, có thể được lập trình để cho
phép hoặc không cho phép thiết bị tạo ra các yêu cầu ngắt. Nếu một thiết bị sử dụng ngắt thì thanh ghi điều khiển ngắt của
thiết bị phải được cấu hình với các ngắt được kích hoạt. Nếu cần, các ngắt thiết bị có thể bị vô hiệu hóa một cách rõ ràng. Do
đó, thuật ngữ bật/tắt các ngắt chỉ nên được áp dụng cho các thiết bị.

3.3.3.2 Che dấu ngắt Khi một


thiết bị đưa ra một ngắt tới CPU, CPU có thể chấp nhận hoặc không chấp nhận ngắt ngay lập tức, tùy thuộc vào các bit che dấu
ngắt trong thanh ghi trạng thái của CPU. Đối với ngắt IRQ, CPU ARM chấp nhận ngắt nếu bit I trong thanh ghi CPSR là 0, nghĩa
là CPU có các ngắt IRQ bị che giấu hoặc bị che giấu. Nó không chấp nhận ngắt trong khi bit I của CPSR là 1, nghĩa là CPU có
các ngắt IRQ bị che giấu. Các ngắt được che dấu không bị mất. Chúng được giữ ở trạng thái chờ cho đến khi bit I của CPSR được
đổi thành 0, lúc đó CPU sẽ chấp nhận ngắt. Do đó, khi được áp dụng cho CPU, việc bật/tắt các ngắt thực sự có nghĩa là các ngắt
che mặt/mặt nạ. Trong hầu hết các nền văn học, những thuật ngữ này được sử dụng thay thế cho nhau nhưng người đọc cần nhận
thức được sự khác biệt của chúng.
Machine Translated by Google

3.3 Xử lý ngắt 55

3.3.3.3 Xóa yêu cầu ngắt thiết bị Khi CPU


chấp nhận một ngắt IRQ, nó bắt đầu thực thi trình xử lý ngắt cho thiết bị đó. Khi kết thúc trình xử lý ngắt, nó phải
xóa yêu cầu ngắt, điều này khiến thiết bị loại bỏ yêu cầu ngắt, cho phép thiết bị tạo ra ngắt tiếp theo. Điều này
thường được thực hiện bằng cách truy cập vào một số thanh ghi giao diện thiết bị. Ví dụ: đọc thanh ghi dữ liệu của
thiết bị đầu vào sẽ xóa yêu cầu ngắt thiết bị. Đối với một số thiết bị đầu ra, có thể cần phải tắt ngắt thiết bị một
cách rõ ràng khi không còn dữ liệu để xuất.

3.3.3.4 Gửi EOI đến Bộ điều khiển ngắt theo vectơ Trong
một hệ thống có nhiều nguồn ngắt, Bộ điều khiển ngắt theo vectơ (VIC) thường được sử dụng để ưu tiên các ngắt của
thiết bị, mỗi nguồn có một địa chỉ vectơ chuyên dụng. Khi kết thúc việc xử lý ngắt hiện tại, bộ xử lý ngắt phải thông
báo cho VIC rằng nó đã xử lý xong ngắt hiện tại (có mức ưu tiên cao nhất), cho phép VIC ưu tiên lại các yêu cầu ngắt
đang chờ xử lý. Điều này được gọi là gửi Thông báo kết thúc ngắt (EOI) tới bộ điều khiển ngắt. Đối với ARM PL190,
việc này được thực hiện bằng cách ghi một giá trị tùy ý vào thanh ghi địa chỉ vectơ của VIC ở cơ sở +0x30.
Bộ xử lý ARM có một cách đơn giản để bật/tắt (vạch mặt/che giấu) các ngắt khi ở chế độ đặc quyền. Các đoạn mã sau đây cho biết cách

bật/tắt các ngắt IRQ của bộ xử lý ARM.

kho a:

MRS r1, CPSR ; đọc CPSR vào r1

ORR r1, r1, #0x80 ; đặt bit-7(I bit) thành 1

MSR CPSR, r1 ; viết r1 trở lại CPSR

mở khóa:

MRS r1, CPSR ; đọc CPSR vào r1

BIC r1, r1, #0x80 ; xóa bit-7 (I bit) trong r1

MSR CPSR r1 ; viết r1 trở lại CPSR

Để kích hoạt ngắt IRQ, trước tiên hãy sao chép CPSR vào thanh ghi đang hoạt động, xóa I_bit (7) trong thanh ghi đang hoạt động về

0. Sau đó sao chép thanh ghi đã cập nhật trở lại CPSR, điều này cho phép ngắt IRQ. Tương tự, cài đặt I_bit của CPSR sẽ vô hiệu hóa

các ngắt IRQ. Các đoạn mã tương tự có thể được sử dụng để bật/tắt các ngắt FIQ (bằng cách xóa/đặt bit-6 trong CPSR).

3.3.4 Trình xử lý ngắt

Trình xử lý ngắt còn được gọi là Quy trình dịch vụ ngắt (ISR). Trình xử lý ngắt có thể được phân thành ba loại khác nhau.

3.3.4.1 Các loại trình xử lý ngắt

. Trình xử lý ngắt không lồng nhau: xử lý một ngắt tại một thời điểm. Các ngắt không được kích hoạt cho đến khi việc thực thi ISR
hiện tại kết thúc.

. Trình xử lý ngắt lồng nhau: Khi ở trong ISR, hãy bật IRQ để cho phép xảy ra các ngắt IRQ có mức độ ưu tiên cao hơn. Điều này ngụ ý

rằng trong khi thực thi ISR hiện tại, nó có thể bị gián đoạn để thực thi ISR khác có mức độ ưu tiên cao hơn.
. Trình xử lý ngắt đăng nhập lại: Kích hoạt IRQ càng sớm càng tốt, cho phép thực thi lại cùng một ISR.

3.3.5 Trình xử lý ngắt không lồng nhau

Khi CPU ARM chấp nhận một ngắt IRQ, nó sẽ chuyển sang chế độ IRQ với các ngắt IRQ bị che đi, ngăn không cho nó chấp nhận các ngắt IRQ

khác. CPU đặt PC trỏ tới mục IRQ trong bảng vectơ và thực hiện lệnh đó.

Lệnh tải PC với địa chỉ mục nhập của trình xử lý ngắt, khiến việc thực thi đi vào trình xử lý ngắt. Trình xử lý ngắt trước tiên lưu

bối cảnh thực thi trước khi ngắt. Sau đó, nó xác định nguồn ngắt và gọi ISR thích hợp. Sau khi phục vụ ngắt, nó khôi phục bối cảnh đã

lưu và đặt PC quay lại bối cảnh tiếp theo.


Machine Translated by Google

56 3 Xử lý ngắt và ngoại lệ

hướng dẫn trước khi bị gián đoạn. Sau đó nó trở lại vị trí ban đầu bị gián đoạn. Trình xử lý ngắt đơn giản nhất chỉ phục vụ một ngắt

tại một thời điểm. Trong khi thực hiện trình xử lý ngắt, các ngắt IRQ được ẩn đi cho đến khi điều khiển được đưa trở lại điểm bị
gián đoạn. Thuật toán và luồng điều khiển của trình xử lý ngắt không lồng nhau như sau.

non_nested_interrupt_handler()

// Ngắt IRQ được che giấu trong CPSR

1. Lưu bối cảnh

2. Xác định nguồn ngắt 3. Gọi ISR để xử lý

ngắt

4. Khôi phục bối cảnh

5. Khôi phục CPSR bằng SPSR và quay về điểm ngắt

Đoạn mã sau đây cho thấy cách tổ chức một trình xử lý ngắt IRQ đơn giản. Nó giả định rằng ngăn xếp chế độ IRQ

đã được thiết lập đúng cách, việc này thường được thực hiện trong trình xử lý đặt lại trong quá trình khởi tạo hệ thống.

SUB lr, lr, #4

STMFD sp_irq!, {r0-r12, lr}

{mã quy trình dịch vụ ngắt cụ thể}

LDMFD sp_irq!, {r0-r12, pc}^

Lệnh đầu tiên điều chỉnh thanh ghi liên kết (r14) để quay trở lại điểm bị gián đoạn. Lệnh STMFD lưu bối cảnh tại điểm bị gián
đoạn bằng cách đẩy các thanh ghi CPU phải được bảo toàn vào ngăn xếp. Thời gian thực hiện lệnh STMFD hoặc LDMFD tỷ lệ thuận với số

lượng thanh ghi được truyền. Để giảm độ trễ xử lý ngắt, cần lưu số lượng thanh ghi tối thiểu. Khi viết ISR bằng ngôn ngữ lập trình

cấp cao, chẳng hạn như C, điều quan trọng là phải biết quy ước gọi mã do trình biên dịch tạo ra vì điều này sẽ ảnh hưởng đến quyết
định về những thanh ghi nào sẽ được lưu trên ngăn xếp. Ví dụ: mã được tạo bởi trình biên dịch ARM bảo toàn r4–r11 trong khi gọi hàm,

do đó không cần lưu các thanh ghi này trừ khi chúng được sử dụng bởi trình xử lý ngắt.

Khi các thanh ghi đã được lưu, giờ đây việc gọi các hàm C để xử lý ngắt là an toàn. Khi kết thúc trình xử lý ngắt, lệnh LDMFD sẽ

khôi phục ngữ cảnh đã lưu và trả về từ trình xử lý ngắt. Ký hiệu '^' ở cuối lệnh LDMFD có nghĩa là CPSR sẽ được khôi phục từ SPSR

đã lưu. Như đã lưu ý trong Chap. 2 theo hướng dẫn ARM, '^' chỉ khôi phục SPSR đã lưu nếu PC được tải cùng lúc. Nếu không, nó chỉ
khôi phục các thanh ghi ngân hàng của chế độ trước đó, ngoại trừ SPSR đã lưu. Tính năng đặc biệt này có thể được sử dụng để truy

cập vào các thanh ghi Chế độ người dùng khi ở chế độ đặc quyền.

Việc tổ chức trình xử lý ngắt đơn giản phù hợp để xử lý cả ngắt FIQ và IRQ cùng một lúc mà không cần lồng nhau. Sau khi lưu bối

cảnh thực thi, trình xử lý ngắt phải xác định nguồn ngắt. Trong hệ thống ARM đơn giản không sử dụng các ngắt có vectơ, nguồn ngắt

nằm trong thanh ghi trạng thái ngắt, được biểu thị bằng IRQstatus, nằm ở một địa chỉ đã biết (được ánh xạ bộ nhớ). Để xác định nguồn
ngắt, chỉ cần đọc thanh ghi IRQstatus và quét nội dung để tìm bất kỳ bit hoặc bit nào được đặt. Mỗi bit khác 0 là viết tắt của một

yêu cầu ngắt hoạt động. Trình xử lý ngắt có thể quét các bit theo một thứ tự cụ thể, xác định mức độ ưu tiên xử lý ngắt trong phần

mềm.
Với những thông tin cơ bản ở trên về ngắt và xử lý ngắt, chúng ta đã sẵn sàng viết một số chương trình thực tế sử dụng ngắt.

Trong phần sau đây, chúng tôi sẽ trình bày cách viết các trình xử lý ngắt cho các thiết bị I/O. Trình điều khiển thiết bị sử dụng
ngắt được gọi là trình điều khiển thiết bị điều khiển ngắt. Cụ thể, chúng tôi sẽ trình bày cách triển khai trình điều khiển điều

khiển ngắt cho bộ hẹn giờ, bàn phím, UART và Thẻ kỹ thuật số an toàn (SDC).

3.4 Trình điều khiển hẹn giờ

3.4.1 Bộ định thời 926EJS đa năng ARM

Bo mạch ARM đa năng 926EJS chứa hai mô-đun hẹn giờ kép ARM SB804 [Bộ hẹn giờ ARM 2004]. Mỗi mô-đun hẹn giờ chứa hai bộ định thời,

được điều khiển bởi cùng một đồng hồ. Địa chỉ cơ sở của bộ định thời là tại
Machine Translated by Google

3.4 Trình điều khiển hẹn giờ 57

Bộ hẹn giờ0: 0x101E2000, Bộ hẹn giờ1: 0x101E2020


Bộ hẹn giờ2: 0x101E3000, Bộ hẹn giờ3: 0x101E3020

Ngắt hẹn giờ 0 và hẹn giờ 1 tại IRQ4. Ngắt hẹn giờ 2 và hẹn giờ 3 ở IRQ5, cả hai đều trên Bộ điều khiển vectơ chính

(VIC). Để bắt đầu, chúng ta sẽ không sử dụng các ngắt có vectơ. Các ngắt có vector sẽ được thảo luận sau. Từ một chương trình

Theo quan điểm, các thanh ghi định thời quan trọng nhất là các thanh ghi điều khiển và bộ đếm. Sau đây liệt kê ý nghĩa của

bit thanh ghi điều khiển hẹn giờ.

Bit --------- Chức năng ------- ----- Cài đặt=0x66 --------

7 tắt/bật hẹn giờ 0 được đặt thành 1 trong time_start()


6 chế độ chạy tự do/định kỳ 1 cho chế độ định kỳ

5 ngắt vô hiệu hóa/kích hoạt 1 để kích hoạt ngắt

4 Không được sử dụng 0 (sử dụng 0 làm mặc định)

Chia 3:2:00=1,01=8,10=256 01 để chia cho 8

1 Giá trị bộ đếm 16/32-bit 1 cho bộ đếm 32 bit

0 chế độ bao quanh/oneshot 0 cho chế độ bao quanh


-------------------------------------------------- ------------

3.4.2 Chương trình điều khiển hẹn giờ

Chương trình mẫu C3.1 trình diễn trình điều khiển hẹn giờ cho bo mạch Đa năng ARM. Chương trình bao gồm những điều sau đây

các thành phần.

(1). tệp t.ld: Tệp tập lệnh liên kết chỉ định reset_handler làm địa chỉ mục nhập. Nó xác định hai vùng 4 KB là SVC và IRQ
ngăn xếp.

NHẬP(reset_handler)

PHẦN

. = 0x10000; /* đang tải địa chỉ */

.chữ : { ts.o *(.text) }

.dữ liệu : { *(.dữ liệu) }


.bss : { *(.bss) }

. = CÁNH(8);

. = . + 0x1000; /* 4kB không gian ngăn xếp SVC */

svc_stack_top = .;

. = . + 0x1000; /* 4kB không gian ngăn xếp IRQ */

irq_stack_top = .;

(2). tệp ts.s: Tệp mã hợp ngữ, ts.s, xác định điểm vào và mã đặt lại.

/***************** ts.s tệp của C3.1 *******************/

.chữ

.code 32

.global reset_handler, vectơ_start, vectơ_end


Machine Translated by Google

58 3 Xử lý ngắt và ngoại lệ

reset_handler:

LDR sp, =svc_stack_top // đặt ngăn xếp chế độ SVC

Bản sao BL_vector // sao chép bảng vectơ vào địa chỉ 0

MSR cpsr, #0x92 // sang chế độ IRQ

LDR sp, =irq_stack_top // đặt ngăn xếp chế độ IRQ

MSR cpsr, #0x13 // quay lại chế độ SVC khi bật IRQ

BL chính // gọi hàm main() trong C


B . // vòng lặp nếu main quay trở lại

irq_handler:

phụ lr, lr, #4

stmfd sp!, {r0-r12, lr} // ngăn xếp r0-r12 và lr

bl IRQ_handler // gọi IRQ_hanler() trong C

ldmfd sp!, {r0-r12, pc}^ // trả về

vectơ_start:

PC LDR, reset_handler_addr

Máy tính LDR, undef_handler_addr

Máy tính LDR, swi_handler_addr

PC LDR, tìm nạp trước_abort_handler_addr

PC LDR, dữ liệu_abort_handler_addr

B .

Máy tính LDR, irq_handler_addr

PC LDR, fiq_handler_addr

đặt lại_handler_addr: .word reset_handler

undef_handler_addr: .word undef_handler

swi_handler_addr: .word swi_handler

prefetch_abort_handler_addr: .word prefetch_abort_handler

dữ liệu_abort_handler_addr: .word data_abort_handler

irq_handler_addr: .word irq_handler

fiq_handler_addr: .word fiq_handler

vectơ_end:

Khi chương trình khởi động, QEMU tải hình ảnh thực thi, t.bin, về 0x10000. Khi vào ts.s, quá trình thực thi bắt đầu

từ nhãn reset_handler. Đầu tiên, nó đặt con trỏ ngăn xếp chế độ SVC và gọi copy_vector() trong C để sao chép các vectơ sang

địa chỉ 0. Nó chuyển sang chế độ IRQ để thiết lập con trỏ ngăn xếp IRQ. Sau đó nó chuyển về chế độ SVC với các ngắt IRQ

được bật và gọi main() trong C. Chương trình chính thường chạy ở chế độ SVC. Nó chỉ vào chế độ IRQ để xử lý các ngắt.

Vì chúng tôi không sử dụng FIQ và cũng không cố gắng xử lý bất kỳ trường hợp ngoại lệ nào tại thời điểm này nên tất cả các trình xử lý ngoại lệ khác đều

(trong tệp ngoại lệ.c) là các vòng lặp while(1).

(3). tc: tệp này chứa main(), copy_vector() và IRQ_handler() trong C

#include "định nghĩa.h" // Địa chỉ LCD, TIMER và UART

#include "string.c" // strcmp, strlen, v.v.

#include "timer.c" // tập tin xử lý hẹn giờ

#include "vid.c" // Tệp trình điều khiển LCD

#include "Exceptions.c" // các trình xử lý ngoại lệ khác

void copy_vectors(void){// sao chép bảng vectơ trong ts.s sang 0x0

vectơ_start u32 bên ngoài, vectơ_end;

u32 *vectors_src = &vectors_start;

u32 *vectors_dst = (u32 *)0;

trong khi (vectors_src < &vectors_end)

*vector_dst++ = *vector_src++;

}
Machine Translated by Google

3.4 Trình điều khiển hẹn giờ 59

void time_handler();

THỜI GIAN *tp[4]; // 4 con trỏ cấu trúc TIMER

khoảng trống IRQ_handler() // Trình xử lý ngắt IRQ trong C

// đọc các thanh ghi trạng thái VIC để xác định nguồn ngắt

int vicstatus = VIC_STATUS;

// BIT trạng thái VIC: time0,1=4, uart0=13, uart1=14

nếu (vicstatus & (1<<4)){ // bit4=1:timer0,1

if (*(tp[0]->base+TVALUE)==0) // bộ đếm thời gian 0

bộ đếm thời gian_handler(0);

if (*(tp[1]->base+TVALUE)==0) // bộ đếm thời gian 1

bộ đếm thời gian_handler(1);

nếu (vicstatus & (1<<5)){ // bit5=1:timer2,3

if (*(tp[2]->base+TVALUE)==0) // bộ đếm thời gian 2

bộ đếm thời gian_handler(2);

if (*(tp[3]->base+TVALUE)==0) // bộ đếm thời gian 3

bộ đếm thời gian_handler(3);

int chính()

int tôi;

màu = ĐỎ; // int màu trong file vid.c

fbuf_init(); // khởi tạo trình điều khiển LCD

printf("bắt đầu chính\n"); /* kích

hoạt VIC để ngắt bộ đếm thời gian */

VIC_INTENABLE = 0;

VIC_INTENABLE |= (1<<4); // bộ đếm thời gian0,1 tại VIC.bit4 VIC_INTENABLE |=

(1<<5); // bộ đếm thời gian2,3 tại VIC.bit5

bộ đếm thời gian_init();

cho (i=0; i<4; i++){ // khởi động cả 4 bộ định thời

tp[i] = &timer[i];

bộ đếm thời gian_start(i);

printf("Nhập vòng lặp while(1), xử lý các ngắt hẹn giờ\n");

(4). tập tin hẹn giờ.c: time.c thực hiện các trình xử lý hẹn giờ

// đăng ký bộ định thời u32 offset từ địa chỉ cơ sở

#define TLOAD 0x0

#define TVALUE 0x1

#define TCNTL 0x2

#define TINTCLR 0x3

#xác định TRIS 0x4

#define TMIS 0x5

#define TBGLOAD 0x6


Machine Translated by Google

60 3 Xử lý ngắt và ngoại lệ

bộ đếm thời gian cấu trúc dễ bay hơi typedef {

u32 *cơ sở; // địa chỉ cơ sở của bộ định thời; như con trỏ u32 int tích,

hh, mm, ss; // mỗi vùng dữ liệu hẹn giờ

đồng hồ char[16];

}Đồng hồ hẹn giờ;

bộ đếm thời gian TIMER dễ bay hơi [4]; // 4 bộ định thời; 2 mỗi đơn vị; ở 0x00 và 0x20

làm trống bộ đếm thời gian_init()

int tôi; THỜI GIAN *tp;

printf("timer_init()\n");

cho (i=0; i<4; i++){

tp = &timer[i];

if (i==0) tp->base = (u32 *)0x101E2000;

if (i==1) tp->base = (u32 *)0x101E2020;

if (i==2) tp->base = (u32 *)0x101E3000;

if (i==3) tp->base = (u32 *)0x101E3020;

*(tp->base+TLOAD) = 0x0; // cài lại

*(tp->base+TVALUE)= 0xFFFFFFFF;

*(tp->base+TRIS) = 0x0;

*(tp->base+TMIS) = 0x0;

*(tp->base+TLOAD) = 0x100;

// CntlReg=011-0010=|En|Pe|IntE|-|scal=01|32bit|0=wrap|=0x66

*(tp->base+TCNTL) = 0x66;

*(tp->base+TBGLOAD) = 0x1C00; // giá trị bộ đếm thời gian tp->tick = tp->hh = tp-

>mm = tp->ss = 0; // khởi tạo đồng hồ treo tường strcpy((char *)tp->clock, "00:00:00");

void time_handler(int n) { int i;

THỜI GIAN *t = &timer[n];

t->đánh dấu++; // Giả sử 120 tích tắc mỗi giây

nếu (t->tick==120){

t->đánh dấu = 0; t->ss++;

nếu (t->ss == 60){

t->ss = 0; t->mm++;

nếu (t->mm == 60){

t->mm = 0; t->hh++; // không có vòng quay 24 giờ

t->clock[7]='0'+(t->ss%10); t->clock[6]='0'+(t->ss/10);

t->clock[4]='0'+(t->mm%10); t->đồng hồ[3]='0'+(t->mm/10);

t->clock[1]='0'+(t->hh%10); t->clock[0]='0'+(t->hh/10);

màu = n; // hiển thị với màu khác

cho (i=0; i<8; i++){

kpchar(t->clock[i], n, 70+i); // tới dòng n của LCD

hẹn giờ_clearInterrupt(n); // xóa ngắt hẹn giờ

void time_start(int n) // time_start(0), 1, v.v.

{
Machine Translated by Google

3.4 Trình điều khiển hẹn giờ 61

Hình 3.3 Đầu ra của bộ xử lý ngắt hẹn giờ

THỜI GIAN *tp = &timer[n];

kprintf("timer_start %d base=%x\n", n, tp->base); *(tp->base+TCNTL) |=

0x80; // đặt bit kích hoạt 7

int time_clearInterrupt(int n) // time_start(0), 1, v.v.

THỜI GIAN *tp = &timer[n];

*(tp->base+TINTCLR) = 0xFFFFFFFF;

void time_stop(int n) // dừng bộ đếm thời gian

THỜI GIAN *tp = &timer[n]; *(tp-

>base+TCNTL) &= 0x7F; // xóa bit kích hoạt 7

Hình 3.3 hiển thị màn hình LCD chạy chương trình C3.1. Mỗi bộ hẹn giờ hiển thị một đồng hồ treo tường ở góc trên bên phải màn
hình. Trong trình điều khiển LCD, giới hạn cuộn lên được đặt thành một dòng bên dưới logo và đồng hồ treo tường để chúng không bị
ảnh hưởng trong quá trình cuộn lên. Đồng hồ treo tường được cập nhật từng giây. Như bài tập, người đọc có thể thay đổi giá trị
bắt đầu của đồng hồ treo tường để hiển thị giờ địa phương ở các múi giờ khác nhau hoặc thay đổi giá trị bộ đếm thời gian để tạo
ra các ngắt hẹn giờ với các tần số khác nhau.

Trình điều khiển bàn phím 3.5

3.5.1 Giao diện chuột-bàn phím ARM PL050

Bo mạch đa năng ARM bao gồm Giao diện bàn phím-chuột ARM PL050 (MKI) cung cấp hỗ trợ cho chuột và bàn phím tương thích PS/2 [ARM
PL050 MKI 1999]. Địa chỉ cơ sở của bàn phím là 0x1000600. Nó có một số thanh ghi 32 bit, nằm ở vị trí lệch so với địa chỉ cơ sở.
Machine Translated by Google

62 3 Xử lý ngắt và ngoại lệ

Đăng ký bù đắp Chuyển nhượng bit


------ ---------- -----------------------------

0x00 Điều khiển bit 5=0(AT) 4=IntEn 2=Bật

0x04 Trạng thái bit 4=RXF 3=RXBUSY

0x08 Dữ liệu mã quét đầu vào

0x0C ClkDiv (giá trị trong khoảng 0-15)

0x10 Bit IntStatus 0=ngắt RX


--------------------------------------------------

3.5.2 Trình điều khiển bàn phím

Trong phần này, chúng ta sẽ phát triển một trình điều khiển điều khiển ngắt đơn giản cho bàn phím đa năng ARM. Để sử dụng các ngắt,

thanh ghi điều khiển của bàn phím phải được khởi tạo thành 0x14, tức là bit 2 kích hoạt bàn phím và bit 4 kích hoạt Rx (đầu vào)

ngắt quãng. Bàn phím ngắt ở IRQ3 trên VIC phụ, được chuyển đến IRQ31 trên VIC chính. Thay vì

Mã ACSII, bàn phím tạo mã quét. Danh sách đầy đủ các mã quét được bao gồm trong trình điều khiển bàn phím.

Việc dịch mã quét sang ASCII được thực hiện bằng cách ánh xạ các bảng trong phần mềm. Điều này cho phép sử dụng cùng một bàn phím cho

ngôn ngữ khác nhau. Đối với mỗi phím được gõ, bàn phím sẽ tạo ra hai ngắt; một khi nhấn phím và một cái khác

khi chìa khóa được thả ra. Mã quét của phím nhả là 0x80 + mã quét của phím bấm, tức là bit7 là 0 cho phím bấm và 1

để phát hành khóa. Khi bàn phím bị gián đoạn, mã quét nằm trong thanh ghi dữ liệu (0x08). Trình xử lý ngắt phải đọc

thanh ghi dữ liệu để lấy mã quét, thao tác này cũng xóa ngắt bàn phím. Một số phím đặc biệt tạo phím thoát

các chuỗi, ví dụ: phím mũi tên LÊN tạo ra 0xE048, trong đó 0xE0 chính là phím thoát. Sau đây cho thấy ánh xạ

bảng để dịch mã quét sang ASCII. Bàn phím có 105 phím. Mã quét trên 0x39 (57) là các phím đặc biệt,

không thể ánh xạ trực tiếp nên chúng không được hiển thị trong các bản đồ chính. Những chìa khóa đặc biệt này được người lái xe nhận biết và

xử lý tương ứng. Hình 3.4 thể hiện các bảng ánh xạ chính.

3.5.3 Thiết kế trình điều khiển theo hướng ngắt

Mỗi trình điều khiển thiết bị được điều khiển bằng ngắt bao gồm ba phần; nửa dưới là bộ xử lý ngắt, nửa trên là

phần được chương trình ứng dụng gọi và vùng dữ liệu chung chứa bộ đệm cho các biến dữ liệu và điều khiển

để đồng bộ hóa, được chia sẻ bởi phần dưới và phần trên. Hình 3.5 thể hiện tổ chức của bàn phím

tài xế. Phần trên cùng của hình hiển thị kbd_init(), khởi tạo trình điều khiển KBD khi hệ thống khởi động. Phần giữa

hiển thị đường dẫn điều khiển và luồng dữ liệu từ thiết bị KBD đến chương trình. Phần dưới cùng hiển thị nửa dưới, bộ đệm đầu vào,

và tổ chức nửa trên của trình điều khiển KBD.

Khi chương trình chính khởi động, nó phải khởi tạo các biến điều khiển của trình điều khiển bàn phím. Khi nhấn một phím, KBD

tạo ra một ngắt, khiến trình xử lý ngắt được thực thi. Trình xử lý ngắt lấy mã quét từ KBD

cổng dữ liệu. Đối với các lần nhấn phím thông thường, nó sẽ dịch mã quét thành ASCII, nhập ký tự ASCII vào bộ đệm đầu vào, buf[N],

Hình 3.4 Các bảng ánh xạ chính


Machine Translated by Google

Trình điều khiển bàn phím 3.5 63

Hình 3.5 Tổ chức driver KBD

và thông báo cho nửa trên của char đầu vào. Khi phía chương trình cần một char đầu vào, nó gọi getc() của trình điều khiển nửa
trên, cố gắng lấy char từ buf[N]. Chương trình chờ nếu không có char trong buf[N]. Biến điều khiển, dữ liệu, được sử dụng để
đồng bộ hóa bộ xử lý ngắt và chương trình chính. Việc lựa chọn biến điều khiển phụ thuộc vào công cụ đồng bộ hóa được sử dụng.
Phần sau đây hiển thị mã C của trình điều khiển KBD đơn giản. Trình điều khiển chỉ xử lý các phím chữ thường. Việc mở rộng
trình điều khiển để xử lý chữ hoa và các phím đặc biệt được coi là một bài tập trong phần Vấn đề.

3.5.4 Chương trình điều khiển bàn phím

Chương trình mẫu C3.2 trình bày trình điều khiển bàn phím điều khiển ngắt đơn giản. Nó bao gồm các thành phần sau.

(1). tệp t.ld: Tệp tập lệnh liên kết giống như trong C3.1.
(2). Tệp ts.s: Tệp ts.s gần giống như trong C3.1, ngoại trừ việc nó thêm các chức năng khóa và mở khóa để bật/tắt các ngắt IRQ.

/********* tập tin ts.s *************/

.chữ

.code 32

.thiết lập lại toàn cầu, vectơ_start, vectơ_end

khóa .global, mở khóa

reset_handler://điểm vào của chương trình

// đặt ngăn xếp SVC

LDR sp, =svc_stack_top

// sao chép bảng vectơ vào địa chỉ 0

Bản sao BL_vector

// chuyển sang chế độ IRQ để đặt ngăn xếp IRQ

MSR cpsr, #0x92

LDR sp, =irq_stack_top // quay lại

chế độ SVC khi bật ngắt IRQ

MSR cpsr, #0x13

// gọi main() ở chế độ SVC

BL chính

B .

irq_handler:

phụ lr, lr, #4

stmfd sp!, {r0-r12, lr} // xếp chồng TẤT CẢ các thanh ghi

bl IRQ_handler // gọi IRQ_hanler() trong C

ldmfd sp!, {r0-r3, r12, pc}^ // trả về

kho a: // che dấu các ngắt IRQ

MRS r0, cpsr

ORR r0, r0, #0x80 // đặt bit I có nghĩa là CHE KHẮC các ngắt IRQ

MSR cpsr, r0

di chuyển máy tính, lr


Machine Translated by Google

64 3 Xử lý ngắt và ngoại lệ

mở khóa: // mặt nạ trong các ngắt IRQ

MRS r0, cpsr

BIC r0, r0, #0x80 // clr I bit có nghĩa là MASK trong các ngắt IRQ

MSR cpsr, r0

di chuyển máy tính, lr

vectơ_start:

// bảng vectơ: giống như trước

vectơ_end:

(3). tc: Tệp này triển khai các hàm main(), copy_vector() và IRQ_handler(). Để đơn giản, chúng ta sẽ chỉ sử dụng bộ đếm thời gian0
để hiển thị một chiếc đồng hồ treo tường.

/*************** tập tin tc *************/

#include "định nghĩa.h"

#include "string.c"

void time_handler();

#include "kbd.c"

#include "timer.c"

#include "vid.c"

#include "ngoại lệ.c"

void copy_vectors(){ // giống như trước }

khoảng trống IRQ_handler() // Trình xử lý ngắt IRQ trong C

// đọc các thanh ghi trạng thái VIC để tìm ra ngắt nào

int vicstatus = VIC_STATUS;

// BIT trạng thái VIC: time0,1=4, uart0=13, uart1=14

nếu (vicstatus & (1<<4)){ // bit4=1:timer0,1

bộ đếm thời gian_handler(0); // chỉ hẹn giờ0

if (vicstatus & (1<<31)){ // PIC.bit31= Ngắt SIC if (sicstatus & (1<<3)){ // SIC.bit3 =

Ngắt KBD

kbd_handler();

int chính()

int tôi;

dòng char[128];

màu = ĐỎ; // int màu trong file vid.c

fbuf_init(); // khởi tạo màn hình LCD /* kích hoạt ngắt VIC:

time0 tại IRQ3, SIC tại IRQ31 */

VIC_INTENABLE = 0;

VIC_INTENABLE |= (1<<4); VIC_INTENABLE // bộ đếm thời gian0,1 tại PIC.bit4

|= (1<<5); VIC_INTENABLE |= (1<<31); // // bộ đếm thời gian2,3 tại PIC.bit5

SIC tới PIC.bit31

/* bật KBD IRQ trên SIC */

SIC_INTENABLE = 0;

SIC_INTENABLE |= (1<<3); // KBD int=SIC.bit3

bộ đếm thời gian_init(); // khởi tạo bộ đếm thời gian

bộ đếm thời gian_start(0); // bắt đầu hẹn giờ0


Machine Translated by Google

Trình điều khiển bàn phím 3.5 65

kbd_init(); // khởi tạo trình điều khiển bàn phím

printf("Khởi động C3.2: kiểm tra trình điều khiển KBD và TIMER\n");

trong khi(1){

màu = CYAN;

printf("Nhập một dòng từ KBD\n"); kgets(dòng);

printf("line =

%s\n", line);

Để sử dụng các ngắt, các thiết bị phải được cấu hình để tạo ra các ngắt. Điều này được thực hiện trong mã khởi tạo thiết bị,
trong đó mỗi thiết bị được khởi tạo với các ngắt được kích hoạt. Ngoài ra, cả Bộ điều khiển ngắt chính và phụ (PIC và SIC) phải
được cấu hình để cho phép ngắt thiết bị. Bàn phím ngắt ở IRQ3 trên SIC, được chuyển đến IRQ31 trên PIC. Để kích hoạt ngắt bàn
phím, cả bit 3 của thanh ghi SIC_INTENABLE và bit 31 của thanh ghi VIC_INTENABLE phải được đặt thành 1.

Những điều này được thực hiện trong main(). Sau khi khởi tạo các thiết bị cho các ngắt, chương trình chính sẽ thực hiện vòng lặp while(1), trong

nó sẽ nhắc nhập dòng đầu vào từ KBD và in dòng đó ra màn hình LCD.

(4). Tệp kbd.c: Tệp kbd.c triển khai trình điều khiển bàn phím đơn giản.

/********* tập tin kbd.c *******************/

#include "sơ đồ bàn phím"

/******** Độ lệch byte đăng ký KBD; cho char *cơ sở *****/

#define KCNTL 0x00 // 7-6- 5(0=AT)4=RxIntEn 3=TxIntEn

#define KSTAT 0x04 // 7-6=TxE 5=TxBusy 4=RXFull 3=RxBusy

#define KDATA 0x08 // thanh ghi dữ liệu;

#define KCLK 0x0C // thanh ghi chia đồng hồ; (không được sử dụng)

#define KISTA 0x10 // thanh ghi trạng thái ngắt;(không được sử dụng)

typedef dễ bay hơi struct kbd{ // base = 0x10006000

char *cơ sở; // địa chỉ cơ sở của KBD, dưới dạng char *

char buf[128]; // bộ đệm đầu vào

int đầu, đuôi, dữ liệu, phòng; // biến điều khiển

}KBD;

KBD kbd dễ bay hơi; // Cấu trúc dữ liệu KBD

int kbd_init()

KBD *kp = &kbd; kp->base

= (char *)0x10006000; *(kp->cơ sở+KCNTL) =

0x14; // 00010100=INTenable, Bật *(kp->base+KCLK) = 8;

// Hướng dẫn sử dụng PL051 cho biết giá trị từ 0 đến 15

kp->dữ liệu = 0; kp->phòng = 128; // quầy

kp->đầu = kp->đuôi = 0; // lập chỉ mục vào bộ đệm

khoảng trống kbd_handler() // Trình xử lý ngắt KBD trong C

mã u8, c;

int tôi;

KBD *kp = &kbd;

màu = ĐỎ; // int màu trong file vid.c

scode = *(kp->base+KDATA); // đọc mã quét trong thanh ghi dữ liệu

nếu (mã & 0x80) // bỏ qua các bản phát hành chính
Machine Translated by Google

66 3 Xử lý ngắt và ngoại lệ

trở lại;

c = unsh[scode]; // ánh xạ mã quét tới ASCII

if (c != '\r')

printf("kbd ngắt: c=%x %c\n", c, c); kp->buf[kp->head++] = c; //

nhập key vào CIRCULAR buf[ ]

kp->đầu %= 128;

kp->dữ liệu++; kp->phòng--; // cập nhật bộ đếm

int kgetc() // chương trình chính gọi kgetc() để trả về một char

ký tự c;

KBD *kp = &kbd;

mở khóa(); // kích hoạt ngắt IRQ

while(kp->data <= 0); // chờ dữ liệu; CHỈ ĐỌC

kho a(); // vô hiệu hóa các ngắt IRQ

c = kp->buf[kp->tail++];// lấy ac và cập nhật chỉ số đuôi kp->tail %= 128; kp->dữ liệu--; kp-

>phòng++; // cập nhật với

các ngắt TẮT

mở khóa(); // kích hoạt ngắt IRQ

trả lại c;

int kgets(char s[ ]) // lấy một chuỗi từ KBD

ký tự c;

while((c=kgetc()) != '\r'){

*s++ = c;

*s = 0;

trả về (các) strlen;

(5). Trình tự xử lý ngắt

(5).1. Khi xảy ra ngắt, CPU tuân theo vectơ IRQ ở 0x18 để nhập

irq_handler:

phụ lr, lr, #4 // điều chỉnh lr

stmfd sp!, {r0-r12, lr} // lưu reg trong ngăn xếp IRQ

bl IRQ_handler // gọi IRQ_hanler() trong C

ldmfd sp!, {r0-r12, pc}^ // trở lại

irq_handler trước tiên trừ 4 từ thanh ghi liên kết lr_irq. lr được điều chỉnh là địa chỉ trả lại chính xác đến điểm bị
gián đoạn. Nó đẩy các thanh ghi r0–r12 và lr vào ngăn xếp IRQ. Sau đó, nó gọi IRQ_handler() trong C. Khi trở về từ
IRQ_handler(), nó sẽ bật ngăn xếp, tải PC với lr đã lưu và cũng khôi phục SPSR, khiến điều khiển quay trở lại điểm gián
đoạn ban đầu. Để tăng tốc độ xử lý ngắt, irq_handler chỉ có thể lưu các thanh ghi phải được bảo toàn.
Vì trình xử lý ngắt của chúng ta được viết bằng C, không phải bằng hợp ngữ, nên chỉ cần lưu r0–r3, r12 (con trỏ khung ngăn xếp) và lr

(thanh ghi liên kết).

(5).2. IRQ_handler(): IRQ_handler() trước tiên đọc các thanh ghi trạng thái của cả PIC và SIC. Bộ xử lý ngắt phải quét
các bit của thanh ghi trạng thái để xác định nguồn ngắt. Mỗi bit = 1 trong thanh ghi trạng thái biểu thị một ngắt hoạt động.
Thứ tự quét phải tuân theo thứ tự ưu tiên ngắt, tức là từ bit 0 đến bit 31.
Machine Translated by Google

Trình điều khiển bàn phím 3.5 67

nạn nhân = VIC_STATUS;

sicstatus = SIC_STATUS;

// BIT trạng thái VIC: time0=4, uart0=13, uart1=14, SIC=31: KBD lúc 3

nếu (vicstatus & (1<<4)) // bit 4 => ngắt bộ định thời

bộ đếm thời gian_handler(0);

if (vicstatus & (1<<31)){ // SIC ngắt=bit_31=>KBD ở bit 3

if (sicstatus & (1<<3)){ // Bit SIC 3 => Ngắt KBD

kbd_handler();

(5).3. kbd_handler(): kbd_handler() đọc mã quét từ thanh ghi dữ liệu KBD, xóa mã ngắt KBD. Nó bỏ qua mọi thao tác nhả phím, vì vậy
trình điều khiển chỉ có thể xử lý các phím chữ thường và không có phím đặc biệt nào cả. Đối với mỗi phím được nhấn, nó sẽ in thông
báo "phím ngắt kbd" lên màn hình LCD. Như đã lưu ý trước đó, chúng tôi đã điều chỉnh hàm printf() chung để in được định dạng cho
màn hình LCD. Sau đó, nó ánh xạ mã quét tới một ký tự ASCII (chữ thường) và nhập ký tự đó vào bộ đệm đầu vào. Biến điều khiển, dữ
liệu, biểu thị số lượng ký tự trong bộ đệm đầu vào. (5).4. Hàm kgetc() và kgets(): kgetc() dùng
để lấy char đầu vào từ bàn phím. kgets() là để nhận dòng đầu vào kết thúc bằng phím \r. Trình điều khiển KBD đơn giản chủ yếu nhằm
minh họa nguyên tắc thiết kế của trình điều khiển thiết bị đầu vào được điều khiển bằng ngắt. Các biến đệm và điều khiển của trình
điều khiển tạo thành một vùng quan trọng vì chúng được truy cập bởi cả chương trình chính và trình xử lý ngắt KBD. Khi trình xử lý
ngắt thực thi, chương trình chính về mặt logic sẽ không được thực thi. Vì vậy chương trình chính không thể can thiệp vào bộ xử lý
ngắt. Tuy nhiên, trong khi chương trình chính thực thi, các ngắt có thể xảy ra khiến chương trình chuyển hướng sang thực thi trình
xử lý ngắt, điều này có thể gây trở ngại cho chương trình chính. Vì lý do này, khi một chương trình gọi kgetc(), chương trình có
thể sửa đổi các biến dùng chung trong trình điều khiển, nó phải che giấu các ngắt để ngăn xảy ra ngắt bàn phím. Trong kgetc(),
chương trình chính trước tiên sẽ kích hoạt các ngắt, đây là tùy chọn nếu chương trình đang chạy với các ngắt được kích hoạt. Sau
đó, nó lặp cho đến khi biến kp->data khác 0, nghĩa là có các ký tự trong bộ đệm đầu vào. Sau đó, nó vô hiệu hóa các ngắt, lấy char
từ bộ đệm đầu vào và cập nhật các biến dùng chung.
Đoạn mã đảm bảo các biến được chia sẻ chỉ có thể được cập nhật bởi một thực thể thực thi tại một thời điểm, thường được gọi là
Vùng quan trọng hoặc Phần quan trọng (Silberschatz và cộng sự 2009; Stallings 2011; Wang 2016). Cuối cùng, nó cho phép ngắt và trả
về một ký tự. Trên CPU ARM, không thể chỉ che giấu các ngắt bàn phím. Thao tác lock() che giấu tất cả các ngắt IRQ, việc này hơi
quá mức cần thiết nhưng nó hoàn thành công việc. Ngoài ra, chúng tôi có thể ghi vào thanh ghi điều khiển của bàn phím để tắt/bật
rõ ràng các ngắt bàn phím. Điểm bất lợi là nó phải truy cập vào các vị trí được ánh xạ bộ nhớ, tốc độ này chậm hơn nhiều so với
việc che giấu các ngắt thông qua thanh ghi CPSR của CPU.

Hình 3.6 hiển thị trình điều khiển KBD sử dụng các ngắt. Như hình minh họa, chương trình chính chỉ in các dòng hoàn chỉnh, nhưng
mỗi phím đầu vào tạo ra một ngắt và in thông báo "ngắt kbd", cùng với ký tự đầu vào trong ASCII.

Trình điều khiển UART 3.6

3.6.1 Giao diện UART ARM PL011

Bo mạch đa năng ARM hỗ trợ bốn thiết bị PL011 UART cho I/O nối tiếp (ARM PL011 2005). Mỗi thiết bị UART có một địa chỉ cơ sở trong
bản đồ bộ nhớ hệ thống.

UART0: 0x101F1000

UART1: 0x101F2000

UART2: 0x101F3000

UART3: 0x10009000
Machine Translated by Google

68 3 Xử lý ngắt và ngoại lệ

Hình 3.6 Trình điều khiển KBD sử dụng ngắt

3 UART đầu tiên, UART0 đến UART2 nằm liền kề nhau trong bản đồ bộ nhớ hệ thống. Chúng ngắt ở IRQ12 đến IRQ14 trên

PIC chính. UART4 nằm ở 0x10000900 và nó bị gián đoạn ở IRQ6 trên SIC. Nói chung, UART phải được khởi tạo

bằng các bước sau.

(1). Viết giá trị chia vào thanh ghi tốc độ baud để có tốc độ truyền mong muốn. Danh sách hướng dẫn tham khảo kỹ thuật ARM PL011

các giá trị chia số nguyên sau (dựa trên đồng hồ UART 7,38 MHz) cho tốc độ truyền thường được sử dụng:

0x4=1152000, 0xC=38400, 0x18=192000, 0x20=14400, 0x30=9600

(2). Ghi vào thanh ghi Line Control để chỉ định số bit trên mỗi char và chẵn lẻ, ví dụ 8 bit trên mỗi char không có chẵn lẻ, v.v.

(3). Ghi vào thanh ghi Mặt nạ ngắt để bật/tắt các ngắt RX và TX

Khi sử dụng bo mạch ARM Versatilepb mô phỏng trong QEMU, có vẻ như QEMU tự động sử dụng các giá trị mặc định

đối với cả tham số điều khiển tốc độ truyền và đường truyền, thực hiện các bước (1) và (2) là tùy chọn hoặc không cần thiết. Trong thực tế, nó được quan sát

việc ghi bất kỳ giá trị nào vào thanh ghi số nguyên (0x24) sẽ có tác dụng, nhưng người đọc nên biết rằng đây không phải là chuẩn mực

cho UART trong hệ thống thực. Trong trường hợp này chúng ta chỉ cần lập trình thanh ghi Interrupt Mask (nếu sử dụng ngắt) và kiểm tra

thanh ghi cờ trong quá trình I/O nối tiếp.

3.6.2 Thanh ghi UART

Mỗi giao diện UART chứa một số thanh ghi 32 bit. Các thanh ghi UART quan trọng nhất là

0ffset Tên Chức năng

------ -------- -----------------------------

0x00 UARTDR thanh ghi dữ liệu: để đọc/ghi ký tự

0x04 UARTSR nhận trạng thái/xóa lỗi

0x18 UARTFR TxEmpty, RxFull, v.v.

0x24 UARIBRD đặt tốc độ truyền

0x2c Thanh ghi điều khiển dòng UARTLCR

0x30 UARTCR thanh ghi điều khiển

0x38 Mặt nạ ngắt UARTIMSC cho TX và RX

0x40 Trạng thái ngắt UARTMIS


Machine Translated by Google

Trình điều khiển UART 3.6 69

Một số thanh ghi UART đã được giải thích trong chương trình C2.3 của Chương. 2. Ở đây chúng ta sẽ tập trung vào các thanh ghi
liên quan đến ngắt. Giao diện ARM UART hỗ trợ nhiều loại ngắt. Để đơn giản, chúng ta chỉ xem xét các ngắt Rx (Nhận) và Tx (Truyền)
để truyền dữ liệu và bỏ qua các ngắt đó như trạng thái modem và tình trạng lỗi. Để cho phép ngắt UART Rx và Tx, bit 4 và 5 của thanh
ghi mặt nạ ngắt (UARTIMSC) phải được đặt thành 1. Khi UART ngắt, thanh ghi trạng thái ngắt được che dấu (UARTMIS) chứa nhận dạng
ngắt, ví dụ: bit 4 = 1 nếu đó là ngắt Rx và bit 5 = 1 nếu đó là ngắt Tx. Tùy thuộc vào loại ngắt, bộ xử lý ngắt có thể phân nhánh
tới ISR tương ứng để xử lý ngắt.

3.6.3 Chương trình trình điều khiển UART điều khiển ngắt

Phần này trình bày thiết kế và triển khai trình điều khiển UART điều khiển ngắt cho I/O nối tiếp. Để giữ cho việc phân tích đơn
giản, chúng ta sẽ chỉ sử dụng UART0 và UART1 nhưng mã tương tự cũng có thể áp dụng cho các UART khác. Chương trình trình điều khiển
UART được ký hiệu là C3.3, được tổ chức như sau.

3.6.3.1 Tệp uart.c Tệp này

triển khai trình điều khiển UART bằng cách sử dụng các ngắt. Mỗi UART được đại diện bởi một cấu trúc UART. Nó chứa địa chỉ cơ sở
UART, số đơn vị, bộ đệm đầu vào, bộ đệm đầu ra và các biến điều khiển. Cả hai bộ đệm đều có dạng hình tròn, với con trỏ đầu để nhập
ký tự và con trỏ đuôi để xóa ký tự. Trong số các biến điều khiển, dữ liệu là số ký tự trong bộ đệm và room là số khoảng trống trong
bộ đệm. Đối với đầu ra, txon là cờ cho biết UART đã ở trạng thái truyền hay chưa. Để sử dụng các ngắt, thanh ghi Bộ/Xóa mặt nạ ngắt
UART (IMSC) phải được thiết lập đúng cách. UART hỗ trợ nhiều loại ngắt. Để đơn giản, chúng ta chỉ xem xét các ngắt TX (bit 5) và RX
(bit 4). Trong uart_init(), câu lệnh C

*(lên->cơ sở+IMSC) |= 0x30; // bit 4,5 = 1

đặt bit 4 và 5 của IMSC thành 1, cho phép ngắt TX và RX của UART. Để đơn giản, chúng ta sẽ chỉ sử dụng UART0 và UART1. Ngắt UART0
tại IRQ12 và ngắt UART1 tại IRQ13, cả hai đều trên VIC chính. Để cho phép ngắt UART, bit 12 và 13 của PIC phải được đặt thành 1.
Việc này được thực hiện trong main() bằng câu lệnh

VIC_INTENABLE |= (1<<12); // UART0 tại bit12

VIC_INTENABLE |= (1<<13); // UART1 tại bit13

Điểm đặc biệt của ARM PL011 UART là hỗ trợ bộ đệm FIFO trong phần cứng cho cả hoạt động gửi và nhận. Nó có thể được lập trình để

tăng các ngắt khi bộ đệm FIFO ở các mức khác nhau giữa ĐẦY ĐỦ và EMPTY. Để giữ cho trình điều khiển UART đơn giản, chúng ta sẽ không
sử dụng bộ đệm FIFO phần cứng. Họ bị vô hiệu hóa bởi
tuyên bố

*(lên->cơ sở+CNTL) &= ~0x10; // vô hiệu hóa UART FIFO

Điều này làm cho UART hoạt động ở chế độ char đơn. Ngoài ra, ngắt TX chỉ được kích hoạt sau khi ghi char vào thanh ghi dữ liệu.
Khi hệ thống không còn đầu ra cho UART nữa, nó phải tắt ngắt TX. Phần sau đây hiển thị mã trình điều khiển uart.c.

/*************** tệp uart.c ****************/

#xác định UDR 0x00

#define UDS 0x04

#xác định UFR 0x18

#define CNTL 0x2C

#define IMSC 0x38

#define MIS 0x40

#define SBUFSIZE 128


Machine Translated by Google

70 3 Xử lý ngắt và ngoại lệ

typedef cấu trúc dễ bay hơi uart {

char *cơ sở; // địa chỉ cơ sở; như ký tự *

int n; // số uart 0-3

char inbuf[SBUFSIZE]; int

indata, inroom, inhead, intail;

char outbuf[SBUFSIZE];

int outdata, outroom, outhead, outtail;

int txon dễ bay hơi; // 1=Ngắt TX đang bật

}UART;

UART uart[4]; // 4 cấu trúc UART

int uart_init()

int tôi; UART *lên;

for (i=0; i<4; i++){ // uart0 đến uart2 liền kề

lên = &uart[i];

up->base = (char *)(0x101F1000 + i*0x1000);

*(lên->cơ sở+CNTL) &= *0x10; // vô hiệu hóa UART FIFO

*(lên->cơ sở+IMSC) |= 0x30;

lên->n = tôi; Số ID UART

up->indata = up->inhead = up->intail = 0;

lên->trong phòng = SBUFSIZE;

lên->outdata = up->outhead = up->outtail = 0;

lên->phòng ngoài = SBUFSIZE;

lên->txon = 0;

uart[3].base = (char *)(0x10009000); // uart3 tại 0x10009000

void uart_handler(UART *up)

u8 mis = *(up->base + MIS); // đọc thanh ghi MIS if (mis & (1<<4)) //

MIS.bit4=RX ngắt do_rx(up); if (mis & (1<<5)) // MIS.bit5=TX

ngắt do_tx(up);

int do_rx(UART *up) // Trình xử lý ngắt RX

ký tự c;

c = *(up->base+UDR);

printf("ngắt rx: %c\n", c);

nếu (c==0xD)

printf("\n");

up->inbuf[up->inhead++] = c;

lên->đầu vào %= SBUFSIZE;

lên->indata++; lên->trong phòng--;

int do_tx(UART *up) // xử lý ngắt TX

ký tự c;

printf("Ngắt TX\n"); if (up->outdata

<= 0){ // if outbuf[ ] trống *(up->base+MASK) = 0x10; // vô hiệu

hóa ngắt TX up->txon = 0; // tắt cờ txon

trở lại;

}
Machine Translated by Google

Trình điều khiển UART 3.6 71

c = up->outbuf[up->outtail++];

lên->outtail %= SBUFSIZE; *(up-

>base+UDR) = (int)c; // ghi c vào DR

up->outdata--; lên->phòng ngoài++;

int ugetc(UART *up) // trả về một char từ UART

ký tự c;

while(up->indata <= 0); // vòng lặp cho đến khi up->data > 0 READONLY

c = up->inbuf[up->intail++];

lên->intail %= SBUFSIZE; // cập

nhật biến: phải tắt các ngắt

kho a();

lên->indata--; lên->trong phòng++;

mở khóa();

trả lại c;

int uputc(UART *up, char c) // xuất một char thành UART

kprintf("uputc %c", c); if (up->txon)

{ // nếu TX được bật, hãy nhập c vào outbuf[]

up->outbuf[up->outhead++] = c;

lên->đầu ra %= 128;

kho a();

lên-> dữ liệu lỗi ++; lên->phòng ngoài--;

mở khóa();

trở lại;

// txon==0 có nghĩa là TX bị tắt => xuất ra c & kích hoạt ngắt TX

// PL011 TX chỉ bị gian lận nếu ghi char, nếu không thì không bị ngắt TX

int i = *(up->base+UFR); // đọc FR

while( *(up->base+UFR) & 0x20 ); // lặp trong khi FR=TXF

*(up->base+UDR) = (int)c; // ghi c vào DR

UART0_IMSC |= 0x30; // 0000 0000: bit5=Mặt nạ TX bit4=Mặt nạ RX

lên->txon = 1;

int uget(UART *up, char *s) // lấy một dòng từ UART

kprintf("%s", "in ugets: "); while ((*s =

(char)ugec(up)) != '\r'){ uputc(up, *s++);

*s = 0;

int up(UART *up, char *s) // in một dòng ra UART

trong khi(*s)

uputc(up, *s++);

}
Machine Translated by Google

72 3 Xử lý ngắt và ngoại lệ

3.6.3.2 Giải thích về Mã trình điều khiển UART

(1). uart_handler(UART *up): Trình xử lý ngắt uart đọc thanh ghi MIS để xác định loại ngắt. Đó là ngắt RX nếu bit 4 của MIS
là 1. Đó là ngắt TX nếu bit 5 của MIS là 1. Tùy thuộc vào loại ngắt, nó gọi do_rx() hoặc do_tx() để xử lý ngắt. (2). do_rx():
Đây là ngắt đầu vào. Nó
đọc char ASCII từ thanh ghi dữ liệu, xóa ngắt RX của UART. Sau đó, nó nhập char vào bộ đệm đầu vào hình tròn và tăng biến dữ
liệu lên 1. Biến dữ liệu biểu thị số lượng ký tự trong bộ đệm đầu vào. (3). do_tx(): Đây là một ngắt đầu ra, được kích hoạt
bằng cách ghi char cuối cùng vào thanh
ghi dữ liệu đầu ra và quá trình truyền char đó đã kết thúc. Trình xử lý kiểm tra xem có bất kỳ ký tự nào trong bộ đệm đầu ra
hay không. Nếu bộ đệm đầu ra trống, nó sẽ vô hiệu hóa ngắt UART TX và trả về. Nếu ngắt TX không bị vô hiệu hóa, nó sẽ gây ra
chuỗi ngắt TX vô hạn. Nếu bộ đệm đầu ra không trống, nó sẽ lấy một char từ bộ đệm và ghi nó vào thanh ghi dữ liệu để truyền

ngoài.

(4). ugetc(): ugetc dành cho chương trình chính để lấy char từ cổng UART. Logic và đồng bộ hóa của nó với bộ xử lý ngắt RX
giống như kgetc() của trình điều khiển bàn phím. Vì thế chúng ta sẽ không lặp lại chúng ở đây nữa. (5).
uputc(): uputc() dành cho chương trình chính xuất char sang cổng UART. Nếu cổng UART không truyền (cờ txon tắt), nó sẽ ghi
char vào thanh ghi dữ liệu, cho phép ngắt TX và đặt cờ txon. Ngược lại, nó nhập char vào bộ đệm đầu ra, cập nhật biến dữ liệu
và trả về. Trình xử lý ngắt TX sẽ xuất các ký tự từ bộ đệm đầu ra trên mỗi lần ngắt liên tiếp.

(6). In có định dạng: uprintf(UART *up, char *fmt,…) là để in được định dạng sang cổng UART. Nó dựa trên uputc().

3.6.3.3 Trình diễn trình điều khiển KBD và UART Tệp tc

chứa hàm IRQ_handler() và hàm main(). Hàm main() trước tiên khởi tạo các thiết bị UART và VIC để phát hiện các ngắt. Sau
đó, nó kiểm tra trình điều khiển UART bằng cách cấp I/O nối tiếp trên các thiết bị đầu cuối UART. Đối với mỗi ngắt IRQ,
IRQ_handler() xác định nguồn ngắt và gọi trình xử lý thích hợp để xử lý ngắt. Để rõ ràng, mã liên quan đến UART được
hiển thị bằng các dòng in đậm.

/*************** C3.3. tập tin tc *******************/

#include "định nghĩa.h"

#include "string.c"

#include "uart.c" // Trình điều khiển UART

#include "kbd.c" // trình điều khiển bàn phím

#include "timer.c" // hẹn giờ

#include "vid.c" // Trình điều khiển màn hình LCD

#include "ngoại lệ.c"

void copy_vectors(void){ // giống như trước }

khoảng trống IRQ_handler() // Trình xử lý ngắt IRQ trong C

// đọc các thanh ghi trạng thái VIC để tìm ra ngắt nào

int vicstatus = VIC_STATUS;

// BIT trạng thái VIC: time0,1=4,uart0=13,uart1=14

nếu (vicstatus & (1<<5)){ // bit5:timer2,3

if (*(tp[2]->base+TVALUE)==0) // bộ đếm thời gian 2

bộ đếm thời gian_handler(2);

if (*(tp[3]->base+TVALUE)==0) // bộ đếm thời gian 3

bộ đếm thời gian_handler(3);

nếu (vicstatus & (1<<4)){ // bit4=1:timer0,1

bộ đếm thời gian_handler(0);

nếu (vicstatus & (1<<12)) // bit12=1: uart0

uart_handler(&uart[0]);
Machine Translated by Google

Trình điều khiển UART 3.6 73

nếu (vicstatus & (1<<13)) // bit13=1: uart1

uart_handler(&uart[1]);

if (vicstatus & (1<<31)){ // PIC.bit31= Ngắt SIC if (sicstatus & (1<<3)){ // SIC.bit3

= Ngắt KBD

kbd_handler();

int chính()

dòng char[128];

UART *lên;

KBD *kp;

fbuf_init(); // khởi tạo màn hình LCD printf("Khởi động C3.4:

kiểm tra trình điều khiển KBD TIMER UART\n");

/* kích hoạt ngắt hẹn giờ0,1, uart0,1 SIC */

VIC_INTENABLE = 0;

VIC_INTENABLE |= (1<<4); // bộ đếm thời gian0,1 ở bit4

VIC_INTENABLE |= (1<<5); // bộ đếm thời gian2,3 ở bit5

VIC_INTENABLE |= (1<<12); // UART0 tại bit12

VIC_INTENABLE |= (1<<13); // UART1 tại bit13

VIC_INTENABLE |= (1<<31); // SIC tới IRQ31 của VIC

SIC_INTENABLE = 0;

SIC_INTENABLE |= (1<<3); // KBD int=bit3 trên SIC kbd_init();

// khởi tạo bàn phím

uart_init(); // khởi tạo UART

lên = &uart[0]; // kiểm tra I/O UART0 kp = &kbd;

bộ đếm thời gian_init();

bộ đếm thời gian_start(0); // chỉ hẹn giờ0

trong khi(1){

kprintf("Nhập một dòng từ KBD\n");

kgets(dòng);

kprintf("Nhập một dòng từ UART0\n");

uprints("Nhập một dòng từ UARTS\n\r");

ugets(lên, dòng);

uprintf("%s\n", dòng);

Hình 3.7 cho thấy các đầu ra của trình điều khiển KBD và UART điều khiển ngắt. Hình vẽ cho thấy đầu vào UART là do ngắt
rx và đầu ra là do ngắt tx.

3.7 Thẻ kỹ thuật số an toàn (SD)

Đối với hầu hết các hệ thống nhúng, thiết bị lưu trữ dung lượng lớn chính là thẻ Kỹ thuật số an toàn (SD) (SDC 2016) do kích
thước nhỏ gọn, mức tiêu thụ điện năng thấp và khả năng tương thích với các loại thiết bị di động khác. Nhiều hệ thống nhúng
có thể không có bất kỳ thiết bị lưu trữ dung lượng lớn nào để cung cấp hỗ trợ hệ thống tệp nhưng chúng thường khởi động từ
thẻ nhớ flash hoặc thẻ SD. Một ví dụ điển hình là Raspberry Pi (Raspberry_Pi 2016). Nó yêu cầu thẻ SD để khởi động hệ điều
hành, thường là phiên bản Linux, được gọi là Raspbian, được điều chỉnh cho phù hợp với kiến trúc ARM. Hầu hết các hệ thống
dựa trên ARM bao gồm giao diện thẻ đa phương tiện ARM PrimeCell PL180/PL181 (ARM PL180 1998; ARM PL181 2001) để cung cấp hỗ trợ cho
Machine Translated by Google

74 3 Xử lý ngắt và ngoại lệ

Hình 3.7 Trình diễn trình điều khiển KBD và UART

cả thẻ đa phương tiện và thẻ SD. Máy ảo ARM Versatilepb được mô phỏng trong QEMU cũng bao gồm giao diện đa phương tiện PL180 nhưng nó

chỉ hỗ trợ thẻ SD.

3.7.1 Giao thức thẻ SD

Giao thức thẻ SD đơn giản nhất là Giao diện ngoại vi nối tiếp (SPI) (SPI 2016). Giao thức SPI yêu cầu máy chủ

có cổng SPI, cổng này có sẵn trong nhiều hệ thống dựa trên ARM. Đối với máy chủ không có cổng SPI, thẻ SD phải sử dụng

giao thức SD gốc [thông số SD 2016], có khả năng cao hơn và do đó phức tạp hơn giao thức SPI.

Giao diện đa phương tiện của QEMU hỗ trợ thẻ SD ở chế độ gốc nhưng không hỗ trợ ở chế độ SPI. Vì lý do này, chúng tôi sẽ phát triển một SD

trình điều khiển thẻ hoạt động ở chế độ SD gốc.

3.7.2 Trình điều khiển thẻ SD

Chương trình mẫu C3.4 triển khai trình điều khiển thẻ SD điều khiển ngắt. Nó thể hiện trình điều khiển SD bằng cách ghi vào

các khu vực của thẻ SD và sau đó đọc lại các khu vực đó để xác minh kết quả. Chương trình bao gồm các thành phần sau.

(1). sdc.h: Tệp tiêu đề này xác định các thanh ghi và mặt nạ bit của Thẻ đa phương tiện PL180 (MMC). Để cho ngắn gọn, chúng tôi

chỉ hiển thị các thanh ghi PL180.

/** PL180 đăng ký từ BASE **/

#xác định SỨC MẠNH 0x00

#xác định ĐỒNG HỒ 0x04

#xác định LUẬN LUẬN 0x08

#xác định LỆNH 0x0C

#define PHẢN HỒI 0x10

#xác định PHẢN HỒI0 0x14

#xác định PHẢN HỒI1 0x18


Machine Translated by Google

3.7 Thẻ kỹ thuật số an toàn (SD) 75

#xác định TRẢ LỜI2 0x1C

#xác định PHẢN HỒI3 0x20

#define DATATIMER 0x24

#xác định ĐỘ DỮ LIỆU 0x28

#define DATACTRL 0x2C

#define DATACOUNT 0x30

#xác định TÌNH TRẠNG 0x34

#define STATUS_CLEAR 0x38

#xác định MẶT NẠ0 0x3C

#xác định MASK1 0x40

#define CARD_SELECT 0x44

#xác định FIFO_COUNT 0x48

#xác định FIFO 0x80

/** thêm thanh ghi ID KHÔNG ĐƯỢC SỬ DỤNG **/

(2). Tệp sdc.c: Tệp này triển khai trình điều khiển SDC.

/************ sdc.c Trình điều khiển ARM PL180 SDC **********/

#include "sdc.h"

cơ sở u32;

#define printf kprintf

int delay(){ int i; cho (i=0; i<100; i++); }

int do_command(int cmd, int arg, int resp)

*(u32 *)(cơ sở + ARGUMENT) = (u32)arg;

*(u32 *)(cơ sở + LỆNH) = 0x400 | (resp<<6) | cmd;

trì hoãn();

int sdc_init()

u32 RCA = (u32)0x45670000; // RCA mã hóa cứng của QEMU

căn cứ = (u32)0x10005000; // Địa chỉ cơ sở PL180

printf("sdc_init : ");

*(u32 *)(cơ sở + POWER) = (u32)0xBF; // bật nguồn

*(u32 *)(cơ sở + CLOCK) = (u32)0xC6; // CLK mặc định

// gửi chuỗi lệnh init

do_command(0, 0, MMC_RSP_NONE);// trạng thái rảnh

do_command(55, 0, MMC_RSP_R1); // trạng thái sẵn sàng

do_command(41, 1, MMC_RSP_R3); // đối số không được bằng 0

do_command(2, 0, MMC_RSP_R2); // hỏi CID thẻ

do_command(3, RCA, MMC_RSP_R1); // gán RCA

do_command(7, RCA, MMC_RSP_R1); // trạng thái truyền: phải sử dụng RCA

do_command(16, 512, MMC_RSP_R1); // thiết lập độ dài khối dữ liệu

// đặt bit thanh ghi MASK0 ngắt = RxAvail|TxEmpty

*(u32 *)(cơ sở + MASK0) = (1<<21)|(1<<18); //0x00240000;

printf("xong\n");

// các biến được chia sẻ giữa trình điều khiển SDC và trình xử lý ngắt

char dễ bay hơi *rxbuf, *txbuf;


Machine Translated by Google

76 3 Xử lý ngắt và ngoại lệ

biến động int rxcount, txcount, rxdone, txdone; int sdc_handler()

trạng thái u32, lỗi;

int tôi;

u32 *lên;

// đọc thanh ghi trạng thái để tìm ra RxDataAvail hoặc TxBufEmpty

trạng thái = *(u32 *)(cơ sở + TÌNH TRẠNG);

if (status & (1<<21)){ // RxDataAvail: đọc dữ liệu

printf("Ngắt SDC RX: ); up = (u32 *)rxbuf;

err = trạng thái & (DCRCFAIL

| DTIMEOUT | RXOVERR);

while (!err && rxcount) {

printf("R%d", rxcount); *(up) = *(u32

*)(base + FIFO);

lên++;

rxcount -= sizeof(u32);

trạng thái = *(u32 *)(cơ sở + TÌNH TRẠNG);

lỗi = trạng thái & (DCRCFAIL | DTIMEOUT | RXOVERR);

rxdone = 1;

else if (status & (1<<18)){ // TxBufEmpty: gửi dữ liệu

printf("Ngắt SDC TX: );

up = (u32 *)txbuf;

status_err = trạng thái & (DCRCFAIL | DTIMEOUT);

while (!status_err && txcount) {

printf("W%d", txcount);

*(u32 *)(base + FIFO) = *up;

lên++;

txcount -= sizeof(u32);

trạng thái = *(u32 *)(cơ sở + TÌNH TRẠNG);

status_err = trạng thái & (DCRCFAIL | DTIMEOUT);

txdone = 1;

//printf("ghi để xóa thanh ghi\n");

*(u32 *)(base + STATUS_CLEAR) = 0xFFFFFFFF;

printf("Trả về bộ xử lý ngắt SDC\n");

int get_sector(int khu vực, char *buf)

u32 cmd, arg;

//printf("get_sector %d %x\n", cung, buf);

rxbuf = buf; rxcount = 512; rxdone = 0;

*(u32 *)(cơ sở + DATATIMER) = 0xFFFF0000;

// ghi data_len vào datalength reg

*(u32 *)(cơ sở + DATALENGTH) = 512;

//printf("dataControl=%x\n", 0x93); // 0x93=|9|0011|=|9|

DMA=0,0=BLOCK,1=Máy chủ<-Card,1=Bật
Machine Translated by Google

3.7 Thẻ kỹ thuật số an toàn (SD) 77

*(u32 *)(cơ sở + DATAACTRL) = 0x93; cmd = 17;


// CMD17 = ĐỌC khối đơn

arg = (ngành*512);

do_command(cmd, arg, MMC_RSP_R1);

while(rxdone == 0);

printf("get_sector return\n");

int put_sector(int khu vực, char *buf)

u32 cmd, arg;

//printf("put_sector %d %x\n", cung, buf);

txbuf = buf; txcount = 512; txdone = 0;

*(u32 *)(cơ sở + DATATIMER) = 0xFFFF0000;

*(u32 *)(cơ sở + DATALENGTH) = 512;

cmd = 24; // CMD24 = Viết khối đơn

arg = (u32)(sector*512);

do_command(cmd, arg, MMC_RSP_R1);

//printf("dataControl=%x\n", 0x91);

// 0x91=|9|0001|=|9|DMA=0,BLOCK=0,0=Máy chủ->Thẻ,1=Bật

*(u32 *)(cơ sở + DATAACTRL) = 0x91; // Máy chủ-> thẻ

while(txdone == 0);

printf("put_sector return\n");

Trình điều khiển SDC bao gồm 4 phần chính được giải thích bên dưới.

(1). sdc_init(): Lệnh này được gọi từ chương trình chính để khởi tạo SDC. Khi ghi trình điều khiển SDC, bước quan trọng
nhất là khởi tạo thẻ SD, bao gồm các bước sau.

. gửi CMD0 để đưa SDC về trạng thái rảnh


. gửi CMD8 để xác định loại SDC và dải điện áp gửi CMD55
. theo sau là CMD41 để đưa SDC về trạng thái sẵn sàng gửi CMD2 để
. nhận Nhận dạng thẻ (CID) gửi CMD3 để lấy/
. đặt Địa chỉ tương đối của thẻ (RCA) ) gửi CMD7 để
. chọn SDC bằng RCA để truyền trạng thái dữ liệu. đặt các bit
. MASK0 của thanh ghi Mặt nạ ngắt để cho phép ngắt Rx và Tx

Nói chung, mọi lệnh ngoại trừ CMD0 đều mong đợi một phản hồi thuộc loại khác nhau. Sau khi gửi lệnh, các phản hồi sẽ
nằm trong thanh ghi phản hồi nhưng chúng bị bỏ qua trong trình điều khiển SD. Người ta quan sát thấy rằng trong PL180
MMC mô phỏng của QEMU, Địa chỉ thẻ tương đối (RCA) được gán cho SDC được mã hóa cứng là 0x4567. Trên thực tế, mỗi lệnh
CMD3 sẽ tăng RCA lên 0x4567. Lý do cho hành vi khá đặc biệt này có lẽ là do lỗi đánh máy trong trình giả lập PL180 của
QEMU. Nó chỉ nên đặt RCA một lần bằng RCA = 0x4567, thay vì RCA += 0x4567, tăng thêm 0x4567 trên mỗi lệnh CMD3. Sau khi
khởi tạo, trình điều khiển có thể cấp CMD17 để đọc khối hoặc CMD24 để ghi khối. Đối với SDC, kích thước khối (sector)
mặc định là 512 byte. SDC cũng hỗ trợ đọc/ghi nhiều cung từ CMD18 và CMD25 tương ứng. Truyền dữ liệu có thể sử dụng một
trong ba sơ đồ khác nhau: bỏ phiếu, ngắt hoặc DMA. DMA phù hợp để truyền lượng lớn dữ liệu. Để giữ cho mã trình điều
khiển đơn giản, trình điều khiển SDC chỉ sử dụng các ngắt chứ không phải DMA để đọc/ghi một cung 512 byte mỗi lần. Nó sẽ
được mở rộng để đọc/ghi nhiều lĩnh vực sau này. Ngắt SDC được kích hoạt bằng cách thiết lập các bit trong thanh ghi Mặt
nạ ngắt MASK0. Trong sdc_init(), các bit mặt nạ ngắt được đặt thành RxDataAvail (bit 21) và TxBufEmpty (bit 18). MMC sẽ
tạo ra ngắt SDC khi có dữ liệu trong bộ đệm đầu vào hoặc có chỗ trống trong bộ đệm đầu ra. Nó vô hiệu hóa và bỏ qua các
loại ngắt SDC khác, ví dụ như ngắt do điều kiện lỗi.
Machine Translated by Google

78 3 Xử lý ngắt và ngoại lệ

(2). get_sector(int Sector, char *buf): get_sector() dùng để đọc một cung 512 byte từ SDC. Thuật toán của get_sector() như sau.

. Đặt rxbuf = buf toàn cầu và rxdone = 0 để trình xử lý ngắt Rx sử dụng; . Đặt
DataTimer thành mặc định, đặt DataLength thành
512; . Đặt DataCntl thành 0x93 (kích thước khối = 2**9, respR1, SDC thành Máy chủ
và Bật); . Gửi CMD17 với đối số = Sector*512 (địa chỉ byte trên SDC); .
(Bận) đợi trình xử lý ngắt Rx đọc xong dữ liệu;

Khi nhận được CMD17, Bộ điều khiển đa phương tiện PL180 (MMC) bắt đầu truyền dữ liệu từ SDC sang bộ đệm đầu vào bên trong
của nó. MMC có bộ đệm dữ liệu đầu vào FIFO 16x32 bit. Khi có dữ liệu, nó sẽ tạo ra ngắt Rx, khiến SDC_handler() được thực thi,
thực tế là chuyển dữ liệu từ MMC sang rxbuf. Sau khi gửi CMD17, chương trình chính bận rộn chờ cờ rxdone dễ thay đổi, cờ này
sẽ được bộ xử lý ngắt đặt khi quá trình truyền dữ liệu hoàn tất.

(3). put_sector(int Sector, char *buf): put_sector() dùng để ghi một khối dữ liệu vào SDC. Thuật toán của put_sector() như sau.

. Đặt txbuf=buf toàn cầu và txdone=0 để xử lý ngắt Tx sử dụng; . Đặt


DataTimer thành mặc định và DataLength thành 512; .
Gửi CMD24 với đối số=sector*512 (địa chỉ byte trên SDC); . Đặt
DataCntl thành 0x91 (kích thước khối=2**9, respR1, Host thành SDC,
Enable); . (Bận) đợi trình xử lý ngắt Tx ghi xong dữ liệu;

Khi nhận được CMD24, PL180 MMC bắt đầu truyền dữ liệu. MMC có bộ đệm dữ liệu đầu ra FIFO 16x32 bit. Nếu bộ đệm Tx trống, nó
sẽ tạo ra ngắt SDC, khiến SDC_handler() được thực thi, thực tế là chuyển dữ liệu từ buf sang MMC. Sau khi gửi CDM24, chương
trình chính bận rộn chờ cờ txdone dễ thay đổi, cờ này sẽ được bộ xử lý ngắt thiết lập khi quá trình truyền dữ liệu hoàn tất.

(4). sdc_handler(): Đây là trình xử lý ngắt SDC. Đầu tiên nó kiểm tra thanh ghi trạng thái để xác định nguồn ngắt. Nếu đó là
ngắt RxDataAvail (bit 21 được đặt), nó sẽ truyền dữ liệu từ bộ điều khiển MMC sang rxbuf theo một vòng lặp.

while (!err && rxcount) {

printf("R%d", rxcount);

*(up) = *(u32 *)(base + FIFO);

lên++;

rxcount -= sizeof(u32);

trạng thái = *(u32 *)(cơ sở + TÌNH TRẠNG);

lỗi = trạng thái & (SDI_STA_DCRCFAIL | SDI_STA_DTIMEOUT |

SDI_STA_RXOVERR);

Loại bỏ bất kỳ lỗi nào, mỗi lần lặp của vòng lặp sẽ đọc một u32 (4 byte) từ bộ đệm đầu vào FIFO của MMC, giảm rxcount đi 4
cho đến khi rxcount đạt 0. Sau đó, nó đặt cờ rxdone thành 1, cho phép chương trình chính trong get_sector() thực hiện Tiếp tục.
Nếu ngắt SDC là ngắt Tx (bit 18 được đặt), nó sẽ ghi dữ liệu từ txbuf vào FIFO của MMC bằng một vòng lặp.

while (!err && txcount){

printf("W%d", txcount);

*(u32 *)(base + FIFO) = *up;

lên++;

txcount -= sizeof(u32);

trạng thái = *(u32 *)(cơ sở + TÌNH TRẠNG);

lỗi = trạng thái & (SDI_STA_DCRCFAIL | SDI_STA_DTIMEOUT);


}
Machine Translated by Google

3.7 Thẻ kỹ thuật số an toàn (SD) 79

Trừ bất kỳ lỗi nào, mỗi lần lặp của vòng lặp sẽ ghi một u32 (4 byte) vào bộ đệm đầu ra FIFO của MMC, giảm txcount đi 4
cho đến khi txcount đạt 0. Sau đó, nó đặt cờ txdone thành 1, cho phép chương trình chính trong put_sector() thực hiện Tiếp tục.

(3). Tệp tc: Tệp tc giống như trong C3.3, ngoại trừ mã được thêm vào để khởi tạo và kiểm tra SDC. Để rõ ràng, các dòng sửa
đổi của tc được hiển thị bằng chữ in đậm.

#include "định nghĩa.h"

#include "string.c" #define

printf kprintf

char *tab = "0123456789ABCDEF";

#include "uart.c"

#include "kbd.c"

#include "timer.c"

#include "vid.c"

#include "ngoại lệ.c"

#include "sdc.c"

void copy_vectors(void) { // giống như trước }

khoảng trống IRQ_handler()

int vicstatus, sicstatus;

int usstatus, kstatus;

// đọc các thanh ghi trạng thái VIC SIV để tìm ra ngắt nào

nạn nhân = VIC_STATUS;

sicstatus = SIC_STATUS;

// BIT trạng thái VIC: time0=4, uart0=13, uart1=14, SIC=31: KBD lúc 3 if (vicstatus & (1<<4)){ //

bit 4: time0

bộ đếm thời gian_handler(0);

if (vicstatus & (1<<12)){ // Bit 12: UART0

uart_handler(&uart[0]);

if (vicstatus & (1<<13)){ // bit 13: UART1

uart_handler(&uart[1]);

if (vicstatus & (1<<31)){ // SIC ngắt=bit_31 trên VIC

if (sicstatus & (1<<3)){ // KBD tại IRQ3 của SIC

kbd_handler();

if (sicstatus & (1<<22)){ // SDC tại IRQ22 của SIC

sdc_handler();

char rbuf[512], wbuf[512];

char *line[2] = {"ĐÂY LÀ DÒNG THỬ NGHIỆM", "đây là dòng thử nghiệm"};

int chính()

int i, ngành, N;

fbuf_init();

kbd_init();

uart_init();
Machine Translated by Google

80 3 Xử lý ngắt và ngoại lệ

/* kích hoạt ngắt hẹn giờ0,1, uart0,1 SIC */

VIC_INTENABLE = (1<<4); // bộ đếm thời gian0,1 ở bit4

VIC_INTENABLE |= (1<<12); // UART0 tại bit12

VIC_INTENABLE |= (1<<13); // UART1 tại bit13

VIC_INTENABLE |= (1<<31); // SIC tới IRQ31 của VIC

/* kích hoạt KBD và SDC IRQ */

SIC_INTENABLE = (1<<3); // KBD int=bit3 trên SIC

SIC_INTENABLE |= (1<<22); // SDC int=bit22 trên SIC

SIC_ENSET = (1<<3); // KBD int=3 trên SIC

SIC_ENSET |= (1<<22); // SDC int=22 trên SIC

bộ đếm thời gian_init();

bộ đếm thời gian_start(0);

/* Mã kiểm tra trình điều khiển UART và KBD bị bỏ qua */

printf("kiểm tra TRÌNH ĐIỀU KHIỂN SDC\n");

sdc_init();

N = 1; // Viết|Đọc N khu vực của SDC

for (sector=0; Sector < N; Sector++){

printf("Ghi khu vực %d: ", khu vực);

bộ nhớ(wbuf, '', 512); // bỏ trống wbuf

cho (i=0; i<12; i++) // viết dòng vào wbuf

strcpy(wbuf+i*40, line[sector % 2]);

put_sector(ngành, wbuf);

printf("\n");

for (sector=0; Sector < N; Sector++){

printf("ĐỌC khu vực %d\n", khu vực);

get_sector(ngành, rbuf);

cho (i=0; i<512; i++){

printf("%c", rbuf[i]);

printf("\n");

printf("trong vòng lặp while(1): nhập khóa từ KBD hoặc UART\n");

trong khi(1);

Hướng dẫn sử dụng đa năng ARM chỉ định rằng MMCI0 ngắt ở IRQ22 trên cả VIC và SIC. Tuy nhiên, trong PL180 được mô
phỏng bởi QEMU, nó thực sự bị gián đoạn ở IRQ22 của SIC, được chuyển đến IRQ31 của VIC. Lý do cho sự khác biệt này vẫn
chưa được biết. Ngoài sự khác biệt nhỏ này, PL180 mô phỏng hoạt động như mong đợi. Hình 3.8 cho thấy kết quả đầu ra khi
chạy chương trình trình điều khiển SDC C3.4.

3.7.3 Trình điều khiển SDC được cải tiến

Trong trình điều khiển SDC, trình xử lý ngắt thực hiện tất cả việc truyền dữ liệu trên một ngắt duy nhất. Do việc truyền
dữ liệu từ MMC sang SDC có thể bị chậm nên bộ xử lý ngắt phải thực hiện vòng lặp truyền dữ liệu nhiều lần trong khi chờ
MMC sẵn sàng cung cấp hoặc chấp nhận dữ liệu. Hạn chế của sơ đồ này là về cơ bản nó giống hoặc thậm chí tệ hơn I/O bằng
cách bỏ phiếu. Nói chung, trình xử lý ngắt phải được hoàn thành càng sớm càng tốt. Phải tránh hoặc loại bỏ bất kỳ việc
kiểm tra và chờ đợi quá mức nào bên trong trình xử lý ngắt. Do đó, mong muốn giảm thiểu số lượng ngắt và tối đa hóa
lượng truyền dữ liệu trên mỗi ngắt. Điều này dẫn chúng ta đến một trình điều khiển SDC được cải tiến. Trong trình điều
khiển SDC cải tiến, chúng tôi lập trình MMC để chỉ tạo ra các ngắt khi Rx FIFO đầy hoặc Tx FIFO trống. Đối với mỗi ngắt,
chúng tôi truyền 16 dữ liệu u32 trên mỗi ngắt. Các đoạn mã sau đây hiển thị trình điều khiển SDC cải tiến, trong đó các
sửa đổi được hiển thị bằng các dòng in đậm.
Machine Translated by Google

3.7 Thẻ kỹ thuật số an toàn (SD) 81

Hình 3.8 Đầu ra của chương trình điều khiển SDC

// các biến được chia sẻ giữa trình điều khiển SDC và trình xử lý ngắt

char dễ bay hơi *rxbuf, *txbuf;

biến động int rxcount, txcount, rxdone, txdone;

int sdc_handler()

trạng thái u32, status_err;

int tôi;

u32 *lên;

// đọc thanh ghi trạng thái để tìm ra ngắt TXempty hoặc RxFull

trạng thái = *(u32 *)(cơ sở + TÌNH TRẠNG);

if (status & (1<<17)){ // RxFull: đọc 16 u32 cùng một lúc;

printf("Ngắt RX: "); up = (u32 *)rxbuf;

status_err = trạng thái &

(DCRCFAIL | DTIMEOUT | RXOVERR);

if (!status_err && rxcount) {

printf("R%d", rxcount);

vì (i = 0; i < 16; i++)

*(up + i) = *(u32 *)(base + FIFO);

lên += 16;

rxcount -= 64;

rxbuf += 64;

trạng thái = *(u32 *)(cơ sở + TÌNH TRẠNG); // xóa ngắt Rx

nếu (rxcount == 0)

rxdone = 1;

else if (status & (1<<18)){ // TXempty: viết 16 u32 mỗi lần

printf("Ngắt TX: ");


Machine Translated by Google

82 3 Xử lý ngắt và ngoại lệ

up = (u32 *)txbuf;

status_err = trạng thái & (DCRCFAIL | DTIMEOUT); if (!status_err &&

txcount) { printf("W%d ", txcount);

vì (i = 0; i < 16; i++)

*(u32 *)(cơ sở + FIFO) = *(up + i);

lên += 16;

txcount -= 64;

txbuf += 64; // nâng cao txbuf cho lần viết tiếp theo

trạng thái = *(u32 *)(cơ sở + TÌNH TRẠNG); // xóa ngắt Tx

nếu (txcount == 0)

txdone = 1;

//printf("ghi để xóa thanh ghi\n");

*(u32 *)(base + STATUS_CLEAR) = 0xFFFFFFFF;

// printf("Xử lý ngắt SDC đã hoàn tất\n");

Hình 3.9 cho thấy kết quả đầu ra khi chạy chương trình trình điều khiển SDC cải tiến. Như hình minh họa, mỗi ngắt truyền 16
dữ liệu 4 byte, do đó số byte truyền giảm đi 64 trên mỗi ngắt. Để cải tiến hơn nữa, người đọc có thể lập trình MMC để tạo ra
các ngắt khi Rx FIFO đầy một nửa và Tx FIFO trống một nửa. Trong trường hợp đó, mỗi ngắt có thể truyền 8 dữ liệu 4 byte. Nó cải
thiện tốc độ truyền dữ liệu nhưng phải trả giá bằng nhiều ngắt hơn và do đó tốn nhiều chi phí hơn do xử lý ngắt.

3.7.4 Truyền dữ liệu đa ngành

Trình điều khiển SD ở trên truyền một cung (512 byte) dữ liệu cùng một lúc. Một hệ thống nhúng có thể hỗ trợ các hệ thống tệp,
thường sử dụng kích thước khối tệp 1 hoặc 4 kB. Trong trường hợp đó, sẽ hiệu quả hơn khi truyền dữ liệu từ/đến thẻ SD ở nhiều
khu vực phù hợp với kích thước khối tệp. Các đoạn mã sau đây hiển thị trình điều khiển SD đã được sửa đổi để truyền dữ liệu
theo nhiều lĩnh vực.

Để đọc nhiều lĩnh vực, hãy ra lệnh CMD18. Để viết nhiều ngành, hãy ra lệnh CMD25. Trong cả hai trường hợp, độ dài dữ liệu là

kích thước khối tệp. Đối với truyền dữ liệu đa khu vực, việc truyền dữ liệu phải được chấm dứt bằng lệnh dừng truyền CMD12,
lệnh này được đưa ra trong trình xử lý ngắt khi số byte (rxcount hoặc txcount) đạt đến 0. Các dòng sửa đổi của trình điều khiển
được hiển thị bằng chữ in đậm. Trong đoạn mã, FBLK_SIZE được định nghĩa là 4096. Mỗi lệnh gọi get_block()/put_block() đọc/ghi
một khối (tệp) dữ liệu 4 KB.

// các biến được chia sẻ giữa trình điều khiển SDC và trình xử lý ngắt

#xác định FBLK_SIZE 4096

char dễ bay hơi *rxbuf, *txbuf;

biến động int rxcount, txcount, rxdone, txdone;

int sdc_handler()

trạng thái u32, err, *up;

int tôi;

// đọc thanh ghi trạng thái để tìm ra TXempty hoặc RxAvail

trạng thái = *(u32 *)(cơ sở + TÌNH TRẠNG);

if (status & (1<<17)){ // RxFull: đọc 16 u32 cùng một lúc;

up = (u32 *)rxbuf;

lỗi = trạng thái & (DRCFAIL | DTIMEOUT | RXOVERR);

if (!err && rxcount){

vì (i = 0; i < 16; i++)


Machine Translated by Google

3.7 Thẻ kỹ thuật số an toàn (SD) 83

Hình 3.9 Đầu ra của trình điều khiển SDC cải tiến

*(up + i) = *(u32 *)(base + FIFO);

lên += 16;

rxcount -= 64;

rxbuf += 64;

trạng thái = *(u32 *)(cơ sở + TÌNH TRẠNG); // đọc trạng thái

nếu (rxcount == 0){

do_command(12, 0, MMC_RSP_R1); // dừng truyền

rxdone = 1;

else if (status & (1<<18)){ // TXempty: viết 16 u32 mỗi lần

up = (u32 *)txbuf;

lỗi = trạng thái & (DCRCFAIL | DTIMEOUT);

if (!err && txcount) {

vì (i = 0; i < 16; i++)

*(u32 *)(cơ sở + FIFO) = *(up + i);

lên += 16;

txcount -= 64;

txbuf += 64; // nâng cao txbuf cho lần viết tiếp theo

trạng thái = *(u32 *)(cơ sở + TÌNH TRẠNG); // đọc trạng thái

nếu (txcount == 0){

do_command(12, 0, MMC_RSP_R1); // dừng truyền

txdone = 1;

*(u32 *)(base + STATUS_CLEAR) = 0xFFFFFFFF;


Machine Translated by Google

84 3 Xử lý ngắt và ngoại lệ

}
int get_block(int blk, char *buf)

u32 cmd, arg;

rxbuf = buf; rxcount = FBLK_SIZE; rxdone = 0;

*(u32 *)(cơ sở + DATATIMER) = 0xFFFF0000;

// ghi data_len vào datalength reg

*(u32 *)(cơ sở + DATALENGTH) = FBLK_SIZE;

// 0x93=|9|0011|=|9|DMA=0,0=BLOCK,1=Máy chủ<-Card,1=Bật
*(u32 *)(cơ sở + DATAACTRL) = 0x93; // Thẻ tới máy chủ

cmd = 18; // CMD18 = ĐỌC đa ngành

arg = (blk*FBLK_SIZE);

do_command(cmd, arg, MMC_RSP_R1);

while(rxdone == 0);

printf("get_block return\n");

int put_block(int blk, char *buf)

u32 cmd, arg;

txbuf = buf; txcount = FBLK_SIZE; txdone = 0;

*(u32 *)(cơ sở + DATATIMER) = 0xFFFF0000;

*(u32 *)(cơ sở + DATALENGTH) = FBLK_SIZE;

cmd = 25; // CMD25 = Viết nhiều cung

arg = (u32)(blk*fBLK_SIZE);

do_command(cmd, arg, MMC_RSP_R1);

// 0x91=|9|0001|=|9|DMA=0,0=BLOCK,0=Máy chủ->Thẻ,1=Bật
*(u32 *)(cơ sở + DATAACTRL) = 0x91; // Lưu trữ vào thẻ

while(txdone == 0);

printf("put_block return\n");

Bạn đọc có thể thay thế trình điều khiển SDC trong chương trình mẫu C3.4 bằng đoạn mã trên và chạy thử chương trình để xác minh

việc truyền dữ liệu đa khu vực. Người đọc cũng có thể thay đổi FBLK_SIZE để phù hợp với các kích thước khối khác, kích thước này phải

là bội số của kích thước cung (512).

3.8 Ngắt có véc-tơ

Cho đến nay, tất cả các chương trình ví dụ đều sử dụng các ngắt không có vectơ. Nhược điểm của ngắt không có vectơ là bộ xử lý ngắt

phải quét thanh ghi trạng thái ngắt để tìm các bit khác 0 để xác định nguồn ngắt, việc này rất tốn thời gian. Trong nhiều hệ thống máy

tính khác, chẳng hạn như PC chạy Intel x86, các ngắt được điều khiển bởi phần cứng. Trong sơ đồ ngắt có vectơ, mỗi ngắt được gán một

số vectơ được xác định theo mức độ ưu tiên của ngắt. Khi xảy ra ngắt, CPU có thể lấy số vectơ của ngắt từ phần cứng bộ điều khiển ngắt

và sử dụng nó để gọi trực tiếp quy trình dịch vụ ngắt tương ứng. Bộ điều khiển ngắt Vectored ARM PL190 (VIC) cũng có khả năng này.

Trong phần này, chúng tôi trình bày cách lập trình PL190 VIC để xử lý các ngắt theo vectơ.

3.8.1 Bộ điều khiển ngắt vectơ ARM PL190 (VIC)

PL190 VIC của bo mạch ARM Versatile/926EJ-S hỗ trợ các ngắt theo vectơ. Sách hướng dẫn kỹ thuật VIC [ARM PL190 2004] chứa thông tin

sau về cách lập trình VIC cho các ngắt vectơ.


Machine Translated by Google

3.8 Ngắt có véc-tơ 85

. Thanh ghi VectorAddr (0x30): Thanh ghi VectorAddr chứa địa chỉ ISR của IRQ đang hoạt động hiện tại. Ở cuối ISR hiện tại,
ghi một giá trị vào thanh ghi này để xóa ngắt hiện tại. PL192 VIC có thêm khả năng ưu tiên các nguồn IRQ bằng cách ghi các
giá trị 0–15 vào các thanh ghi IntPriority. Khi kết thúc ISR hiện tại, việc ghi vào thanh ghi VectorAddr cho phép VIC tái
ưu tiên các IRQ đang chờ xử lý.
. Thanh ghi DefaultVecAddr (0x34): Thanh ghi này chứa địa chỉ ISR của một ngắt mặc định, ví dụ như đối với bất kỳ ngắt giả
nào.
. Các thanh ghi VectorAddress [0–15] (0x100-0x13C): Mỗi thanh ghi này chứa địa chỉ ISR của IRQ0 đến IRQ15. PL192 có 32
thanh ghi vectorAddress cho 32 ISR.
. Các thanh ghi VectorControl [0–15] (0x200-0x23C): Mỗi thanh ghi này chứa nguồn ngắt (bit 4–1) và một bit Enable (bit 5).

Để sử dụng các ngắt có vectơ, mỗi thiết bị phải được kích hoạt cho các ngắt ở cả cấp độ thiết bị và cả ở cấp độ thiết bị.
VIC. Chúng tôi chứng minh các ngắt có vectơ bằng chương trình mẫu C3.5 cho các thiết bị sau.

. Bộ hẹn giờ0: VectorInt0

. UART0: VectorInt1

. UART1: VectorInt2

. Bàn phím: VectorInt3

Các đoạn mã được sử dụng để lập trình VIC cho các ngắt có vectơ được liệt kê bên dưới.

3.8.2 Cấu hình VIC cho các ngắt có vectơ

C3.5.1 : Hàm vecotrInt_init() cấu hình VIC cho các ngắt có vectơ.

int vectorInt_init() / Sử dụng các ngắt có vectơ của PL190 VIC

printf("vectorInterrupt_init()\n");

/*********** thiết lập các ngắt có vectơ *******************

(1). ghi vào vectoraddr0 (0x100) với ISR của clock0

vectoraddr1 (0x104) với ISR của UART0

vectoraddr2 (0x108) với ISR của UART1

vectoraddr3 (0x10C) với ISR của KBD

**************************************************** *******/

*((int *)(VIC_BASE_ADDR+0x100)) = (int)timer0_handler;

*((int *)(VIC_BASE_ADDR+0x104)) = (int)uart0_handler; *((int *)

(VIC_BASE_ADDR+0x108)) = (int)uart1_handler; *((int *)(VIC_BASE_ADDR+0x10C)) =

(int)kbd_handler; // (2). ghi vào intControlRegs = E=1|IRQ#=1xxxxx *((int *)

(VIC_BASE_ADDR+0x200)) = 0x24; //100100 tại IRQ 4 *((int *)

(VIC_BASE_ADDR+0x204)) = 0x2C; //101100 tại IRQ 12 *((int *)(VIC_BASE_ADDR+0x208)) =

0x2D; //101101 tại IRQ 13

*((int *)(VIC_BASE_ADDR+0x20C)) = 0x3F; //111111 tại IRQ 31

// (3). ghi số 0 vào IntSelectReg để tạo ngắt IRQ //(bất kỳ bit nào=1 tạo ngắt FIQ)

*((int *)(VIC_BASE_ADDR+0x0C)) = 0;

}
Machine Translated by Google

86 3 Xử lý ngắt và ngoại lệ

3.8.3 Trình xử lý ngắt có vectơ

C3.5.2. Viết lại IRQ_handler() cho các ngắt có vectơ. Khi sử dụng các ngắt có vectơ, mọi ngắt IRQ vẫn đến IRQ_handler()
như bình thường. Tuy nhiên, chúng ta phải viết lại IRQ_handler() để sử dụng các ngắt có vectơ. Khi truy cập IRQ_handler(),
trước tiên chúng ta phải đọc thanh ghi VectorAddr để xác nhận ngắt. Không giống như trường hợp ngắt không có vectơ, không
cần đọc các thanh ghi trạng thái để xác định nguồn ngắt. Thay vào đó, chúng ta có thể lấy trực tiếp địa chỉ của trình xử
lý IRQ hiện tại từ thanh ghi vectorAddr. Sau đó chỉ cần gọi trình xử lý theo địa chỉ mục nhập của nó. Ngoài ra, nguồn
ngắt cũng có thể được xác định từ thanh ghi VectorStatus. Khi bộ xử lý trả về, hãy gửi EOI tới bộ điều khiển VIC bằng
cách ghi một giá trị (bất kỳ) vào thanh ghi vectorAddr, cho phép nó ưu tiên lại các yêu cầu ngắt đang chờ xử lý. Phần sau
đây cho thấy hàm IRQ_handler() đã được sửa đổi cho các ngắt có vectơ.

void IRQ_handler( )

void *(*f)( ); // f làm con trỏ hàm

trạng thái int = *(int *)(VIC_BASE_ADDR+0x30); f =(void *)*((int

*)(VIC_BASE_ADDR+0x30));

f(); // gọi hàm ISR

*((int *)(VIC_BASE_ADDR+0x30)) = 1; // ghi vào vectorAddr dưới dạng EOI

3.8.4 Trình diễn ngắt vectơ

C3.5.3. tc: Để cho ngắn gọn, chúng tôi chỉ hiển thị mã có liên quan của main() được sử dụng để kiểm tra các ngắt thiết bị
khác nhau.

int chính()

fbuf_init(); // màn hình LCD

kbd_init(); // KBD

uart_init(); // UART

bộ đếm thời gian_init(); // Hẹn giờ

lên = &uart[0];

kp = &kbd;

/* kích hoạt ngắt hẹn giờ0,1, uart0,1 SIC */

VIC_INTENABLE = 0;

VIC_INTENABLE |= (1<<4); // bộ đếm thời gian0,1 ở bit4

VIC_INTENABLE |= (1<<12); // UART0 tại bit12

VIC_INTENABLE |= (1<<13); // UART1 tại bit13

VIC_INTENABLE |= (1<<31); // SIC tới IRQ31 của VIC

/* kích hoạt IRQ KBD từ SIC */

SIC_INTENABLE = 0;

SIC_INTENABLE |= (1<<3); // KBD int=bit3 trên SIC printf("Khởi động

chương trình: kiểm tra các ngắt theo vectơ\n");

vectorInt_init(); // phải làm điều này SAU driver_init()

bộ đếm thời gian_start(0); // bắt đầu hẹn giờ0

printf("kiểm tra I/O UART0: nhập văn bản từ UART 0\n");

trong khi(1){

ugets(lên, dòng);

uprintf("line=%s\n", line);

if (strcmp(dòng, "kết thúc")==0)

phá vỡ;

}
Machine Translated by Google

3.8 Ngắt có véc-tơ 87

Hình 3.10 Minh họa các ngắt có vectơ

printf("kiểm tra I/O UART1: nhập văn bản từ UART 1\n"); lên = &uart[1];

trong khi(1){

ugets(lên, dòng);

ufprintf(up, " line=%s\n", line);

if (strcmp(dòng, "kết thúc")==0)

phá vỡ;

printf("kiểm tra đầu vào KBD\n"); // in ra LCD

trong khi(1){

kgets(dòng);

printf("line=%s\n", line);

if (strcmp(dòng, "kết thúc")==0)

phá vỡ;

printf("HẾT CUỐI chạy %d\n", 1234);

Hình 3.10 cho thấy kết quả đầu ra mẫu khi chạy chương trình C3.5, minh họa bộ đếm thời gian, trình điều khiển UART và KBD
sử dụng các ngắt có vector.

3.9 Các ngắt lồng nhau

3.9.1 Tại sao ngắt lồng nhau

Trong các chương trình ví dụ ở trên, C3.1–C3.5, minh họa quá trình xử lý ngắt, mọi trình xử lý ngắt IRQ đều bắt đầu thực thi với
các ngắt IRQ bị vô hiệu hóa (bị che). Các ngắt IRQ không được bật (được che giấu) cho đến khi trình xử lý ngắt
Machine Translated by Google

88 3 Xử lý ngắt và ngoại lệ

Đã hoàn thành xong. Điều này ngụ ý rằng mỗi lần ngắt chỉ có thể được xử lý một lần. Nhược điểm của sơ đồ này là nó có thể dẫn
đến đảo ngược mức ưu tiên ngắt, trong đó việc xử lý ngắt có mức ưu tiên thấp có thể chặn hoặc trì hoãn việc xử lý các ngắt có
mức ưu tiên cao hơn. Đảo ngược ưu tiên ngắt có thể làm tăng thời gian phản hồi của hệ thống đối với các ngắt, điều này không
mong muốn trong các hệ thống nhúng có yêu cầu quan trọng về thời gian. Để khắc phục điều này, các hệ thống nhúng nên cho phép
các ngắt lồng nhau. Trong sơ đồ ngắt lồng nhau, ngắt có mức ưu tiên cao hơn có thể ưu tiên xử lý các ngắt có mức ưu tiên thấp
hơn, tức là trước khi trình xử lý ngắt hiện tại kết thúc, nó có thể chấp nhận và xử lý các ngắt có mức ưu tiên cao hơn, từ đó
giảm độ trễ xử lý ngắt và cải thiện phản ứng của hệ thống đối với các ngắt.

3.9.2 Các ngắt lồng nhau trong ARM

Bộ xử lý ARM không được thiết kế để hỗ trợ các ngắt lồng nhau một cách hiệu quả do các đặc tính sau của kiến trúc bộ xử lý ARM.

Khi CPU ARM chấp nhận ngắt IRQ, nó sẽ chuyển sang chế độ IRQ, chế độ này có các thanh ghi ngân hàng riêng lr_irq, sp_irq,
spsr và cpsr. CPU lưu địa chỉ trả về (có offset +4) vào lr_irq, lưu CPSR chế độ trước đó vào spsr và nhập trình xử lý ngắt để
xử lý ngắt hiện tại. Để hỗ trợ các ngắt lồng nhau, trình xử lý ngắt phải vạch mặt các ngắt tại một thời điểm nào đó để cho
phép các ngắt có mức độ ưu tiên cao hơn xảy ra. Tuy nhiên, điều này tạo ra hai vấn đề.

(1). Việc chấp nhận một ngắt khác có thể làm hỏng thanh ghi liên kết: Giả sử rằng, sau khi kích hoạt các ngắt IRQ, trình xử lý
ngắt gọi ISR để xử lý ngắt hiện tại, như trong

irq_handler:

// tìm ra nguồn IRQ để xác định ISR của nó

// kích hoạt ngắt IRQ

bl ISR

ĐÂY:

Khi gọi ISR để xử lý ngắt hiện tại, thanh ghi liên kết lr_irq chứa địa chỉ trả về nhãn TẠI ĐÂY.
Trong khi thực thi ISR, nếu một ngắt khác xảy ra, CPU sẽ nhập lại irq_handler, thao tác này sẽ thay đổi lr_irq thành địa chỉ
trả về của điểm ngắt mới. Điều này làm hỏng thanh ghi liên kết ban đầu lr_irq, khiến ISR trở về địa chỉ sai khi hoàn tất.

(2). Ghi đè CPSR đã lưu: Khi bộ xử lý ARM chấp nhận một ngắt, nó sẽ lưu CPSR của điểm bị gián đoạn trong SPSR_irq (được lưu
trong ngân hàng), SPSR đã lưu có thể ở chế độ USER hoặc SVC nếu mã bị gián đoạn đang thực thi ở chế độ USER hoặc SVC . Trong
khi thực thi ISR ở chế độ IRQ, nếu một ngắt khác xảy ra, CPU sẽ ghi đè SPSR_irq bằng CPSR ở chế độ IRQ, điều này sẽ khiến ISR
đầu tiên quay lại chế độ sai khi nó kết thúc.

Việc giải quyết vấn đề ở (2) khá dễ dàng. Khi truy cập vào bộ xử lý ngắt nhưng trước khi cho phép IRQ thực hiện các ngắt
tiếp theo, chúng ta có thể lưu SPSR vào ngăn xếp chế độ IRQ. Khi ISR kết thúc, chúng tôi khôi phục SPSR đã lưu từ ngăn xếp.
Tuy nhiên, nếu chúng ta cho phép các ngắt lồng nhau ở chế độ IRQ thì không có cách nào khắc phục được vấn đề ở (1). Nó sẽ gây
ra một vòng lặp vô hạn vì mọi ISR sẽ quay trở lại phần đầu của trình xử lý ngắt. Cách duy nhất để giảm bớt vấn đề này là điều
khiển CPU khỏi chế độ IRQ. Vì lý do này, ARM đã giới thiệu chế độ SYS, đây là chế độ đặc quyền nhưng có thanh ghi liên kết
khác với chế độ IRQ. Giả sử rằng, trước khi cho phép các ngắt IRQ tiếp theo, chúng ta chuyển CPU sang chế độ SYS và gọi ISR ở
chế độ SYS. Nếu một ngắt khác xảy ra, nó sẽ thay đổi thanh ghi liên kết chế độ IRQ lr_irq nhưng không thay đổi thanh ghi liên
kết lr_sys ở chế độ SYS. Điều này cho phép ISR trở lại địa chỉ chính xác khi kết thúc. Vì vậy, sơ đồ xử lý ngắt lồng nhau như
sau.

3.9.3 Xử lý các ngắt lồng nhau trong chế độ SYS

irq_handler: // điểm vào của các ngắt IRQ


(1). Lưu bối cảnh của điểm bị gián đoạn và lr vào ngăn xếp chế độ IRQ
Machine Translated by Google

3.9 Các ngắt lồng nhau 89

(2). Lưu SPSR vào ngăn xếp chế độ IRQ


(3). Chuyển sang chế độ SYS với IRQ bị tắt

(4). Xác định nguồn ngắt và xóa ngắt.

(5). Kích hoạt ngắt IRQ


(6). Gọi ISR ở chế độ SYS cho ngắt IRQ hiện tại
(7). Chuyển về chế độ IRQ khi tắt IRQ
(số 8). Khôi phục ngăn xếp chế độ IRQ của biểu mẫu SPSR đã lưu

(9). Trả về theo ngữ cảnh đã lưu và lr trong ngăn xếp chế độ IRQ

Nhiều bộ xử lý ARM yêu cầu ngăn xếp liên kết 8 byte. Khi thay đổi CPU sang chế độ SYS, có thể cần phải
trước tiên hãy kiểm tra và điều chỉnh ngăn xếp chế độ SYS để căn chỉnh phù hợp. Vì ngăn xếp chế độ SYS bắt đầu ở ranh giới 8 byte, nên chúng tôi

có thể cho rằng việc kiểm tra và điều chỉnh là không cần thiết. Danh sách sau đây mã irq_handler thực hiện
thuật toán trên.

irq_handler:

stmfd sp!, {r0-r3, r12, lr} // lưu ngữ cảnh vào ngăn xếp IRQ

bà r12, spsr // sao chép spsr vào r12

stmfd sp!, {r12} // lưu SPSR vào ngăn xếp IRQ

msr cpsr, #0x9F // chuyển sang chế độ SYS với IRQ bị tắt

ldr r1, =vectorAddr // đọc vectorAddr reg để báo nhận ngắt

ldr r0, [r1]

msr cpsr, #0x1F // bật IRQ ở chế độ SYS

bl IRQ_xử lý // xử lý IRQ hiện tại ở chế độ SYS

msr cpsr, #0x92 // chuyển về chế độ IRQ khi IRQ bị tắt

ldmfd sp!, {r12} // lấy SPSR đã lưu từ ngăn xếp IRQ

msr spsr, r12 // khôi phục spsr

ldr r1, =vectorAddr // ghi vào vectorAddr dưới dạng EOI

str r0, [r1]

ldmfd sp!, {r0-r3, r12, lr} // khôi phục ngữ cảnh đã lưu từ ngăn xếp IRQ

subs pc, r14, #4 // quay lại điểm bị gián đoạn

ARM khuyến nghị xử lý các ngắt lồng nhau trong chế độ SYS (Ngắt lồng nhau 2011) nhưng cũng có thể được thực hiện trong SVC
chế độ được sử dụng trong chương trình trình diễn.

3.9.4 Trình diễn các ngắt lồng nhau

C3.6.1. ts.s: Đầu tiên, chúng tôi hiển thị irq_handler đã sửa đổi cho các ngắt lồng nhau. Thay vì chế độ SYS, nó xử lý các dữ liệu lồng nhau

ngắt ở chế độ SVC. Hàm get_cpsr() trả về chế độ bộ xử lý trong CPSR. Nó dùng để hiển thị chế độ hiện tại
của CPU.

.set vectorAddr, 0x10140030 // VIC vectorĐăng ký địa chỉ

irq_handler:

stmfd sp!, {r12, lr} // lưu r12, lr vào ngăn xếp IRQ

bà r12, spsr // sao chép spsr vào r12

stmfd sp!, {r12} // lưu spsr vào ngăn xếp IRQ

msr cpsr, #0x93 // chuyển sang chế độ SVC với IRQ bị tắt

stmfd sp!, {r0-r3, r12, lr} // lưu ngữ cảnh trong ngăn xếp chế độ SVC

ldr r1, =vectorAddr

ldr r0, [r1] // đọc VIC vectAddr tới ngắt ACK

// bl enterINT // Xem ý kiến

msr cpsr, #0x13 // bật IRQ ở chế độ SVC


Machine Translated by Google

90 3 Xử lý ngắt và ngoại lệ

bl IRQ_handler // xử lý IRQ ở chế độ SVC

ldmfd sp!, {r0-r3, r12, lr} // khôi phục ngữ cảnh từ ngăn xếp SVC

msr cpsr, #0x92 // sang chế độ IRQ với IRQ bị tắt

ldmfd sp!, {r12} // bật spsr đã lưu

msr spsr, r12 // khôi phục spsr

// bl exitINT // xem bình luận

ldr r1, =vectorAddr // cấp EOI cho VIC

str r0, [r1]

ldmfd sp!, {r12, r14} // return sub pc, r14, #4

get_cpsr: // trả về CPSR

bà r0, cpsr mov pc,

lr

C3.6.2. Tệp tc: Tệp tc giống như trong C3.5, ngoại trừ các hàm được thêm vào enterINT() và exitINT(). Trong C3.5
chương trình sử dụng các ngắt có vectơ, mức độ ưu tiên của các ngắt là (từ cao đến thấp)
Bộ định thời0, UART0, UART1, KBD

Không có các ngắt lồng nhau, mỗi ngắt được xử lý từ đầu đến cuối mà không có quyền ưu tiên. Với các ngắt lồng nhau, việc xử
lý ngắt có mức ưu tiên thấp có thể bị ưu tiên bởi ngắt có mức ưu tiên cao hơn. Để chứng minh điều này, chúng tôi thêm đoạn mã
sau vào chương trình C3.6.

(1). Trong mã irq_handler, trước khi cho phép ngắt IRQ và gọi ISR cho ngắt hiện tại, chúng ta để trình xử lý ngắt gọi enterINT(),
đọc thanh ghi VICstatus để xác định nguồn ngắt. Nếu đó là một ngắt KBD, nó sẽ đặt cờ inKBD toàn cầu dễ bay hơi thành 1, xóa số
tcount toàn cầu dễ bay hơi về 0 và in thông báo enterKBD. Nếu đó là ngắt hẹn giờ ở giữa quá trình xử lý ngắt KBD, thì nó sẽ tăng
tcount lên 1.
(2). Khi ISR trở về irq_handler, nó gọi exitINT(). Nếu ngắt hiện tại là từ KBD, nó sẽ in giá trị tcount và đặt lại tcount về 0.

Giá trị tcount biểu thị số lượng ngắt hẹn giờ có mức độ ưu tiên cao được phục vụ trong khi thực thi trình xử lý KBD có mức
độ ưu tiên thấp. Người đọc có thể bỏ nhận xét các câu lệnh có nhãn (1) và (2) trong mã irq_handler để xác minh tác động của các
ngắt (bộ đếm thời gian) lồng nhau. Sau đây liệt kê mã của enterINT() và exitINT().

trạng thái int dễ bay hơi, inKBD, tcount;

int enterINT()

trạng thái = *((int *)(VIC_BASE_ADDR)); // đọc thanh ghi VICstatus

if (trạng thái & (1<<31)){ // KBD

tcount = 0;

inKBD = 1;

printf("enterKBD ");

if ((status & (1<<4)) && (inKBD){ // time0 bên trong trình xử lý KBD

tcount++; // tcount=số lần ngắt hẹn giờ

int exitINT()

if (trạng thái & 0x80000000){ // KBD

printf("exitKBD=%d\n", tcount); // hiển thị tcount trong trình xử lý KBD

tcount=0; // đặt lại tcount về 0

}
Machine Translated by Google

3.9 Các ngắt lồng nhau 91

void IRQ_handler( )

void *(*f)(); // f làm con trỏ hàm f =(void *)*((int *)(VIC_BASE_ADDR+0x30)); // đọc địa chỉ ISR

(*f)( ); // gọi hàm ISR

//*((int *)(VIC_BASE_ADDR+0x30)) = 1; // ghi EOI vào vectorAddr

int chính()

dòng char[128];

UART *lên; kp = &kbd;

fbuf_init();

kbd_init();

uart_init(); //

kích hoạt các ngắt hẹn giờ0,1 uart0,1 SIC và KBD như trong C8 vectorInt_init(); // giống

như ở C8

bộ đếm thời gian_init();

bộ đếm thời gian_start(0);

printf("Khởi động chương trình C3.6: kiểm tra các ngắt NESTED\n");

lên = &uart[0];

printf("kiểm tra I/O uart0: nhập văn bản từ UART 0\n"); while(1){ ugets(up,

line);

uprintf("line=%s\n",

line); if (strcmp(dòng, "kết thúc")==0)

phá vỡ;

printf("kiểm tra I/O UART1: nhập văn bản từ UART 1\n"); lên = &uart[1];

trong khi(1){

ugets(lên, dòng);

ufprintf(up, " line=%s\n", line);

if (strcmp(dòng, "kết thúc")==0)

phá vỡ;

printf("kiểm tra đầu vào KBD\n"); // in ra LCD

trong khi(1){

kgets(dòng);

printf("line=%s\n", line);

if (strcmp(dòng, "kết thúc")==0)

phá vỡ;

printf("HẾT CUỐI chạy %d\n", 1234);

Hình 3.11 cho thấy kết quả đầu ra khi chạy chương trình ví dụ C3.6, thể hiện các ngắt lồng nhau. Như hình
cho thấy, nhiều lần ngắt hẹn giờ có thể xảy ra trong khi xử lý một ngắt KBD.
Machine Translated by Google

92 3 Xử lý ngắt và ngoại lệ

Hình 3.11 Minh họa các ngắt lồng nhau

3.10 Các ngắt lồng nhau và chuyển đổi quy trình

Sơ đồ ARM xử lý các ngắt lồng nhau trong chế độ SYS hoặc SVC chỉ hoạt động nếu mọi trình xử lý ngắt quay trở lại điểm
gián đoạn ban đầu, do đó không có quá trình chuyển đổi nào xảy ra sau khi ngắt. Nó sẽ không hoạt động nếu một ngắt có
thể khiến chuyển ngữ cảnh sang một quy trình khác. Điều này là do một phần ngữ cảnh thuộc quy trình đã tắt vẫn còn trong
ngăn xếp IRQ, có thể bị ghi đè khi quy trình mới xử lý một ngắt khác. Nếu điều này xảy ra, quá trình đã tắt sẽ không bao
giờ có thể tiếp tục chạy lại do ngữ cảnh thực thi bị hỏng hoặc bị mất. Để ngăn chặn điều này, nội dung ngăn xếp IRQ phải
được chuyển sang ngăn xếp SVC của quá trình đã tắt (và đặt lại con trỏ ngăn xếp IRQ để ngăn nó phát triển vượt quá giới
hạn). Một cách có thể để tránh chuyển nội dung ngăn xếp là phân bổ một ngăn xếp IRQ riêng cho mỗi quy trình. Nhưng điều
này sẽ đòi hỏi nhiều không gian bộ nhớ dành riêng cho các tiến trình dưới dạng ngăn xếp IRQ, về cơ bản sẽ vô hiệu hóa
lợi thế của việc sử dụng một ngăn xếp IRQ duy nhất để xử lý ngắt. Ngoài ra, trong kiến trúc ARM không thể sử dụng cùng
một vùng bộ nhớ cho cả ngăn xếp SVC và IRQ của một quy trình do các con trỏ ngăn xếp riêng biệt ở chế độ SVC và IRQ.
Những điều này đòi hỏi phải chuyển nội dung ngăn xếp IRQ trong quá trình chuyển đổi ngữ cảnh, đây dường như là điểm yếu
cố hữu của kiến trúc bộ xử lý ARM về mặt đa nhiệm.

3.11 Tóm tắt

Ngắt và xử lý ngắt là điều cần thiết cho các hệ thống nhúng. Chương này bao gồm các trường hợp ngoại lệ và xử lý ngắt.
Nó mô tả các chế độ hoạt động của bộ xử lý ARM, các loại ngoại lệ và vectơ ngoại lệ. Nó giải thích chi tiết các chức
năng của bộ điều khiển ngắt và các nguyên tắc xử lý ngắt. Sau đó, nó áp dụng các nguyên tắc xử lý ngắt để thiết kế và
triển khai trình điều khiển thiết bị điều khiển ngắt, bao gồm trình điều khiển cho bộ hẹn giờ, bàn phím, thẻ UART và SD,
đồng thời trình diễn trình điều khiển thiết bị bằng các chương trình mẫu. Nó giải thích những ưu điểm của các ngắt có
vectơ, chỉ ra cách cấu hình VIC cho các ngắt có vectơ và trình bày quá trình xử lý các ngắt có vectơ. Nó cũng giải
thích các nguyên tắc của các ngắt lồng nhau và trình diễn việc xử lý các ngắt lồng nhau bằng các chương trình ví dụ.
Machine Translated by Google

3.11 Tóm tắt 93

Danh sách các chương trình mẫu

C3.1: Trình điều khiển hẹn giờ

C3.2: Trình điều khiển

KBD C3.3: Trình điều khiển UART

C3.4: Trình điều khiển SDC: Khu vực R/W

Trình điều khiển SDC được cải tiến: R/W dài 64 mỗi lần ngắt
R/W đa cung cho tệp BLKSIZE

C3.5: Các ngắt được vector hóa cho Bộ hẹn giờ, UART, KBD C3.6:

Các ngắt lồng nhau cho KBD và Bộ hẹn giờ

Các vấn đề

1. Trong chương trình ví dụ C3.1, bảng vectơ nằm ở cuối tệp ts.s.

(1). Trong file ts.s chú thích dòng BL copy_vector

Biên dịch lại và chạy lại chương trình. Chương trình KHÔNG nên hoạt động. Giải thích vì sao?

(2). Di chuyển bảng vectơ đến đầu tệp ts.s, như trong

vectơ_start:

// bảng vectơ

vectơ_end:

reset_handler:

Thay đổi điểm vào trong t.ld thành vectơ_start. Biên dịch lại và chạy lại chương trình. Nó cũng sẽ hoạt động. Giải thích vì sao?

2. Trong ví dụ 3.2, irq_handler lưu tất cả các thanh ghi trong ngăn xếp IRQ. Sửa đổi nó để lưu số lượng thanh ghi TỐI THIỂU. Chỉ ra những thanh ghi nào phải

được lưu để chương trình vẫn hoạt động.

3. Sửa đổi trình điều khiển KBD trong chương trình C3.2 để hỗ trợ chữ in hoa và các phím điều khiển đặc biệt, Control-C và Control-D.

4. Sửa đổi chương trình trình điều khiển UART C3.3 để hỗ trợ UART2 và UART3.

5. Sửa đổi chương trình trình điều khiển UART C3.3 để hỗ trợ bộ đệm FIFO bên trong của UART.

6. Bộ điều khiển ngắt ARM VIC chỉ định mức độ ưu tiên IRQ cố định theo thứ tự IRQ0 (cao) đến IRQ31 (thấp). Các ngắt có vectơ cho phép sắp xếp lại các ưu

tiên IRQ. Trong chương trình ví dụ C3.5, mức độ ưu tiên của các ngắt có vectơ được chỉ định theo thứ tự ban đầu của chúng. Sửa đổi vectorInt_init() để gán

các ưu tiên ngắt theo vectơ khác nhau, ví dụ theo thứ tự KBD, UART1, UART0, hẹn giờ0, từ cao xuống thấp. Kiểm tra xem các ngắt vectơ có còn hoạt động không.

Thảo luận ý nghĩa của việc phân công các ưu tiên ngắt như vậy.

7. Chương trình ví dụ C3.6 xử lý các ngắt lồng nhau ở chế độ SVC. Sửa đổi nó để xử lý các ngắt lồng nhau trong chế độ SYS, theo đề xuất của ARM.

8. Trong chương trình ví dụ C3.6, chương trình hỗ trợ các ngắt lồng nhau, có hai dòng trong irq_handler:

LDR r1, =vectorAddr

LDR r0, [r1] // đọc VIC vectAddr tới ngắt ACK

Hãy bình luận những dòng này để xem điều gì sẽ xảy ra và giải thích tại sao?

9. Trong các hệ thống nhúng, thẻ SD thường được sử dụng làm thiết bị khởi động để khởi động hệ điều hành. Trong quá trình khởi động, mã khởi động có thể

đọc các cung từ thẻ SD bằng cách thăm dò vì nó phải chờ dữ liệu. Thay vì sử dụng các ngắt, hãy viết lại trình điều khiển SD bằng cách sử dụng tính năng bỏ

phiếu.

10. Viết lại trình điều khiển SD bằng cách sử dụng DMA để truyền lượng lớn dữ liệu.
Machine Translated by Google

94 3 Xử lý ngắt và ngoại lệ

Người giới thiệu

Kiến trúc ARM: https://en.wikipedia.org/wiki/ARM_architecture, 2016.


Kiến trúc bộ xử lý ARM: http://www.arm.com/products/processors/instruction-set-architectures, 2016.
ARM PL011: Hướng dẫn tham khảo kỹ thuật ARM PrimeCellUART (PL011), http://infocenter.arm.com/help/topic/com.arm.doc.ddi0183, 2005.
ARMPL180: Hướng dẫn tham khảo kỹ thuật giao diện thẻ đa phương tiện ARM PrimeCell (PL180), Trung tâm thông tin ARM, 1998.
ARMPL181: Hướng dẫn tham khảo kỹ thuật Giao diện thẻ đa phương tiện ARM PrimeCell (PL181), Trung tâm thông tin ARM, 2001.
ARM PL190: Bộ điều khiển ngắt theo vectơ PrimeCell (PL190), http://infocenter.arm.com/help/topic/com.arm.doc.ddi0181e/DDI0181.pdf, 2004.
Bộ hẹn giờ ARM: Hướng dẫn tham khảo kỹ thuật mô-đun hẹn giờ kép ARM (SP804), Trung tâm thông tin cánh tay, 2004.

ARM PL050: Hướng dẫn tham khảo kỹ thuật giao diện bàn phím/chuột ARM PrimeCell PS2 (PL050), Trung tâm thông tin ARM, 1999.
Ngắt lồng nhau: Ngắt lồng nhau, Trung tâm thông tin ARM, 2011.
Thông số kỹ thuật SD: Phiên bản đơn giản hóa của Thông số bộ điều khiển máy chủ SD, https://www.sdcard.org/developers/overview/host_controller/simple_spec.
SDC: Thẻ kỹ thuật số an toàn: Tổng quan về tiêu chuẩn SD - Hiệp hội SD https://www.sdcard.org/developers/overview, 2016.
SPI: Giao diện ngoại vi nối tiếp, https://en.wikipedia.org/wiki/Serial_Peripheral_Interface_Bus, 2016, 2016.
Raspberry_Pi: https://www.raspberrypi.org/products/raspberry-pi-2-model-b, 2016.
Silberschatz, A., PA Galvin, PA, Gagne, G, "Khái niệm hệ điều hành, Phiên bản thứ 8", John Wiley & Sons, Inc. 2009.
Stallings, W. "Hệ điều hành: Nguyên tắc thiết kế và nội bộ ( Ấn bản thứ 7)", Prentice Hall, 2011.
Wang, KC, "Thiết kế và triển khai Hệ điều hành MTX, Nhà xuất bản Quốc tế Springer AG, 2015.
Machine Translated by Google

Mô hình hệ thống nhúng


4

4.1 Cấu trúc chương trình của hệ thống nhúng

Trong những ngày đầu, hầu hết các hệ thống nhúng đều được thiết kế cho các ứng dụng cụ thể. Một hệ thống nhúng thường bao gồm một bộ

vi điều khiển, được sử dụng để giám sát một số cảm biến và tạo tín hiệu để điều khiển một số thiết bị bên ngoài, chẳng hạn như bật đèn

LED hoặc kích hoạt rơle và động cơ servo để điều khiển robot, v.v. Vì lý do này, chương trình điều khiển của các hệ thống nhúng đời

đầu cũng rất đơn giản. Chúng được viết dưới dạng cấu trúc chương trình siêu vòng lặp hoặc hướng sự kiện.

Tuy nhiên, khi sức mạnh tính toán và nhu cầu về các hệ thống đa chức năng tăng lên trong những năm gần đây, các hệ thống nhúng đã trải

qua một bước nhảy vọt vượt bậc cả về ứng dụng lẫn độ phức tạp. Để đáp ứng nhu cầu ngày càng tăng về chức năng bổ sung và độ phức tạp

của hệ thống, các cấu trúc chương trình theo sự kiện và siêu vòng lặp không còn phù hợp nữa. Các hệ thống nhúng hiện đại cần phần mềm

mạnh hơn. Cho đến nay, nhiều hệ thống nhúng trên thực tế là những máy tính có công suất cao, có khả năng chạy các hệ điều hành chính

thức. Một ví dụ điển hình là điện thoại thông minh sử dụng lõi ARM với bộ nhớ trong gig byte và thẻ micro SD nhiều gig byte để lưu

trữ và chạy các phiên bản Linux thích ứng, chẳng hạn như Android (Android 2016). Xu hướng thiết kế hệ thống nhúng hiện nay rõ ràng

đang chuyển động theo hướng phát triển hệ điều hành đa chức năng phù hợp với môi trường di động. Trong chương này, chúng ta sẽ thảo

luận về các cấu trúc chương trình và mô hình lập trình khác nhau phù hợp với các hệ thống nhúng hiện tại và tương lai.

4.2 Mô hình siêu vòng lặp

Siêu vòng lặp là cấu trúc chương trình bao gồm một vòng lặp vô hạn, với tất cả các nhiệm vụ của hệ thống được chứa trong vòng lặp đó.

Dạng tổng quát của chương trình siêu vòng lặp là

chủ yếu()

system_initialization();

trong khi(1){

Kiểm tra_Device_Status();

Process_Device_Data();

Output_Response();

Sau khi khởi tạo hệ thống, chương trình sẽ thực hiện một vòng lặp vô hạn, trong đó nó kiểm tra trạng thái của một thành phần hệ

thống, chẳng hạn như trạng thái của thiết bị đầu vào. Khi thiết bị cho biết có dữ liệu đầu vào, nó sẽ thu thập dữ liệu đầu vào, xử lý

dữ liệu và tạo đầu ra dưới dạng phản hồi. Sau đó nó lặp lại vòng lặp.

© Springer International Publishing AG 2017 95


KC Wang, Hệ điều hành nhúng và thời gian thực, DOI
10.1007/978-3-319-51517-5_4
Machine Translated by Google

96 4 mô hình hệ thống nhúng

4.2.1 Ví dụ về chương trình siêu vòng lặp

Chúng tôi minh họa cấu trúc chương trình siêu vòng lặp bằng các ví dụ. Trong chương trình ví dụ đầu tiên, được ký hiệu là C4.1,

chúng tôi giả sử rằng một hệ thống nhúng điều khiển UART cho I/O. Mục tiêu của chúng tôi ở đây là phát triển một chương trình điều

khiển liên tục kiểm tra xem có bất kỳ đầu vào nào từ cổng UART hay không. Bất cứ khi nào một phím được nhấn, nó sẽ nhận được phím
đầu vào, xử lý đầu vào và tạo ra đầu ra, chẳng hạn như bật đèn LED, bật công tắc, v.v. Khi chạy chương trình trên máy ảo ARM giả

lập, không có bất kỳ phím nào. LED hoặc công tắc, chúng ta chỉ cần lặp lại phím đầu vào để mô phỏng quá trình xử lý đầu vào và tạo

ra phản hồi đầu ra.


Trong chương trình, nó kiểm tra UART để tìm đầu vào. Đối với mỗi phím chữ cái ở dạng chữ thường, nó sẽ chuyển đổi phím thành chữ

hoa và hiển thị phím đó cho cổng UART. Ngoài ra, nó còn xử lý phím return bằng cách xuất ra một dòng char mới để tạo ra hiệu ứng

hình ảnh phù hợp. Mã hợp ngữ của chương trình giống với mã ở Chap. 3. Chúng tôi chỉ hiển thị mã C của chương trình.

Chương trình ví dụ C4.1: Chương trình siêu vòng lặp

/*************** tệp tc của chương trình siêu vòng lặp C4.1 *************/

#xác định UDR 0x00

#xác định UFR 0x18

char *ubase;

int chính()

ký tự c;

ubase = (char *)0x101F1000; // 1. khởi tạo địa chỉ cơ sở UART0

trong khi(1){

if (*(ubase + UFR) & 0x40){ // 2. kiểm tra UART0 xem RxFull

c = *(ubase + UDR); // 3. lấy khóa đầu vào UART0

if (c > = 'a' && c <= 'z') // nếu phím chữ thường

c += ('A' - 'a'); // chuyển sang chữ hoa

*(ubase + UDR) = c; // 4. tạo đầu ra

nếu (c == '\r')

*(ubase + UDR) = '\n';

Khi chạy chương trình C4.1 trên máy ảo ARM trong QEMU, nó sẽ lặp lại từng phím chữ cái thành UART0 bằng chữ in hoa. Thay vì một

thiết bị duy nhất, chương trình có thể được khái quát hóa để giám sát và điều khiển một số thiết bị, tất cả trong cùng một vòng lặp.
Chúng tôi chứng minh kỹ thuật này bằng chương trình tiếp theo, C4.2, chương trình này giám sát và điều khiển hai thiết bị, UART và

bàn phím.

Chương trình ví dụ C4.2: Chương trình giám sát và điều khiển 2 thiết bị trong một siêu vòng lặp.

/*********** tc file của Chương trình C4.2 ***********/ #include "keymap"

#define KSTAT 0x04

#define KDATA 0x08

#xác định UDR 0x00

#xác định UFR 0x18

char *ubase,*kbase;

int uart_init(){ ubase = (char *)0x101F1000; }

int kbd_init() { kbase = (char *)0x10006000; }

int chính()

char c, mã;

uart_init(); kbd_init(); // 1. khởi tạo


Machine Translated by Google

4.2 Mô hình siêu vòng lặp 97

trong khi(1){ // siêu vòng lặp

nếu (*(ubase + UFR) & 0x40){ // 2. if UART RxFull

c = *(ubase + UDR); // 3. xử lý đầu vào UART

nếu (c >= 'a' && c <= 'z')

c += ('A' - 'a');

*(ubase + UDR) = c;

nếu (c == '\r')

*(ubase + UDR) = '\n';

if (*(kbase + KSTAT) & 0x10){ // 4. if KBD RxFull

scode = *(kbase + KDATA); // 5. xử lý phím KBD (nhấn)

nếu (mã & 0x80)

Tiếp tục;

c = unsh[scode];

*(ubase + UDR) = c; // echo khóa KBD tới UART

nếu (c == '\r')

*(ubase + UDR) = '\n';

Khi chạy chương trình C4.2, nó lặp lại đầu vào UART0 ở dạng chữ hoa và đầu vào bàn phím ở dạng chữ thường, tất cả thành UART0.

4.3 Mô hình hướng sự kiện

4.3.1 Những hạn chế của chương trình siêu vòng lặp

Hạn chế của mô hình chương trình siêu vòng lặp là chương trình phải liên tục kiểm tra trạng thái của từng thiết bị, ngay cả khi thiết

bị đó chưa sẵn sàng. Điều này không chỉ lãng phí thời gian của CPU mà còn gây tiêu hao điện năng quá mức. Trong một hệ thống nhúng, việc

giảm mức tiêu thụ điện năng thường được mong muốn hơn là tăng mức sử dụng CPU. Thay vì liên tục kiểm tra trạng thái của mọi thiết bị,

một cách khác là đợi thiết bị sẵn sàng. Ví dụ, trong chương trình C4.1, chúng ta có thể thay thế các câu lệnh kiểm tra trạng thái thiết

bị bằng một vòng lặp bận-chờ, như trong

whileðdevice không có dữ liệuÞ;

Nhưng điều này không khắc phục được vấn đề vì CPU vẫn liên tục thực hiện vòng lặp bận chờ. Một nhược điểm khác của sơ đồ này là chương

trình sẽ không thể phản hồi bất kỳ đầu vào KBD nào trong khi đang chờ đầu vào UART và ngược lại.
ngược lại.

4.3.2 Sự kiện

Trong môi trường lập trình, sự kiện là thứ được tạo bởi một nguồn và được người nhận nhận ra, khiến người nhận phải thực hiện hành động

để xử lý sự kiện. Thay vì liên tục kiểm tra đầu vào, một hệ thống nhúng có thể được thiết kế để điều khiển theo sự kiện, tức là nó chỉ

thực hiện hành động để phản hồi lại các sự kiện. Vì lý do này, các hệ thống hướng sự kiện (Cheong và cộng sự 2003; Dunkels và cộng sự

2006) còn được gọi là hệ thống phản ứng. Các sự kiện có thể đồng bộ, nghĩa là chúng xảy ra theo cách có thể dự đoán được hoặc không đồng

bộ, tức là chúng có thể xảy ra bất kỳ lúc nào và theo bất kỳ thứ tự nào. Ví dụ về các sự kiện đồng bộ là các sự kiện định kỳ từ bộ đếm

thời gian, ví dụ khi số đếm bộ đếm thời gian đã đạt đến một giá trị nhất định. Ví dụ về các sự kiện không đồng bộ là đầu vào của người

dùng, chẳng hạn như nhấn phím, nhấp chuột và lật công tắc, v.v. Do tính không thể đoán trước của chúng, cấu trúc chương trình siêu vòng

lặp đơn giản không phù hợp để xử lý các sự kiện không đồng bộ. Trong mô hình lập trình hướng sự kiện, chương trình chính có thể thực thi

theo vòng lặp hoặc ở trạng thái không hoạt động, chờ bất kỳ sự kiện nào xảy ra. Khi một sự kiện xảy ra, trình bắt sự kiện sẽ nhận ra sự

kiện đó và thông báo cho chương trình chính, khiến nó thực hiện hành động thích hợp để xử lý sự kiện. Trong hệ thống nhúng, các sự kiện

thường liên quan đến các ngắt từ thiết bị phần cứng. Trong trường hợp này, một chương trình hướng sự kiện trở thành một chương trình đơn giản
Machine Translated by Google

98 4 mô hình hệ thống nhúng

chương trình điều khiển ngắt. Chúng ta minh họa cấu trúc chương trình điều khiển ngắt bằng hai ví dụ. Ví dụ đầu tiên xử lý các sự

kiện định kỳ và ví dụ thứ hai xử lý các sự kiện không đồng bộ.

4.3.3 Chương trình theo sự kiện định kỳ

Trong ví dụ này, chúng tôi giả sử rằng một hệ thống nhúng bao gồm bộ hẹn giờ và thiết bị hiển thị, ví dụ như màn hình LCD. Bộ hẹn

giờ được lập trình để tạo ra 60 ngắt mỗi giây. Hệ thống phải phản ứng với các sự kiện định thời gian định kỳ sau: Trên mỗi giây, nó

hiển thị đồng hồ treo tường ở định dạng hh:mm:ss trên màn hình LCD. Cứ sau 5 giây, nó lại hiển thị một chuỗi thông báo lên màn hình LCD.

Có hai cách có thể thực hiện một chương trình điều khiển đáp ứng các yêu cầu về thời gian định kỳ này. Nếu các tác vụ cần thực hiện

ngắn, chúng có thể được thực hiện trực tiếp bởi bộ xử lý ngắt hẹn giờ. Trong trường hợp này, sau khi khởi tạo, chương trình chính có

thể thực hiện một vòng lặp không hoạt động. Để giảm mức tiêu thụ điện năng, vòng lặp nhàn rỗi có thể sử dụng Wait-For-Interrupt (WFI)

hoặc lệnh tương đương, đưa CPU vào trạng thái tiết kiệm điện, chờ ngắt. Bo mạch ARM926EJ-S không hỗ trợ lệnh WFI nhưng hầu hết các

bộ xử lý ARM Cortex-5 (ARM Cortex-5 2010) đều triển khai chế độ WFI bằng cách ghi vào bộ đồng xử lý CP15. Sau đây là mã chương trình

của phiên bản đầu tiên của Ví dụ C4.3. Các tác vụ được thực hiện bởi bộ xử lý ngắt hẹn giờ được hiển thị bằng các dòng in đậm.

Phiên bản 1 của C4.3:

/*************** tập tin time.c ****************/

bộ đếm thời gian cấu trúc dễ bay hơi typedef {

u32 *cơ sở; // địa chỉ cơ sở của bộ định thời

int tích tắc, hh, mm, ss; // vùng dữ liệu hẹn giờ

đồng hồ char[16];

}Đồng hồ hẹn giờ;

bộ đếm thời gian TIMER dễ bay hơi; // cấu trúc bộ đếm thời gian

làm trống bộ đếm thời gian_init()

// khởi tạo bộ đếm thời gian0 để tạo 60 ngắt mỗi giây

// time.clock=00:00:00

khoảng trống hẹn giờ_handler() {

TIMER *t = &timer;

t->đánh dấu++; // giả sử 60 tích tắc mỗi giây if (t->tick == 60)

{ // cập nhật ss, mm, hh

t->đánh dấu = 0; t->ss++;

nếu (t->ss == 60){

t->ss = 0; t->mm++;

nếu (t->mm == 60){

t->mm = 0; t->hh++; t->hh %= 24;

if (t->tick == 0){ // mỗi giây, hiển thị đồng hồ treo tường

cho (i=0; i<8; i++)

unkpchar(t->clock[i], 0, 70+i);

t->clock[7]='0'+(t->ss%10); t->clock[6]='0'+(t->ss/10);

t->clock[4]='0'+(t->mm%10); t->đồng hồ[3]='0'+(t->mm/10);

t->clock[1]='0'+(t->hh%10); t->clock[0]='0'+(t->hh/10);

cho (i=0; i<8; i++)

kpchar(t->clock[i], 0, 70+i); // kputchr(char, row, col)

}
Machine Translated by Google

4.3 Mô hình hướng sự kiện 99

if ((t->ss % 5) == 0) // cứ sau 5 giây, hiển thị chuỗi

printf("Sự kiện 5 giây\n");

hẹn giờ_clearInterrupt();

/************** tập tin tc *******************/

#include "timer.c"

#include "vid.c"

#include "interrupts.c" void

IRQ_handler()

int nạn nhân;

// đọc các thanh ghi trạng thái VIC SIV để tìm ra ngắt nào

nạn nhân = VIC_STATUS;

if (vicstatus & (1<<4)){ // time0=bit4

hẹn giờ_handler();

int chính()

fbuf_init(); // Trình điều khiển LCD

VIC_INTENABLE = (1<<4); // kích hoạt bộ đếm thời gian0=bit4

printf("Chương trình sự kiện hẹn giờ bắt đầu\n");

bộ đếm thời gian_init();

bộ đếm thời

gian_start(); printf("Nhập while(1) loop\n", 1234); while(1)

{ asm("MOV

r0, #0; MCR p15,0,R0,c7,c0,4"); // CPU vào trạng thái WFI printf("CPU out trạng thái WFI\n");

Nói chung, trình xử lý ngắt phải càng ngắn càng tốt. Nếu các tác vụ định kỳ dài, ví dụ dài hơn một tích tắc hẹn giờ, thì việc thực hiện các tác vụ

phụ thuộc vào bộ đếm thời gian bên trong bộ xử lý ngắt là điều không mong muốn, trừ khi hệ thống hỗ trợ các ngắt lồng nhau.

Như được trình bày trong Chương. 3, kiến trúc ARM không thể xử lý trực tiếp các ngắt lồng nhau. Trong trường hợp này, tốt hơn là để chương trình chính

thực hiện tất cả các nhiệm vụ. Như trước đây, chương trình chính thực hiện theo vòng lặp. Khi không có sự kiện nào từ bộ hẹn giờ, nó sẽ chuyển sang

trạng thái tiết kiệm năng lượng, chờ lần ngắt tiếp theo. Khi một sự kiện hẹn giờ xảy ra, bộ xử lý ngắt hẹn giờ chỉ cần đặt một biến cờ dễ bay hơi toàn

cục. Sau khi thức dậy từ trạng thái tiết kiệm năng lượng, chương trình chính có thể kiểm tra các biến cờ để có hành động thích hợp.

Trong phiên bản thứ hai của chương trình ví dụ C4.3, nhiệm vụ hiển thị đồng hồ treo tường được viết lại dưới dạng một hàm.

được chương trình chính thực hiện trong mỗi giây. Sau đây là phiên bản thứ hai của chương trình C4.3.

Phiên bản 2 của C4.3: chương trình chính thực hiện các nhiệm vụ định kỳ.

/************ tập tin hẹn giờ mới.c **********/

dễ bay hơi int one_second=0; five_seconds=0; // cờ

khoảng trống hẹn giờ_handler() {

TIMER *t = &timer; t->đánh

dấu++; if (t-

>tick == 60){ t->tick = 0; t-

>ss++;

nếu (t->ss == 60){

t->ss = 0; t->mm++;
Machine Translated by Google

100 4 mô hình hệ thống nhúng

nếu (t->mm == 60){

t->mm = 0; t->hh++;

nếu (t->đánh dấu == 0) // trên mỗi giây

một_giây = 1; // bật cờ one_second

nếu ((t->ss % 5)==0) // cứ sau 5 giây // bật cờ

five_seconds = 1; five_seconds

hẹn giờ_clearInterrupt();

đồng hồ char[16] = "00:00:00";

/***************** tập tin tc ****************/

int wall_clock(TIMER *t)

int i, ss, mm, hh;

int_off();

ss = t->ss; mm = t->mm; hh = t->hh; // sao chép từ cấu trúc bộ đếm thời gian

int_on();

cho (i=0; i<8; i++)

unkpchar(đồng hồ[i], 0, 70+i);

đồng hồ[7]='0'+(ss%10); đồng hồ[6]='0'+(ss/10);

đồng hồ[4]='0'+(mm%10); đồng hồ[3]='0'+(mm/10);

đồng hồ[1]='0'+(hh%10); đồng hồ[0]='0'+(hh/10);

cho (i=0; i<8; i++)

kpchar(đồng hồ[i], 0, 70+i); // kputchr(char, row, col)

int chính()

fbuf_init();

VIC_INTENABLE = (1<<4); // bộ đếm thời gian0 ở bit4

printf("C4.3: Chương trình sự kiện định kỳ\n");

bộ đếm thời gian_init();

bộ đếm thời gian_start();

trong khi(1){

if (one_second)

{ wall_clock(&timer);

một_giây = 0;

nếu (năm_giây){

printf("sự kiện 5 giây\n");

five_seconds = 0;

asm("MOV r0, #0; MCR p15,0,R0,c7,c0,4"); // vào chế độ WFI

printf("CPU thoát ra trạng thái WFI\n");

Hình 4.1 cho thấy kết quả đầu ra khi chạy chương trình C4.3, thể hiện các sự kiện định kỳ.
Machine Translated by Google

4.3 Mô hình hướng sự kiện 101

Hình 4.1 Chương trình hướng sự kiện định kỳ

4.3.4 Chương trình hướng sự kiện không đồng bộ

Các sự kiện không đồng bộ có bản chất không định kỳ, có thể xảy ra bất kỳ lúc nào và theo bất kỳ thứ tự nào. Chương
trình tiếp theo, được ký hiệu là C4.4, thể hiện các sự kiện không định kỳ hoặc không đồng bộ. Trong ví dụ này, chúng tôi
giả sử rằng chương trình giám sát và điều khiển hai thiết bị đầu vào; UART và bàn phím. Chương trình chính cố gắng lấy
dòng đầu vào ở dạng UART hoặc KBD và lặp lại dòng đó. Chương trình này là phiên bản rút gọn của chương trình ví dụ C3.3
của Chap. 3, triển khai trình điều khiển UART và KBD điều khiển ngắt. Đầu tiên, nó khởi tạo các biến cờ toàn cục (dễ bay
hơi), uline và kline về 0. Sau đó, nó liên tục kiểm tra các biến cờ, những biến này sẽ được thiết lập bởi các trình xử
lý ngắt. Khi các phím được nhấn trên thiết bị đầu cuối UART hoặc bàn phím, bộ xử lý ngắt UART hoặc KBD sẽ nhận các phím,
lặp lại chúng và nhập chúng vào bộ đệm đầu vào. Khi nhấn phím ENTER trên một trong hai thiết bị, bộ xử lý ngắt sẽ bật
biến cờ tương ứng để báo hiệu sự kiện xảy ra, cho phép chương trình chính tiếp tục. Khi chương trình chính phát hiện một
sự kiện, nó sẽ trích xuất một dòng từ bộ đệm đầu vào của trình điều khiển thiết bị và lặp lại nó tới màn hình LCD (đối
với đầu vào KBD) hoặc UART. Sau đó, nó xóa biến cờ và tiếp tục vòng lặp. Do tính chất không đồng bộ của các sự kiện, cần
phải tắt các ngắt khi xóa các biến cờ để ngăn tình trạng chạy đua giữa chương trình chính và trình xử lý ngắt.

/** Các tệp uart.c, kbd.c và tc cô đọng của Chương trình C4.4. **/

dễ bay hơi int uline=0, kline=0; // Biến cờ TOÀN CẦU

void uart_handler(UART *up)

u8 mis = *(up->base + MIS); // đọc thanh ghi UART MIS

if (mis & 0x10) do_rx(up);

khác do_tx(lên);

int do_rx(UART *up)

// get và echo input char c; nhập c vào inbuf[](giống như trước)

if (c=='\r'){ // hiện có một dòng đầu vào

uprintf("Trình xử lý ngắt UART: bật cờ uline!\n"); uline = 1;

int do_tx(UART *up){ // xuất một char từ outbuf[ ]; } int ugetc(UART *up){ // trả về

một char từ inbuf[]; } int uputc(UART *up, char c){ // nhập char vào outbuf[]; }

int ugets(UART *up, char *s){// lấy một dòng từ inbuf[]; } int uprints(UART *up, char *s)

{ // in một dòng; } int uprintf(char *fmt, …)/ được định dạng in sang UART; }

// tập tin kbd.c

int kbd_init(){ // Khởi tạo KBD }


Machine Translated by Google

102 4 mô hình hệ thống nhúng

khoảng trống kbd_handler()

// get và echo KBD nhấn phím, nhập key vào buf[ ](giống như trước)

nếu (c=='\r'){

kprintf("Trình xử lý ngắt KBD: bật cờ kline!\n");

kline = 1;

/************ tệp tc của hàm main() *************/

#include "uart.c" // trình điều khiển UART

#include "kbd.c" // trình điều khiển KBD

#include "vid.c" // Trình điều khiển màn hình LCD

#include "ngoại lệ.c"

void IRQ_handler() { // xác

định nguồn IRQ; gọi trình xử lý ngắt UART hoặc KBD; }

int chính()

dòng char[128];

fbuf_init();

uart_init(); // đặt cơ sở UART và kích hoạt các ngắt UART

kbd_init(); // đặt cơ sở KBD và kích hoạt các ngắt KBD // mã để kích hoạt các ngắt

UART và KBD trên VIC và SIC while(1){// chương trình chính: kiểm tra cờ sự kiện; xử lý

sự kiện

nếu (uline){

uget(dòng);

uprintf("UART: line=%s\n", line);

kho a(); uline = 0; mở khóa(); // đặt lại uline về 0

nếu (kline){

được(dòng);

màu = XANH;

printf("KBD: line=%s\n", line);

kho a(); kline = 0; mở khóa(); // đặt lại kline về 0

asm("MOV r0, #0; MCR p15,0,R0,c7,c0,4"); // vào chế độ WFI

Hình 4.2 cho thấy kết quả đầu ra khi chạy chương trình C4.4, thể hiện các sự kiện không đồng bộ.

Hình 4.2 Chương trình hướng sự kiện không đồng bộ


Machine Translated by Google

4.4 Ưu tiên sự kiện 103

4.4 Ưu tiên sự kiện

Trong hệ thống hướng sự kiện, một số sự kiện có thể khẩn cấp hơn những sự kiện khác. Các sự kiện có thể được chỉ định các mức độ ưu tiên

khác nhau tùy theo mức độ khẩn cấp và tầm quan trọng của chúng. Các sự kiện nên được xử lý theo thứ tự ưu tiên của chúng. Có một số cách

để ưu tiên các sự kiện. Đầu tiên, các sự kiện liên quan đến ngắt có thể được bộ điều khiển ngắt chỉ định mức độ ưu tiên khác nhau. Thứ

tự xử lý sự kiện của chương trình chính phải nhất quán với mức độ ưu tiên của các ngắt. Thứ hai, các trình xử lý sự kiện có thể được

triển khai dưới dạng các thực thể thực thi độc lập được gọi là các tiến trình hoặc tác vụ, có thể được lên lịch để chạy theo mức độ ưu

tiên. Chúng tôi sẽ giải thích mô hình lập trình quy trình trong phần tiếp theo. Ở đây, chúng tôi giải thích ngắn gọn sự cần thiết của

các quy trình trước tiên. Các chương trình ví dụ của C4.3 và C4.4 có ba thiếu sót chính. Đầu tiên, để phản hồi nhanh với các ngắt, trình

xử lý ngắt phải càng ngắn càng tốt. Điều này đặc biệt quan trọng đối với các ngắt hẹn giờ để không làm mất các dấu tích của bộ đếm thời

gian hoặc yêu cầu các ngắt lồng nhau. Ngay cả đối với các sự kiện định kỳ, chúng phải được xử lý bởi chương trình chính chứ không phải

trong trình xử lý ngắt hẹn giờ. Thứ hai, khi ở trạng thái tiết kiệm năng lượng, chương trình chính vẫn cần phải thực hiện vòng lặp trên

mỗi ngắt, ngay cả khi các sự kiện được chờ đợi, ví dụ như một dòng đầu vào hoàn chỉnh, vẫn chưa xảy ra. Sẽ tốt hơn nếu chương trình chỉ

có thể thoát khỏi trạng thái tiết kiệm năng lượng khi cần thiết. Bằng cách này, nó không phải thăm dò mọi sự kiện khi nó chạy. Thứ ba,

các sự kiện không bị giới hạn ở đầu vào của người dùng hoặc sự gián đoạn của thiết bị. Chúng có thể bắt nguồn từ các thực thể thực thi

khác trong hệ thống như một phương tiện đồng bộ hóa và liên lạc. Để thực hiện được những điều này, cần phải kết hợp khái niệm về quy

trình hoặc nhiệm vụ vào hệ thống. Điều này dẫn chúng ta đến mô hình quy trình cho các hệ thống nhúng.

4.5 Mô hình quy trình

Trong mô hình quy trình, một hệ thống nhúng bao gồm nhiều quy trình đồng thời. Một quy trình là một thực thể thực thi có thể được lên

lịch để chạy, tạm dừng chạy (và nhường CPU cho các quy trình khác) và tiếp tục chạy lại, v.v. Mỗi quy trình là một đơn vị thực thi độc

lập được thiết kế để thực hiện một tác vụ cụ thể. Tùy thuộc vào môi trường thực thi của các tiến trình, mô hình tiến trình có thể được

phân thành nhiều mô hình con.

4.5.1 Mô hình quy trình đơn xử lý

Hệ thống đơn bộ xử lý (UP) chỉ bao gồm một CPU. Trong hệ thống UP, các tiến trình chạy đồng thời trên cùng một CPU thông qua đa nhiệm.

4.5.2 Mô hình quy trình đa bộ xử lý

Hệ thống đa bộ xử lý (MP) bao gồm nhiều số lượng CPU, bao gồm cả bộ xử lý đa lõi. Trong hệ thống MP, các tiến trình có thể chạy song

song trên các CPU khác nhau. Ngoài ra, mỗi CPU hoặc lõi xử lý cũng có thể chạy các tiến trình thông qua đa nhiệm. Hệ thống MP sẽ được

đề cập trong Chương. 9.

4.5.3 Mô hình quy trình không gian địa chỉ thực

Trong mô hình không gian địa chỉ thực, hệ thống không được trang bị hoặc không sử dụng phần cứng quản lý bộ nhớ do hạn chế về thời gian.

Nếu không có phần cứng quản lý bộ nhớ để cung cấp ánh xạ địa chỉ, tất cả các tiến trình sẽ chạy trong cùng một không gian địa chỉ thực

của nhân hệ thống. Hạn chế của mô hình này là thiếu bảo vệ bộ nhớ. Ưu điểm chính của nó là tính đơn giản, ít yêu cầu tài nguyên phần

cứng và hiệu quả cao.

4.5.4 Mô hình quy trình không gian địa chỉ ảo

Trong mô hình không gian địa chỉ ảo, hệ thống sử dụng phần cứng quản lý bộ nhớ để cung cấp cho mỗi tiến trình một không gian địa chỉ ảo

duy nhất thông qua ánh xạ địa chỉ. Các tiến trình có thể chạy ở chế độ kernel hoặc chế độ người dùng. Trong khi ở kernel
Machine Translated by Google

104 4 mô hình hệ thống nhúng

chế độ, tất cả các tiến trình chia sẻ cùng một không gian địa chỉ của kernel. Khi ở chế độ người dùng, mỗi quy trình có một không gian địa chỉ

ảo riêng biệt được cách ly và bảo vệ khỏi các quy trình khác.

4.5.5 Mô hình quy trình tĩnh

Trong mô hình quy trình tĩnh, tất cả các quy trình được tạo khi hệ thống khởi động và chúng tồn tại vĩnh viễn trong hệ thống.

Mỗi quá trình có thể được thực hiện định kỳ hoặc theo sự kiện. Lập kế hoạch quy trình thường theo mức độ ưu tiên quy trình tĩnh mà không có quyền

ưu tiên, tức là mỗi quy trình chạy cho đến khi nó tự nguyện từ bỏ CPU.

4.5.6 Mô hình quy trình động

Trong mô hình quy trình động, các quy trình có thể được tạo động để thực hiện các nhiệm vụ cụ thể theo yêu cầu. Khi một tiến trình hoàn thành

nhiệm vụ của nó, nó sẽ kết thúc và giải phóng tất cả tài nguyên trở lại hệ thống để tái sử dụng.

4.5.7 Mô hình quy trình không ưu tiên

Trong mô hình quy trình không được ưu tiên, mỗi quy trình sẽ chạy cho đến khi nó tự nguyện nhường CPU, ví dụ: khi một quy trình chuyển sang chế

độ ngủ, tự tạm dừng hoặc nhường CPU cho một quy trình khác một cách rõ ràng.

4.5.8 Mô hình quy trình ưu tiên

Trong mô hình quy trình ưu tiên, CPU có thể được lấy ra khỏi quy trình để chạy quy trình khác bất kỳ lúc nào.

Các phân loại mô hình quy trình nêu trên không loại trừ lẫn nhau. Tùy thuộc vào ứng dụng, một hệ thống nhúng có thể được thiết kế dưới dạng

hỗn hợp các mô hình quy trình thích hợp. Ví dụ, hầu hết các hệ thống nhúng hiện có có thể được phân loại thành các loại sau.

4.6 Mô hình hạt nhân bộ xử lý đơn (UP)

Trong mô hình này, hệ thống chỉ có một CPU không có phần cứng quản lý bộ nhớ để ánh xạ địa chỉ. Tất cả các tiến trình đều chạy trong cùng một

không gian địa chỉ của kernel. Các tiến trình có thể là tĩnh hoặc động. Các quy trình được lên lịch theo mức độ ưu tiên tĩnh mà không có quyền

ưu tiên. Hầu hết các hệ thống nhúng đơn giản đều phù hợp với mô hình này. Hệ thống thu được tương đương với hạt nhân không được ưu tiên của hệ

điều hành. Chúng ta sẽ thảo luận về mô hình hệ thống này chi tiết hơn ở Chương. 5.

4.7 Mô hình hệ điều hành Uniprocessor (UP)

Đây là phần mở rộng của mô hình UP Kernel. Trong mô hình này, hệ thống sử dụng phần cứng quản lý bộ nhớ để hỗ trợ ánh xạ địa chỉ, do đó cung cấp

cho mỗi tiến trình một không gian địa chỉ ảo duy nhất. Mỗi tiến trình chạy ở chế độ kernel hoặc chế độ người dùng riêng biệt. Khi ở chế độ

kernel, tất cả các tiến trình đều chạy trong cùng một không gian địa chỉ của kernel. Khi ở chế độ người dùng, mỗi quy trình sẽ thực thi trong

một không gian địa chỉ riêng, được cách ly và bảo vệ khỏi các quy trình khác. Các tiến trình chỉ chia sẻ các đối tượng dữ liệu chung trong không

gian hạt nhân được bảo vệ. Khi ở chế độ kernel, một tiến trình sẽ chạy cho đến khi nó tự nguyện từ bỏ CPU mà không có quyền ưu tiên. Khi ở chế

độ người dùng, một quy trình có thể được ưu tiên nhường CPU cho quy trình khác có mức độ ưu tiên cao hơn. Hệ thống như vậy tương đương với hệ

điều hành UP có mục đích chung. Chúng ta sẽ thảo luận về hệ điều hành có mục đích chung chi tiết hơn ở Chương. 7.
Machine Translated by Google

4.8 Mô hình hệ thống đa bộ xử lý (MP) 105

4.8 Mô hình hệ thống đa bộ xử lý (MP)

Trong mô hình này, hệ thống bao gồm nhiều CPU hoặc lõi xử lý, dùng chung bộ nhớ vật lý.

Trong hệ thống MP, các tiến trình có thể chạy song song trên các CPU khác nhau. So với các hệ thống UP, hệ thống MP yêu cầu các công

cụ và kỹ thuật lập trình đồng thời tiên tiến để đồng bộ hóa và bảo vệ quy trình. Chúng ta sẽ thảo luận về hệ thống MP ở Chương. 9.

4.9 Mô hình hệ thống thời gian thực (RT)

Nói chung, tất cả các hệ thống nhúng đều được thiết kế với một số yêu cầu về thời gian, chẳng hạn như phản hồi nhanh với các ngắt
và thời gian hoàn thành xử lý ngắt ngắn, v.v. Hệ thống nhúng chỉ có thể sử dụng các yêu cầu về thời gian này làm hướng dẫn nhưng
không đảm bảo rằng các yêu cầu này luôn có thể đạt được . Ngược lại, một hệ thống nhúng dành cho các ứng dụng thời gian thực phải
đáp ứng các yêu cầu rất nghiêm ngặt về thời gian, chẳng hạn như đảm bảo thời gian phản hồi tối thiểu đối với các gián đoạn và hoàn
thành mọi dịch vụ được yêu cầu trong thời hạn quy định. Môi trường này tương đương với một hệ thống thời gian thực. Chúng ta sẽ
thảo luận về các hệ thống nhúng thời gian thực trong Chương. 10.

4.10 Phương pháp thiết kế phần mềm hệ thống nhúng

Khi các hệ thống nhúng trở nên phức tạp hơn bao giờ hết, thiết kế phần mềm truyền thống cho các hệ thống nhúng theo cách tiếp cận đặc

biệt không còn phù hợp nữa. Kết quả là, có nhiều phương pháp thiết kế chính thức được đề xuất cho thiết kế phần mềm hệ thống nhúng,

bao gồm

4.10.1 Hỗ trợ ngôn ngữ cấp cao cho lập trình hướng sự kiện

Phương pháp thiết kế này tập trung vào việc sử dụng sự hỗ trợ các sự kiện và ngoại lệ của các ngôn ngữ lập trình cấp cao, chẳng hạn

như JAVA và C++, làm mô hình để phát triển các chương trình hướng sự kiện cho các hệ thống nhúng. Công việc tiêu biểu trong lĩnh vực

này là mô hình nhiệm vụ cho lập trình hướng sự kiện (Fischer và cộng sự 2007).

4.10.2 Model máy trạng thái

Phương pháp thiết kế này xử lý phần mềm hệ thống nhúng như một máy trạng thái hữu hạn (FSM) (Edwards et al. 1997; Gajski et al.

1994). Máy trạng thái hữu hạn (FSM) là một hệ thống

FSM = {S, X, O, f}, trong đó S = tập hữu hạn các trạng thái,

X = một tập hợp hữu hạn các đầu vào,

O = một tập hợp đầu ra hữu hạn,

f là hàm chuyển trạng thái, ánh xạ SXI vào SX O.

Với mỗi cặp (trạng thái, đầu vào) = (s, x), f(s, x) = (s', o), trong đó s' là trạng thái tiếp theo của s và o là đầu ra được tạo

trong quá trình chuyển đổi trạng thái . Một FSM được chỉ định đầy đủ nếu f(s, x) được xác định cho mọi cặp (s, x). Một FSM mang tính

quyết định nếu với mỗi cặp (s, x), f(s, x) là duy nhất. FSM thuộc mô hình Mealy (Katz và Borriello 2005) nếu đầu ra phụ thuộc vào đầu

vào. FSM thuộc mô hình Moore nếu đầu ra chỉ phụ thuộc vào trạng thái.

Phương pháp thiết kế máy trạng thái mô hình hóa các thông số kỹ thuật của hệ thống nhúng bằng các FSM mô hình Mealy được chỉ định

đầy đủ và xác định, cho phép xác minh chính thức các hệ thống kết quả. Nó cũng khai thác các tính năng của ngôn ngữ lập trình để dịch

các máy trạng thái thành mã chương trình. Chúng tôi minh họa mô hình thiết kế máy trạng thái bằng một ví dụ.

Chương trình ví dụ C4.5: Giả sử rằng các dòng chú thích trong chương trình C bắt đầu bằng hai ký hiệu / liền kề và kết thúc trên cùng

một dòng. Thiết kế một hệ thống nhúng lấy các tệp nguồn chương trình C làm đầu vào và xóa các dòng chú thích khỏi chương trình C.

Thiết kế và triển khai hệ thống như vậy dựa trên mô hình FSM bao gồm ba bước.
Machine Translated by Google

106 4 mô hình hệ thống nhúng

Bước 1: Xây dựng bảng trạng thái FSM: Hệ thống có thể được mô hình hóa bằng FSM với 5 trạng thái.

S0 = trạng thái ban đầu, chưa thấy bất kỳ đầu vào nào

S1 = chưa thấy biểu tượng / nào

S2 = đã nhìn thấy biểu tượng / đầu tiên

S3 = đã thấy 2 ký hiệu // liền kề


S4 = trạng thái cuối cùng hoặc kết thúc

Mặc dù mỗi đầu vào là một ký tự đơn, chúng ta sẽ phân loại các ký tự đầu vào thành các trường hợp khác nhau, được coi là đầu vào riêng biệt của hệ

thống. Vì vậy, chúng tôi xác định đầu vào là

x1 = '/'

x2 = '\n'

x3 = không có trong {'/', \n',

EOF} x4 = EOF (cuối tập tin)

Khi hệ thống ở trạng thái, mỗi đầu vào sẽ chuyển trạng thái sang trạng thái tiếp theo và tạo ra đầu ra (chuỗi). Một FSM có thể được biểu diễn bằng

một bảng trạng thái, trong đó chỉ định các chuyển đổi trạng thái và đầu ra do mỗi đầu vào. Trong ví dụ này, bảng trạng thái ban đầu của FSM được

hiển thị trong Bảng 4.1, trong đó ký hiệu đầu ra - biểu thị chuỗi rỗng.

Trong bảng trạng thái, S0 là trạng thái ban đầu, biểu thị điều kiện hệ thống chưa thấy đầu vào nào và S4 là trạng thái cuối cùng hoặc trạng thái

kết thúc, trong đó hệ thống đã hoàn thành nhiệm vụ và dừng lại. Bảng trạng thái ban đầu được xây dựng phù hợp với đặc tả bài toán. Bắt đầu từ trạng

thái ban đầu S0, nếu đầu vào là '/', thì nó sẽ chuyển sang trạng thái S2, biểu thị điều kiện là hệ thống đã nhìn thấy '/' đầu tiên và tạo ra chuỗi

đầu ra rỗng. Điều này là do '/' này có thể là điểm bắt đầu của dòng nhận xét. Nếu vậy, nó không nên được phát ra như một phần của đầu ra. Nếu đầu

vào là '\n', nó sẽ chuyển sang trạng thái S1 và tạo ra chuỗi đầu ra "\n". Nếu đầu vào không phải là '/' hoặc '\n' hoặc EOF, nó cũng sẽ chuyển đến

S1 và tạo ra một chuỗi đầu ra chứa cùng một ký tự đầu vào. Nếu đầu vào là EOF, nó sẽ chuyển sang trạng thái cuối cùng S4 với chuỗi đầu ra rỗng và

kết thúc. Khi ở trạng thái S2, nếu đầu vào x không phải là '/' hoặc\n hoặc EOF, nó sẽ quay trở lại S1 và tạo chuỗi đầu ra "/x". Điều này là do ký tự

'/' theo sau là ký tự thông thường không phải là dòng nhận xét mà phải là một phần của chuỗi đầu ra. Các mục khác của bảng trạng thái được xây dựng

theo cách tương tự.

Bước 2: Tối thiểu hóa bảng trạng thái: Khi xây dựng bảng trạng thái ban đầu từ đặc tả vấn đề, tùy thuộc vào cách xác định các trạng thái theo cảm

nhận của người thiết kế hệ thống, số lượng trạng thái có thể nhiều hơn mức thực sự cần thiết.

Vì vậy, bảng trạng thái ban đầu có thể không tối thiểu. Ví dụ, nếu chúng ta coi trạng thái cuối cùng S4 là điều kiện mặc định để hệ thống kết thúc

thì S4 là dư thừa và có thể loại bỏ được. Bảng trạng thái ban đầu cũng có thể chứa nhiều trạng thái thực sự tương đương. Bước thứ hai trong mô hình

thiết kế FSM là giảm thiểu bảng trạng thái bằng cách loại bỏ các trạng thái dư thừa và tương đương. Để làm được điều này, trước tiên chúng ta phải

làm rõ ý nghĩa của các trạng thái tương đương.

(1) Quan hệ tương đương: Quan hệ tương đương R là quan hệ nhị phân áp dụng cho một tập đối tượng, được

Bảng 4.1 Bảng trạng thái ban đầu của FSM


Machine Translated by Google

4.10 Phương pháp thiết kế phần mềm hệ thống nhúng 107

Phản xạ: với mọi đối tượng x, x R x đều đúng.

Đối xứng: với mọi đối tượng x, y, x R y suy ra y R x.

Chuyển tiếp: với mọi đối tượng x, y, z, x R y và y R z bao hàm x R z.

Ví dụ: quan hệ = của số thực là quan hệ tương đương. Tương tự, với mọi số nguyên N > 0, modulo-N

(% N) quan hệ của số nguyên không âm cũng là quan hệ tương đương.

(2) Lớp tương đương: Một quan hệ tương đương có thể được sử dụng để phân chia (chia) một tập hợp thành các lớp tương đương sao cho tất cả các đối

tượng trong cùng một lớp đều tương đương nhau. Kết quả là mỗi lớp tương đương có thể được biểu diễn bằng một đối tượng duy nhất của lớp đó.

Ví dụ: Khi áp dụng mối quan hệ % 10 cho tập hợp các số nguyên không âm, nó sẽ phân chia tập hợp đó thành các lớp tương đương {0}–{9}. Mỗi lớp

{i} bao gồm tất cả các số nguyên mang lại phần dư của i khi chia cho 10. Chúng ta có thể sử dụng 0–9 để biểu thị các lớp tương đương {0}–{9}.

(3). Các trạng thái tương đương: Trong một FSM, hai trạng thái Si và Sj là tương đương nếu với mọi đầu vào x,

đầu ra của chúng giống hệt nhau và trạng thái tiếp theo của chúng là tương đương.

Lưu ý rằng định nghĩa về các trạng thái tương đương chỉ yêu cầu rằng, đối với mỗi đầu vào, đầu ra của chúng phải giống nhau chứ không phải các

trạng thái tiếp theo, chỉ cần tương đương. Điều này dường như tạo ra vấn đề quả trứng gà, nhưng chúng ta có thể xử lý nó một cách dễ dàng, như sẽ

được trình bày ngay sau đây.

(4) Giảm thiểu bảng trạng thái: Bước này cố gắng giảm số lượng trạng thái trong bảng trạng thái FSM xuống mức tối thiểu. Mặc dù khó có thể tìm trực

tiếp tất cả các trạng thái tương đương trong bảng trạng thái, nhưng rất dễ dàng phát hiện các cặp trạng thái không thể tương đương dựa trên

kết quả đầu ra của chúng. Trong logic cơ bản, chúng ta biết rằng

nếu A thì B tương đương với nếu không B ½ thì không A ½

Khi cố gắng chứng minh điều gì đó như "nếu A thì B", chúng ta có thể tấn công từ phía trước bằng cách chứng minh rằng "nếu A đúng thì B phải

đúng" hoặc từ phía sau bằng cách chứng minh rằng "nếu B không đúng, thì A không thể đúng" Chiến lược tấn công từ phía sau thường được gọi là chứng

minh bằng mâu thuẫn, được sử dụng trong hầu hết các chứng minh thuộc lý thuyết tính toán trong khoa học máy tính. Vì vậy, thay vì cố gắng xác định

các trạng thái tương đương trong bảng trạng thái, chúng ta sẽ sử dụng chiến lược cố gắng xác định và loại bỏ tất cả các trạng thái không tương

đương. Lược đồ này được triển khai bằng Biểu đồ hàm ý, đây là một bảng chứa tất cả các cặp trạng thái trong bảng trạng thái. Trong biểu đồ hàm ý,

mỗi ô tương ứng với một cặp trạng thái (Si, Sj). Vì biểu đồ hàm ý có tính đối xứng và tất cả các ô đường chéo (Si, Si) rõ ràng là các trạng thái

tương đương nên chỉ cần hiển thị nửa dưới của biểu đồ, không có các ô đường chéo là đủ. Thuật toán xác định các cặp trạng thái không tương đương

trong biểu đồ hàm ý như sau.

(1) Sử dụng kết quả đầu ra của các trạng thái để loại bỏ bất kỳ ô nào (Si, Sj) không thể tương đương.

(2) Đối với mỗi ô không bị gạch chéo (Si, Sj), hãy kiểm tra các cặp trạng thái tiếp theo của chúng (Si', Sj') dưới mỗi đầu vào. Gạch bỏ ô (Si, Sj)

nếu bất kỳ cặp trạng thái tiếp theo nào (Si', Sj') của chúng bị gạch bỏ.

(3) Lặp lại (2) cho đến khi không còn ô cặp trạng thái nào có thể gạch bỏ

Khi thuật toán kết thúc, mỗi ô không bị gạch chéo (Si, Sj) xác định một cặp trạng thái tương đương. Sau đó, sử dụng thuộc tính bắc cầu của các

cặp trạng thái tương đương để xây dựng các lớp tương đương.

Trong ví dụ của chúng tôi, trước tiên chúng tôi xây dựng một biểu đồ hàm ý và gạch bỏ tất cả các ô của các cặp trạng thái không tương đương. Ví

dụ, rõ ràng là S0 và S2 không thể tương đương vì đầu ra của chúng không giống nhau đối với mọi đầu vào. Vì vậy chúng ta gạch bỏ ô của (S0, S2). Vì

lý do tương tự, chúng ta có thể gạch bỏ ô của (S0, S3). Tương tự như vậy, chúng ta có thể gạch bỏ các ô của (S1, S2), (S1, S3) và (S2, S3). Bảng

4.2 cho thấy giỏ hàng hàm ý sau khi áp dụng (1) để gạch bỏ các ô có cặp trạng thái không tương đương.
Machine Translated by Google

108 4 mô hình hệ thống nhúng

Bảng 4.2 Biểu đồ hàm ý ban đầu của FSM

Bảng 4.3 Biểu đồ hàm ý của FSM

Từ biểu đồ hàm ý ban đầu của Bảng 4.2, chúng ta điền vào mỗi ô không bị gạch bỏ các cặp trạng thái tiếp theo, được hiển
thị trong ô (S0, S1) của Bảng 4.3.
Sau đó, chúng tôi áp dụng bước (2) của thuật toán, cố gắng gạch bỏ bất kỳ ô nào chứa các cặp trạng thái đã bị gạch bỏ.
Trong trường hợp này, không có. Thế là thuật toán kết thúc. Biểu đồ hàm ý cuối cùng của Bảng 4.3 cho thấy (S0, S1) là các
trạng thái tương đương, có thể kết hợp thành một trạng thái duy nhất.
Quá trình xác định và loại bỏ các trạng thái tương đương trong các bảng trạng thái được gọi là bài toán tối thiểu hóa FSM,
đã được nghiên cứu kỹ lưỡng trong việc thiết kế các máy trạng thái hữu hạn (Katz và Borriello 2005). Chỉ cần nói rằng chúng
ta luôn có thể giảm bảng trạng thái FSM xác định và được chỉ định đầy đủ về dạng tối thiểu, duy nhất cho đến đẳng cấu (bằng
cách đổi tên các trạng thái). Hơn nữa, thuật toán chỉ yêu cầu thời gian tính toán đa thức. Đối với ví dụ này, bảng trạng thái
tối thiểu được hiển thị trong Bảng 4.4, chỉ có 3 trạng thái không tương đương.
Machine Translated by Google

4.10 Phương pháp thiết kế phần mềm hệ thống nhúng 109

Bảng 4.4 Bảng trạng thái tối thiểu của FSM

Sơ đồ trạng thái của FSM là một đồ thị có hướng, trong đó mỗi nút biểu diễn một trạng thái và một cung có dạng từ Si đến Sj, ký hiệu là

bởi Si->Sj, thể hiện sự chuyển trạng thái từ trạng thái Si sang trạng thái Sj. Cung được đánh dấu bằng tất cả các cặp đầu vào/đầu ra gây ra

chuyển trạng thái. Các bảng trạng thái và sơ đồ trạng thái tương đương nhau theo nghĩa là chúng truyền tải thông tin giống hệt nhau. Các

Người đọc có thể vẽ sơ đồ trạng thái cho bảng trạng thái trong Bảng 4.4. Điều này được để lại như một bài tập.

Bước 3: Dịch Bảng trạng thái/Sơ đồ trạng thái thành Mã: Bảng trạng thái hoặc sơ đồ trạng thái có thể được dịch sang mã C

gần như trực tiếp. Sử dụng câu lệnh switch-case của C, mỗi trạng thái tương ứng với một trường hợp riêng biệt trong câu lệnh switch bên ngoài,

và mỗi đầu vào tương ứng với một trường hợp riêng biệt trong câu lệnh switch bên trong. Chúng tôi minh họa bản dịch bằng chữ C hoàn chỉnh

chương trình mô phỏng hệ thống nhúng dự định.

/****************** Chương trình ví dụ C4.5 *******************/

#include <stdio.h>

int chính()

int c; // ký tự đầu vào

trạng thái int = 1; // trạng thái hiện tại ban đầu = S1

TỆP *fp = fopen("cprogram.c", "r"); // đầu vào là chương trình C

while((c=fgetc(fp))!= EOF){ // if EOF: chấm dứt

chuyển đổi (trạng thái) { // chuyển đổi dựa trên trạng thái hiện tại

trường hợp 1: // trạng thái S1

chuyển đổi (c) { // trạng thái/đầu ra tiếp theo

trường hợp '/' : trạng thái = 2; phá vỡ;

trường hợp '\n': trạng thái = 1; printf("%c", c); phá vỡ;

mặc định: trạng thái = 1; printf("%c", c); phá vỡ;

}; phá vỡ;

trường hợp 2: // trạng thái S2

chuyển đổi (c) { // trạng thái/đầu ra tiếp theo

trường hợp '/' : trạng thái = 3; phá vỡ;

trường hợp '\n': trạng thái = 1; printf("/%c", c); phá vỡ;

mặc định: trạng thái = 1; printf("/%c", c); phá vỡ;

}; phá vỡ;

trường hợp 3: // trạng thái S3

chuyển đổi (c) { // trạng thái/đầu ra tiếp theo

trường hợp '/' : trạng thái = 3; phá vỡ;

trường hợp '\n': trạng thái = 1; printf("%c", c); phá vỡ;

mặc định: trạng thái = 3; phá vỡ;

};

Người đọc có thể biên dịch và chạy chương trình C4.5 trên trong Linux trên các tệp nguồn C sử dụng // làm dòng chú thích.

Kết quả đầu ra sẽ cho thấy rằng nó xóa tất cả các dòng nhận xét khỏi tệp nguồn C. Người đọc cũng có thể tham khảo Bài toán 4.2 để

xử lý một lỗi thiết kế nhỏ của chương trình.


Machine Translated by Google

110 4 mô hình hệ thống nhúng

Cần lưu ý rằng, khi dịch bảng trạng thái hoặc sơ đồ trạng thái FSM thành mã, mã C thu được có thể không đẹp và hiệu quả lắm (về kích

thước mã), nhưng quá trình dịch thuật gần như cơ học, có thể được tự động hóa nếu cần. .

Nó làm cho bước mã hóa trở thành một nỗ lực kỹ thuật hơn là một nghệ thuật lập trình. Đây là tài sản mạnh nhất của mô hình FSM. Tuy nhiên,

mô hình FSM có hạn chế là số lượng trạng thái không thể quá lớn. Trong khi việc xử lý các vấn đề chỉ với một vài trạng thái có thể khá dễ

dàng thì việc quản lý các bảng trạng thái hoặc sơ đồ trạng thái với hàng trăm trạng thái sẽ rất khó khăn. Vì lý do này, việc thiết kế và

triển khai một hệ điều hành hoàn chỉnh theo mô hình FSM là không thực tế và gần như không thể.

4.10.3 Mô hình biểu đồ trạng thái

Mô hình StateChart (Franke B 2016) dựa trên mô hình máy trạng thái hữu hạn. Nó bổ sung tính đồng thời và liên lạc giữa các thực thể thực

thi đồng thời. Nó được thiết kế để mô hình hóa các hệ thống nhúng phức tạp với các nhiệm vụ đồng thời.

Vì mô hình này liên quan đến các khái niệm nâng cao về các quy trình đồng thời, đồng bộ hóa quy trình và giao tiếp giữa các quy trình nên

chúng ta sẽ không thảo luận thêm về nó.

4.11 Tóm tắt

Chương này bao gồm các mô hình của hệ thống nhúng. Nó giải thích mô hình hệ thống siêu vòng lặp đơn giản và chỉ ra những thiếu sót của nó.

Nó thảo luận về mô hình hướng sự kiện và trình diễn các mô hình hệ thống hướng sự kiện định kỳ và không đồng bộ bằng các chương trình mẫu.

Sau đó, nó chứng minh sự cần thiết của các quy trình hoặc nhiệm vụ trong các hệ thống nhúng và thảo luận về các mô hình quy trình khác

nhau. Cuối cùng, nó đã giới thiệu một số phương pháp thiết kế chính thức cho các hệ thống nhúng và nó minh họa mô hình FSM bằng một ví dụ

về thiết kế và triển khai chi tiết.

CÁC VẤN ĐỀ

1. Trong chương trình ví dụ C4.3, sau khi khởi tạo hệ thống, hàm main() sẽ thực thi vòng lặp while(1):

int chính()

// khởi tạo1zation

trong khi(1){

asm("MOV r0, #0; MCR p15,0,R0,c7,c0,4");

printf("CPU ra khỏi trạng thái WFI\n");

(1) Nhận xét dòng asm. Chạy lại chương trình để xem điều gì sẽ xảy ra.

(2) Về mức tiêu thụ điện năng của CPU, câu lệnh asm có gì khác biệt?

2. Trong Chương trình ví dụ C4.5, giả sử rằng một dòng chú thích bắt đầu bằng 2 ký hiệu / liền kề ở cuối dòng.

Tuy nhiên, các hằng chuỗi được đặt trong các cặp dấu ngoặc kép trùng khớp có thể chứa bất kỳ số ký hiệu / nào nhưng chúng không phải là

dòng chú thích, ví dụ printf("this // is not a /// comment line\n"); Sửa đổi bảng trạng thái của chương trình C4.5 để xử lý trường hợp

này. Dịch bảng trạng thái hoặc sơ đồ trạng thái đã sửa đổi sang mã C và chạy chương trình đã sửa đổi để kiểm tra xem nó có hoạt động

chính xác hay không.

3. Giả sử rằng các khối chú thích trong chương trình C bắt đầu bằng /* và kết thúc bằng */. Các khối nhận xét lồng nhau không được phép,

điều này sẽ dẫn đến lỗi. Viết chương trình C để phát hiện và xóa các khối nhận xét khỏi tệp nguồn chương trình C.
Machine Translated by Google

4.11 Tóm tắt 111

(1) Lập mô hình chương trình dưới dạng FSM và xây dựng sơ đồ trạng thái cho FSM.
(2) Viết mã C để triển khai FSM dưới dạng hệ thống hướng sự kiện.

Người giới thiệu

Android, https://en.wikipedia.org/wiki/Android_(operating_system), 2016.


ARM Cortex-5, Bộ xử lý ARM Cortex-A5, Trung tâm thông tin ARM, 2010.
Cheong, E, Liebman, J, Liu, J, Zhao, F, "TinyGALS: Mô hình lập trình cho các hệ thống nhúng hướng sự kiện", hội nghị chuyên đề ACM về Ứng dụng
Máy tính, 2003.
Dunkels, A, Schmidt, O, Voigt, T, Ali, "MProtothreads: đơn giản hóa việc lập trình theo hướng sự kiện của các hệ thống nhúng bị hạn chế về bộ nhớ",
SenSys '06 Proc. của hội nghị quốc tế lần thứ 4 về Hệ thống cảm biến nối mạng nhúng, 2006.
Edwards, S, Lavagno, L, Lee, EA, Sangiovanni-Vincentelli, A, "Thiết kế hệ thống nhúng: Mô hình chính thức, Xác thực và Tổng hợp", Proc.
của IEEE, Tập. 85, Số 3, tháng 3 năm 1997, PP366–390.
Franke, B, "Hệ thống nhúng Bài giảng 4: Biểu đồ trạng thái, Đại học Edinburgh.
Fischer, J, Majumdar, R, Millstein, T, "Nhiệm vụ: Hỗ trợ ngôn ngữ cho hướng sự kiện.
Lập trình", Hội thảo ACM SIGPLAN 2007 về PEPM, 2007.
Gajski, DD, Vahid, F, Narayan, S, Gong, J, "Đặc điểm kỹ thuật và thiết kế của hệ thống nhúng", PTR Prentice Hall, 1994.
Katz, RH và G. Borriello, "Thiết kế logic đương đại", tái bản lần thứ 2, Pearson, 2005.
Machine Translated by Google

Quản lý quy trình trong hệ thống nhúng


5

5.1 Đa nhiệm

Nói chung, đa nhiệm đề cập đến khả năng thực hiện một số hoạt động độc lập cùng một lúc. Ví dụ, chúng ta thường thấy mọi người nói chuyện

trên điện thoại di động khi đang lái xe. Theo một nghĩa nào đó, những người này đang làm nhiều việc cùng một lúc, mặc dù đây là một việc

rất nguy hiểm. Trong điện toán, đa nhiệm đề cập đến việc thực hiện một số nhiệm vụ độc lập cùng một lúc. Trong hệ thống CPU hoặc bộ đơn xử

lý (UP), mỗi lần chỉ có thể thực thi một tác vụ. Đa nhiệm đạt được bằng cách ghép thời gian thực thi của CPU giữa các tác vụ khác nhau, tức

là bằng cách chuyển CPU từ tác vụ này sang tác vụ khác. Nếu chuyển đổi đủ nhanh, nó sẽ tạo ra ảo giác rằng tất cả các tác vụ đang được thực

thi đồng thời. Sự song song logic này được gọi là sự tương tranh. Trong hệ thống đa bộ xử lý (MP), các tác vụ có thể thực thi song song

trên các CPU khác nhau trong thời gian thực. Ngoài ra, mỗi bộ xử lý cũng có thể thực hiện đa nhiệm bằng cách thực hiện đồng thời các tác vụ

khác nhau. Đa nhiệm là nền tảng của tất cả các hệ điều hành, cũng như nền tảng của lập trình đồng thời nói chung. Để đơn giản, trước tiên

chúng ta sẽ xem xét các hệ thống bộ xử lý đơn (UP). Hệ thống MP sẽ được đề cập sau trong Chương. 9 trên các hệ thống đa bộ xử lý.

5.2 Khái niệm quy trình

Một hệ thống đa nhiệm hỗ trợ thực hiện đồng thời nhiều tiến trình. Trung tâm của hệ thống đa nhiệm là một chương trình điều khiển, được gọi

là nhân hệ điều hành (OS), cung cấp các chức năng để quản lý quy trình. Trong hệ thống đa nhiệm, các tiến trình còn được gọi là nhiệm vụ.

Đối với tất cả các mục đích thực tế, thuật ngữ quy trình và nhiệm vụ có thể được sử dụng thay thế cho nhau.

Đầu tiên, chúng tôi xác định hình ảnh thực thi là vùng bộ nhớ chứa mã, dữ liệu và ngăn xếp của thực thi. Về mặt hình thức, một quy trình là

việc thực hiện một hình ảnh. Đó là một chuỗi các lệnh thực thi được nhân hệ điều hành coi là một thực thể duy nhất để sử dụng tài nguyên hệ

thống. Tài nguyên hệ thống bao gồm không gian bộ nhớ, thiết bị I/O và quan trọng nhất là thời gian CPU. Trong nhân hệ điều hành, mỗi tiến

trình được biểu diễn bằng một cấu trúc dữ liệu duy nhất, được gọi là Khối điều khiển tiến trình (PCB) hoặc Khối điều khiển tác vụ (TCB), v.v.

Trong cuốn sách này, chúng ta sẽ gọi nó đơn giản là cấu trúc PROC. Giống như hồ sơ cá nhân chứa tất cả thông tin của một người, cấu trúc

PROC chứa tất cả thông tin của một quy trình. Trong một hệ thống CPU, chỉ có một tiến trình có thể được thực thi tại một thời điểm. Nhân hệ

điều hành thường sử dụng con trỏ PROC toàn cục, đang chạy hoặc hiện tại, để trỏ tới PROC hiện đang thực thi. Trong một hệ điều hành thực,

cấu trúc PROC có thể chứa nhiều trường và khá lớn. Để bắt đầu, chúng ta sẽ định nghĩa một cấu trúc PROC rất đơn giản để biểu diễn các tiến

trình.

quy trình cấu trúc typedef{

struct proc *next;

int *ksp;

int kstack[1024];

}THỦ TỤC;

Trong cấu trúc PROC, trường tiếp theo là một con trỏ trỏ tới cấu trúc PROC tiếp theo. Nó được sử dụng để duy trì PROC trong cấu trúc dữ

liệu động, chẳng hạn như danh sách liên kết và hàng đợi. Trường ksp là con trỏ ngăn xếp đã lưu của một tiến trình khi nó không thực thi và

kstack là ngăn xếp thực thi của một tiến trình. Khi chúng tôi mở rộng nhân hệ điều hành, chúng tôi sẽ thêm nhiều trường hơn vào cấu trúc
PROC sau.

© Springer International Publishing AG 2017 113


KC Wang, Hệ điều hành nhúng và thời gian thực, DOI
10.1007/978-3-319-51517-5_5
Machine Translated by Google

114 5 Quản lý quy trình trong hệ thống nhúng

5.3 Đa nhiệm và chuyển đổi ngữ cảnh

5.3.1 Một chương trình đa nhiệm đơn giản

Chúng tôi bắt đầu chứng minh khả năng đa nhiệm bằng một chương trình đơn giản. Chương trình được ký hiệu là C5.1. Nó bao gồm một tệp ts.s trong ARM

mã hợp ngữ và tệp tc trong C.

(1). Tệp ts.s: tệp ts.s xác định điểm vào reset_handler của chương trình, trong đó nó
(1). đặt con trỏ ngăn xếp SVC ở mức cao nhất của proc0.kstack[ ].
(2). thêm hàm tswitch() trong mã hợp ngữ để chuyển đổi tác vụ.

// ———————————————————— tập tin ts.s của C5.1 —————————————————————— ————————

.global main, proc0, procsize // được nhập từ mã C

.global reset_handler, tswitch, bộ lập lịch, đang chạy

reset_handler:

LDR r0, =proc0 // r0->proc0

LDR r1, =procsize // r1 ->procsize

LDR r2, [r1, #0] // r2 = xử lý

CỘNG r0, r0, r2 // r0 -> cấp cao của Proc0

MOV sp, r0 // sp -> cấp cao của Proc0

BL chính // gọi hàm main() trong C

// THÊM hàm tswitch() để chuyển đổi tác vụ

chuyển đổi:

CỨU:

STMFD sp!, {r0-r12, lr}

LDR r0, =đang chạy // r0=&đang chạy

LDR r1, [r0, #0] // r1->runningPROC

STR sp, [r1, #4] // đang chạy->ksp = sp

TÌM THẤY:

Bộ lập lịch BL // gọi bộ lập lịch() trong C

BẢN TÓM TẮT:

LDR r0, = đang chạy

LDR r1, [r0, #0] // r1->chạy PROC

LDR sp, [r1, #4] // khôi phục đang chạy->ksp

LDMFD sp!, {r0-r12, lr} // khôi phục thanh ghi

Máy tính MOV, LR // trở lại

(2). Tệp tc: Tệp tc bao gồm trình điều khiển LCD và bàn phím cho I/O. Nó định nghĩa kiểu cấu trúc PROC, cấu trúc PROC
proc0 và một con trỏ PROC đang chạy trỏ tới PROC đang thực thi.

/*************** tập tin tc của C5.1 *******************/

#include "vid.c" // Trình điều khiển màn hình LCD

#include "kbd.c" // trình điều khiển KBD

#xác định SSIZE 1024 // kích thước ngăn xếp trên mỗi PROC

quy trình cấu trúc typedef{ //cấu trúc tiến trình

struct proc *next; // con trỏ PROC tiếp theo

int *ksp; // lưu sp khi PROC không chạy

int kstack[SSIZE]; // xử lý chế độ kernel ngăn xếp 4KB

}THỦ TỤC; // PROC là một kiểu

int procSize = sizeof(PROC);


Machine Translated by Google

5.3 Đa nhiệm và chuyển đổi ngữ cảnh 115

PROC proc0, *đang chạy; // cấu trúc proc0 và con trỏ đang chạy

bộ lập lịch int(){ đang chạy = &proc0; }

chủ yếu() // được gọi từ ts.s

đang chạy = &proc0; // thiết lập con trỏ PROC đang chạy printf("call tswitch()\n");

tswitch(); // gọi tswitch()

printf("trở lại main()\n");

Sử dụng chuỗi công cụ ARM (2016) để biên dịch liên kết ts.s và tc để tạo t.bin thực thi nhị phân như bình thường. Sau đó chạy t.bin

VM Versatilepb (Versatilepb 2016) trong QEMU, như trong

qemu-system-arm –M linh hoạtpb –m 128M –kernel t.bin

Trong quá trình khởi động, QEMU tải t.bin về 0x10000 và nhảy tới đó để thực thi hình ảnh đã tải. Khi quá trình thực thi bắt đầu

trong ts.s, nó sẽ đặt con trỏ ngăn xếp chế độ SVC ở mức cao nhất của Proc0. Điều này làm cho vùng kstack của Proc0 trở thành ngăn xếp

ban đầu. Cho đến thời điểm này, hệ thống không có khái niệm về bất kỳ quy trình nào vì không có. Mã hợp ngữ gọi main() trong C. Khi

điều khiển vào main(), chúng ta có một hình ảnh đang được thực thi. Theo định nghĩa của tiến trình, tức là việc thực thi một hình ảnh,

chúng ta có một tiến trình đang được thực thi, mặc dù hệ thống vẫn không biết tiến trình nào đang thực thi. Trong main(), sau khi thiết

lập Running = &proc0, hệ thống lúc này đang thực thi tiến trình Proc0. Đây là cách nhân hệ điều hành điển hình bắt đầu chạy quy trình

ban đầu khi nó bắt đầu. Quá trình ban đầu được thực hiện thủ công hoặc được tạo ra bằng vũ lực. Bắt đầu từ main(), hành vi trong thời

gian chạy của chương trình có thể được theo dõi và giải thích bằng sơ đồ thực hiện của Hình 5.1, trong đó các bước chính được gắn nhãn (1) đến (6).

Tại (1), nó cho phép chạy điểm tới Proc0, như thể hiện ở phía bên phải của Hình 5.1. Vì chúng ta giả định rằng việc chạy luôn trỏ đến

PROC của tiến trình thực thi hiện tại nên hệ thống hiện đang thực thi tiến trình proc0.
Tại (2), nó gọi tswitch(), tải LR(r14) với địa chỉ trả về và nhập tswitch.

Tại (3), nó thực thi phần SAVE của tswitch(), lưu các thanh ghi CPU vào ngăn xếp và lưu con trỏ ngăn xếp sp vào proc0. ksp.

Tại (4), nó gọi bộ lập lịch (), thiết lập việc chạy lại trỏ đến Proc0. Hiện tại, điều này là dư thừa vì việc chạy đã đạt điểm Proc0.

Sau đó, nó thực thi phần TIẾP TỤC của tswitch().

Tại (5), nó đặt sp thành proc0.ksp, điều này lại dư thừa vì chúng giống nhau. Sau đó, nó bật ngăn xếp để khôi phục các thanh ghi CPU đã

lưu.

Tại (6), nó thực thi MOV pc, lr ở cuối RESUME, nó quay trở lại nơi gọi của tswitch().

Hình 5.1 Sơ đồ thực thi của proc0


Machine Translated by Google

116 5 Quản lý quy trình trong hệ thống nhúng

5.3.2 Chuyển ngữ cảnh

Ngoài việc in một vài tin nhắn, chương trình này dường như vô dụng vì thực tế nó không làm được gì cả. Tuy nhiên, nó là nền tảng
của tất cả các chương trình đa nhiệm. Để thấy điều này, giả sử rằng chúng ta có một cấu trúc PROC khác, proc1, được gọi là tswitch()
và thực thi phần SAVE của tswitch() trước đó. Sau đó, ksp của Proc1 phải trỏ đến vùng ngăn xếp của nó, nơi chứa các thanh ghi CPU
đã lưu và địa chỉ trả về từ nơi nó gọi là tswitch(), như trong Hình 5.2.
Trong Scheduler(), nếu chúng ta cho phép chạy điểm tới Proc1, như minh họa ở phía bên phải của Hình 5.2, phần RESUME của
tswitch() sẽ thay đổi sp thành ksp của Proc1. Sau đó, mã TIẾP TỤC sẽ hoạt động trên ngăn xếp của Proc1. Điều này sẽ khôi phục các
thanh ghi đã lưu của proc1, khiến proc1 tiếp tục thực thi từ nơi nó gọi là tswitch() trước đó. Điều này thay đổi môi trường thực
thi từ proc0 thành proc1.
Chuyển ngữ cảnh :

Việc thay đổi môi trường thực thi của một tiến trình này sang một tiến trình khác được gọi là chuyển đổi ngữ cảnh, đó là
cơ chế đa nhiệm cơ bản.

Với chuyển đổi ngữ cảnh, chúng ta có thể tạo một môi trường đa nhiệm chứa nhiều tiến trình. Trong chương trình tiếp theo, ký
hiệu là C5.2, chúng tôi xác định cấu trúc NPROC = 5 PROC. Mỗi PROC có một số pid duy nhất để nhận dạng. PROC được khởi tạo như sau.

đang chạy -> P0 -> P1 -> P2 -> P3 -> P4 -> |


|
<----------------------------------------<-

P0 là quá trình chạy ban đầu. Tất cả các PROC tạo thành một danh sách liên kết vòng tròn để lập kế hoạch quy trình đơn giản.
Mỗi PROC, từ P1 đến P4, được khởi tạo theo cách sẵn sàng tiếp tục chạy từ hàm body(). Vì việc khởi tạo ngăn xếp PROC là rất quan
trọng nên chúng tôi sẽ giải thích các bước một cách chi tiết. Mặc dù các tiến trình này chưa từng tồn tại trước đây nhưng chúng ta
có thể giả vờ rằng chúng không chỉ tồn tại trước đó mà còn chạy trước đó. Lý do tại sao PROC hiện không chạy là vì nó đã gọi
tswitch() để loại bỏ CPU sớm hơn. Nếu vậy, ksp của PROC phải trỏ đến vùng ngăn xếp của nó chứa các thanh ghi CPU đã lưu và địa chỉ
trả về, như trong Hình 5.3, trong đó chỉ mục -i có nghĩa là SSIZE-i.
Vì PROC chưa bao giờ thực sự chạy trước đó nên chúng ta có thể giả sử rằng ngăn xếp của nó ban đầu trống, do đó địa chỉ trả về,
rPC=LR, nằm ở cuối ngăn xếp. rPC nên là gì? Nó có thể trỏ đến bất kỳ mã thực thi nào, ví dụ như địa chỉ đầu vào của hàm body(). Còn
các sổ đăng ký "đã lưu" thì sao? Vì PROC chưa từng chạy trước đó nên các giá trị thanh ghi không thành vấn đề nên tất cả chúng đều
có thể được đặt thành 0. Theo đó, chúng ta khởi tạo từng PROC, P1 đến P4, như trong Hình 5.4 .

Hình 5.2 Sơ đồ thực thi của proc1

Hình 5.3 Nội dung ngăn xếp quy trình


Machine Translated by Google

5.3 Đa nhiệm và chuyển đổi ngữ cảnh 117

Hình 5.4 Nội dung ngăn xếp ban đầu của quy trình

Với thiết lập này, khi PROC bắt đầu chạy, tức là khi chạy trỏ tới PROC, nó sẽ thực thi phần TIẾP TỤC của tswitch(),

LDMFD sp!, {r0-r12, lr}

Máy tính MOV, LR

khôi phục các thanh ghi CPU "đã lưu", theo sau là MOV pc, lr, khiến quá trình thực thi hàm body().

Sau khi khởi tạo, P0 gọi tswitch() để chuyển tiến trình. Trong tswitch(), P0 lưu các thanh ghi CPU vào ngăn xếp của chính nó, lưu con trỏ ngăn xếp trong

PROC.ksp của nó và gọi bộ lập lịch(). Chúng tôi sửa đổi chức năng lập lịch () bằng cách cho phép điểm chạy tới PROC tiếp theo, tức là

đang chạy = đang chạy->tiếp theo;

Vì vậy P0 chuyển sang P1. P1 bắt đầu bằng cách thực thi phần RESUME của tswitch(), khiến nó tiếp tục trở lại hàm body().

Khi ở trong body(), tiến trình đang chạy sẽ in pid của nó và nhắc nhập char đầu vào. Sau đó, nó gọi tswitch() để chuyển sang quy trình tiếp theo, v.v. Vì các

PROC nằm trong danh sách liên kết vòng tròn nên chúng sẽ lần lượt chạy. Sau đây liệt kê lắp ráp và mã C của C5.2.

(1). tập tin ts.s của C5.2

.global vectơ_start, vectơ_end .global main, proc,

procsize .global tswitch, bộ lập lịch,

đang chạy .global lock, unlock

reset_handler:

// đặt SVC sp thành proc[0] cao cấp

LDR r0, =proc

LDR r1, = kích thước

LDR r2, [r1, #0]

CỘNG r0, r0, r2

MOV sp, r0

// sao chép bảng vectơ vào địa chỉ 0

Bản sao BL_vector

// chuyển sang chế độ IRQ để đặt ngăn xếp IRQ

MSR cpsr, #0x92

LDR sp, =irq_stack

// quay lại chế độ SVC khi bật IRQ

MSR cpsr, #0x13

// gọi main() ở chế độ SVC

BL chính

B .
Machine Translated by Google

118 5 Quản lý quy trình trong hệ thống nhúng

chuyển đổi:

bà r0, cpsr // Chế độ SVC, IRQ ngắt

hoặc r0, r0, #0x80

msr cpsr, r0

stmfd sp!, {r0-r12, lr}

LDR r0, = đang chạy // r0=&đang chạy

LDR r1, [r0, #0] // r1->runningPROC

str sp, [r1, #4] // đang chạy->ksp = sp

lịch trình bl

LDR r0, = đang chạy

LDR r1, [r0, #0] // r1->runningPROC

lDR sp, [r1, #4]

bà r0, cpsr // Chế độ SVC, ngắt IRQ

bi r0, r0, #0x80

msr cpsr, r0

ldmfd sp!, {r0-r12, pc}

irq_handler:

phụ lr, lr, #4

stmfd sp!, {r0-r12, lr}

bl irq_chandler

ldmfd sp!, {r0-r12, pc}^

kho a: // vô hiệu hóa các ngắt IRQ

MRS r0, cpsr

ORR r0, r0, #0x80 // đặt bit mặt nạ IRQ

MSR cpsr, r0

di chuyển máy tính, lr

mở khóa: // kích hoạt ngắt IRQ

MRS r0, cpsr

BIC r0, r0, #0x80 // xóa bit mặt nạ IRQ

MSR cpsr, r0

di chuyển máy tính, lr

vectơ_start: // bảng vectơ

PC LDR, reset_handler_addr

Máy tính LDR, undef_handler_addr

Máy tính LDR, swi_handler_addr

PC LDR, tìm nạp trước_abort_handler_addr

PC LDR, dữ liệu_abort_handler_addr

B .

Máy tính LDR, irq_handler_addr

PC LDR, fiq_handler_addr

đặt lại_handler_addr: .word reset_handler

undef_handler_addr: .word undef_handler

swi_handler_addr: .word swi_handler

prefetch_abort_handler_addr: .word prefetch_abort_handler

dữ liệu_abort_handler_addr: .word data_abort_handler

irq_handler_addr: .word irq_handler

fiq_handler_addr: .word fiq_handler

vectơ_end:
Machine Translated by Google

5.3 Đa nhiệm và chuyển đổi ngữ cảnh 119

Mã lắp ráp giống như trong C5.1, ngoại trừ những sửa đổi sau:

. con trỏ ngăn xếp chế độ SVC ban đầu được đặt thành kstack của proc[0]

. sao chép bảng vectơ vào địa chỉ 0


. đặt ngăn xếp chế độ IRQ và cài đặt trình xử lý IRQ (đối với trình điều khiển bàn phím)

. gọi main() ở chế độ SVC có bật ngắt IRQ

(2). Trình điều khiển thiết bị của C5.2: Trình điều khiển LCD giống như trong C5.1. Trình điều khiển bàn phím giống như trong Sect. 3.5.4 của

Chap. 3.
(3). tập tin tc của C5.2:

/************ tập tin tc của C5.2 **********/

#include "vid.c" // Trình điều khiển LCD

#include "kbd.c" // trình điều khiển KBD

#xác định NPROC 5

#xác định SSIZE 1024

quy trình cấu trúc typedef{

struct proc *next;

int *ksp;

int pid;

int kstack[SSIZE];

}THỦ TỤC;

PROC proc[NPROC], *đang chạy;

int procsize = sizeof(PROC);

int cơ thể()

ký tự c;

printf("Proc %d tiếp tục vào body()\n", Running->pid);

trong khi(1){

printf("Proc %d in body() nhập char [s] : ", Running->pid);

c = kgetc(); printf("%c\n", c);

tswitch();

int kernel_init()

int tôi, j;

PROC *p;

printf("kernel_init()\n");

cho (i=0; i<NPROC; i++){

p = &proc[i];

p->pid = tôi;

p->trạng thái = SẴN SÀNG;

for (j=1; j<15; j++) // khởi tạo proc.kstack và lưu ksp

p->kstack[SSIZE-j] = 0; // tất cả các reg đã lưu = 0

p->kstack[SSIZE-1] = (int)nội dung; // điểm tiếp tục = nội dung

p->ksp = &(p->kstack[SSIZE-14]); // đã lưu ksp

p->tiếp theo = p + 1; // trỏ tới PROC tiếp theo

proc[NPROC-1].next = &proc[0]; // danh sách PROC vòng

đang chạy = &proc[0];

}
Machine Translated by Google

120 5 Quản lý quy trình trong hệ thống nhúng

bộ lập lịch int()

printf("Proc %d trong bộ lập lịch\n", đang chạy->pid); đang chạy = đang

chạy->tiếp theo; printf("lần chạy

tiếp theo = %d\n", đang chạy->pid);

int chính()

ký tự c;

fbuf_init(); // khởi tạo trình điều khiển LCD

kbd_init(); // khởi tạo trình điều khiển KBD

printf("Chào mừng đến với WANIX in Arm\n");

kernel_init();

trong khi(1){

printf("P0 đang chạy nhập một khóa: ");

c = kgetc(); printf("%c\n", c);

tswitch();

5.3.3 Trình diễn đa nhiệm

Hình 5.5 cho thấy kết quả đầu ra khi chạy chương trình đa nhiệm C5.2. Nó sử dụng pid quy trình để hiển thị với nhiều màu sắc khác nhau,

chỉ để giải trí.

Trước khi tiếp tục, cần lưu ý những điều sau.

(1). Trong chương trình đa nhiệm C5.2, không có tiến trình nào, từ P1 đến P4, thực sự gọi hàm body(). Những gì chúng tôi đã làm là thuyết

phục mỗi tiến trình mà nó gọi là tswitch() từ địa chỉ mục nhập của body() từ bỏ CPU sớm hơn và đó là nơi nó sẽ tiếp tục khi bắt đầu chạy.

Vì vậy, chúng ta có thể tạo môi trường ban đầu để mỗi quá trình bắt đầu. Quá trình không có lựa chọn nào khác ngoài việc tuân theo. Đây

là sức mạnh (và niềm vui) của việc lập trình hệ thống.

Hình 5.5 Trình diễn đa nhiệm


Machine Translated by Google

5.3 Đa nhiệm và chuyển đổi ngữ cảnh 121

(2). Tất cả các tiến trình, từ P1 đến P4, thực thi cùng một hàm body() nhưng mỗi tiến trình lại thực thi trong môi trường riêng của nó. Ví dụ,

trong khi thực thi hàm body(), mỗi tiến trình có biến cục bộ c riêng trong ngăn xếp tiến trình. Điều này cho thấy sự khác biệt

giữa các tiến trình và chức năng. Hàm chỉ là một đoạn mã thụ động, không có sự sống. Các tiến trình là sự thực thi của

chức năng, làm cho mã chức năng trở nên sống động.

(3). Khi một tiến trình lần đầu tiên đi vào hàm body(), ngăn xếp tiến trình sẽ trống về mặt logic. Ngay khi bắt đầu thực thi,

ngăn xếp quy trình sẽ phát triển (và thu hẹp) theo trình tự gọi hàm như được mô tả trong Phần. 2.7.3.2 của Chap. 2.

(4). Kích thước kstack cho mỗi quá trình được xác định là 4KB. Điều này ngụ ý rằng độ dài tối đa của chuỗi lệnh gọi hàm (và

không gian biến cục bộ liên quan) của mọi quy trình không bao giờ được vượt quá kích thước kstack. Nhận xét tương tự cũng áp dụng cho các

ngăn xếp chế độ đặc quyền, ví dụ ngăn xếp chế độ IRQ để xử lý ngắt. Tất cả đều nằm dưới sự hoạch định và kiểm soát của

người thiết kế hạt nhân. Vì vậy, lỗi tràn ngăn xếp sẽ không bao giờ xảy ra ở chế độ kernel.

5.4 Quy trình động

Trong chương trình C5.2, P0 là quy trình ban đầu. Tất cả các tiến trình khác được tạo tĩnh bởi P0 trong kernel_init(). Ở phần tiếp theo

chương trình, ký hiệu là C5.3, chúng tôi sẽ chỉ ra cách tạo các quy trình một cách linh hoạt.

5.4.1 Tạo quy trình động

(1). Đầu tiên, chúng tôi thêm trạng thái và trường ưu tiên vào cấu trúc PROC và xác định danh sách liên kết PROC: freeList và

ReadyQueuee, được giải thích bên dưới.

#define NPROC 9

#định nghĩa MIỄN PHÍ 0

#xác định SẴN SÀNG 1

#xác định SSIZE 1024

quy trình cấu trúc typedef{

struct proc *next; // con trỏ PROC tiếp theo

int *ksp; // đã lưu sp khi KHÔNG chạy

int pid; // xử lý ID

int trạng thái; // MIỄN PHÍ|SẴN SÀNG, v.v.


int sự ưu tiên; // giá trị ưu tiên

int kstack[SSIZE]; // xử lý ngăn xếp chế độ kernel

}THỦ TỤC;

PROC proc[NPROC], *đang chạy, *freeList, *readyQueue;

. freeList = danh sách liên kết (đơn lẻ) chứa tất cả PROC MIỄN PHÍ. Khi hệ thống khởi động, ban đầu tất cả các PROC đều nằm trong freeList.

Khi tạo một quy trình mới, chúng tôi phân bổ PROC miễn phí từ freeList. Khi một quá trình kết thúc, chúng tôi giải phóng PROC của nó và
phát hành nó trở lại freeList để tái sử dụng.

. ReadyQueue = hàng đợi ưu tiên của PROC đã sẵn sàng để chạy. Các PROC có cùng mức độ ưu tiên được sắp xếp theo thứ tự Nhập trước xuất trước

(FIFO) trong hàng đợi sẵn sàng.

(2). Trong tệp queue.c, chúng tôi triển khai các hàm sau cho các hoạt động danh sách và hàng đợi.

/*************** tập tin queue.c ****************/

PROC *get_proc(PROC **list){ trả về một con trỏ PROC từ danh sách }

int put_proc(PROC **list, PROC *p){ // nhập p vào danh sách }

int enqueue(PROC **queue, PROC *p){ // nhập p vào hàng đợi theo mức độ ưu tiên }

PROC *dequeue(PROC **queue){ // xóa và trả lại PROC đầu tiên khỏi hàng đợi }

int printList(PROC *p){ // in các phần tử danh sách }


Machine Translated by Google

122 5 Quản lý quy trình trong hệ thống nhúng

(3). Trong tệp kernel.c, kernel_init() khởi tạo cấu trúc dữ liệu kernel, chẳng hạn như freeList và ReadyQueue. Nó cũng tạo ra
P0 làm tiến trình chạy ban đầu. Chức năng

int pid = kfork(int func, int ưu tiên)

tạo một quy trình mới để thực thi hàm func() với mức độ ưu tiên được chỉ định. Trong chương trình ví dụ, mọi tiến trình mới
bắt đầu thực thi từ cùng một hàm body(). Khi một tác vụ đã hoàn thành công việc của nó, nó có thể kết thúc bằng hàm

khoảng trống kexit()

sẽ giải phóng cấu trúc PROC của nó trở lại freeList để tái sử dụng. Chức năng lập lịch () dùng để lập lịch trình. Các
sau đây liệt kê mã C của tệp kernel.c và tc.

/************** file kernel.c của Chương trình C5.3 **********/

int kernel_init()

int tôi, j;

PROC *p;

printf("kernel_init()\n");

cho (i=0; i<NPROC; i++){

p = &proc[i];

p->pid = tôi;

p->trạng thái = MIỄN PHÍ;

p->tiếp theo = p + 1;

proc[NPROC-1].next = 0;

freeList = &proc[0]; // tất cả PROC trong freeList

hàng đợi sẵn sàng = 0; // hàng đợi sẵn sàng trống

// tạo P0 làm tiến trình chạy ban đầu p = get_proc(&freeList);

p->ưu tiên = 0; đang chạy = p;

printf("đang chạy = // P0 có mức ưu tiên thấp nhất 0

%d\n", đang chạy-

>pid); printList(freeList);

int body() // mã xử lý

ký tự c;

màu = đang chạy->pid;

printf("Proc %d tiếp tục vào body()\n", Running->pid);

trong khi(1){

printf("Proc %d in body() nhập char [s|f|x] : ", Running->pid);

c = kgetc(); printf("%c\n", c);

chuyển đổi (c) {

trường hợp 's': tswitch(); phá vỡ;

trường hợp 'f': kfork((int)body, 1); phá vỡ;

trường hợp 'x': kexit(); phá vỡ;

// kfork() tạo một tiến trình mới để thực thi func với mức độ ưu tiên int kfork(int func, int

Priority)

int tôi;

PROC *p = get_proc(&freeList);
Machine Translated by Google

5.4 Quy trình động 123

nếu (p==0){

printf("không còn PROC, kfork thất bại\n");

trả về -1; // trả về -1 nếu THẤT BẠI

p->trạng thái = SẴN SÀNG;

p->ưu tiên = ưu tiên;

// đặt kstack cho sơ yếu lý lịch để thực thi func()

cho (i=1; i<15; i++)

p->kstack[SSIZE-i] = 0; // tất cả các reg "đã lưu" = 0

p->kstack[SSIZE-1] = func; p->ksp = &(p- // tiếp tục địa chỉ thực thi

>kstack[SSIZE-14]); // đã lưu ksp

enqueue(&readyQueue, p); // nhập p vào hàng đợi sẵn sàng

printf("%d đã tạo một tiến trình mới %d\n", Running->pid, p->pid);

printf("freeList = "); printList(readyQueue);

trả về p->pid;

void kexit() // được gọi bởi tiến trình để kết thúc

printf("Proc %d kexit\n", Running->pid);

đang chạy->trạng thái = MIỄN PHÍ;

put_proc(đang chạy);

tswitch(); // từ bỏ CPU

bộ lập lịch int()

if (đang chạy->trạng thái == SẴN SÀNG)

enqueue(&readyQueue, đang chạy);

đang chạy = dequeue(&readyQueue);

/************ tệp tc của Chương trình C5.3 *************/

#include "type.h" // Kiểu PROC và các hằng số

#include "string.c" // các hàm thao tác chuỗi

#include "queue.c" // các hàm thao tác danh sách và hàng đợi

#include "vid.c" // Trình điều khiển LCD

#include "kbd.c" // trình điều khiển KBD

#include "ngoại lệ.c"

#include "kernel.c"

void copy_vectors(void){// giống như trước }

void IRQ_handler(){ // chỉ xử lý các ngắt KBD }

int chính()

fbuf_init(); // khởi tạo màn hình LCD

kbd_init(); // khởi tạo trình điều khiển KBD

printf("Chào mừng đến với Wanix trong ARM\n");

kernel_init(); // khởi tạo kernel, tạo và chạy P0

kfork((int)body, 1); // P0 tạo P1 trong hàng đợi sẵn sàng

trong khi(1){

while(readyQueue==0); // P0 lặp nếu hàng đợi trống

tswitch();

}
Machine Translated by Google

124 5 Quản lý quy trình trong hệ thống nhúng

Hình 5.6 Trình diễn quá trình dyamic

Trong tệp tc, đầu tiên nó khởi chạy màn hình LCD và trình điều khiển KBD. Sau đó, nó khởi tạo kernel để chạy tiến trình ban đầu P0, có mức ưu

tiên thấp nhất là 0. P0 tạo một tiến trình P1 mới và nhập nó vào hàng đợi sẵn sàng. Sau đó P0 gọi tswitch() để chuyển tiến trình sang chạy P1. Mọi

tiến trình mới sẽ tiếp tục thực thi hàm body(). Trong khi một tiến trình đang chạy, người dùng có thể nhập 's' để chuyển đổi tiến trình, 'f' để

tạo một tiến trình mới và 'x' để kết thúc, v.v.

5.4.2 Trình diễn các quy trình động

Hình 5.6 hiển thị màn hình chạy chương trình C5.3. Như hình minh họa, đầu vào 'f' khiến P1 phân nhánh một quy trình P2 mới trong hàng đợi sẵn sàng.

Đầu vào của 's' làm cho P1 chuyển tiến trình sang chạy P2, quá trình này sẽ tiếp tục thực thi cùng một hàm body().

Trong khi P2 chạy, người đọc có thể nhập các lệnh để cho phép P2 chuyển đổi quy trình hoặc phân nhánh một quy trình mới, v.v. Trong khi một quy

trình chạy, đầu vào 'x' sẽ khiến quy trình kết thúc.

5.5 Lập kế hoạch quy trình

5.5.1 Thuật ngữ lập lịch trình quy trình

Trong hệ điều hành đa nhiệm, thường có nhiều tiến trình sẵn sàng chạy. Số lượng tiến trình có thể chạy được nói chung lớn hơn số lượng CPU có sẵn.

Lập kế hoạch quy trình là quyết định thời điểm và trên CPU nào sẽ chạy các quy trình để đạt được hiệu suất hệ thống tổng thể tốt. Trước khi thảo

luận về việc lập kế hoạch quy trình, trước tiên chúng ta phải làm rõ các thuật ngữ sau, thường liên quan đến việc lập kế hoạch quy trình.

(1). Các quy trình giới hạn I/O so với các quy trình liên kết tính toán:

Một tiến trình được coi là bị giới hạn I/O nếu nó thường xuyên tự tạm dừng để chờ các thao tác I/O. Các quy trình liên quan đến I/O thường đến

từ những người dùng tương tác mong đợi thời gian phản hồi nhanh. Một quy trình được coi là bị giới hạn tính toán nếu nó sử dụng nhiều thời gian

của CPU. Các quy trình liên quan đến tính toán thường liên quan đến các tính toán dài, chẳng hạn như biên dịch chương trình và tính toán số, v.v.
Machine Translated by Google

5.5 Lập kế hoạch quy trình 125

(2). Thời gian phản hồi so với thông lượng:

Thời gian phản hồi đề cập đến tốc độ hệ thống có thể phản hồi với một sự kiện, chẳng hạn như nhập phím từ bàn phím. Thông lượng

là số tiến trình được hoàn thành trong một đơn vị thời gian.

(3). Lập kế hoạch ưu tiên theo vòng tròn và động:

Trong lập lịch vòng tròn, các tiến trình lần lượt chạy. Trong lập lịch ưu tiên động, mỗi tiến trình có một mức độ ưu tiên,

thay đổi linh hoạt (theo thời gian) và hệ thống cố gắng chạy quy trình với mức độ ưu tiên cao nhất.

(4). Ưu tiên so với không ưu tiên:

Quyền ưu tiên có nghĩa là CPU có thể bị lấy đi khỏi một tiến trình đang chạy bất cứ lúc nào. Không được ưu tiên có nghĩa là một tiến trình đang chạy

cho đến khi nó tự bỏ CPU, ví dụ như khi quá trình kết thúc, chuyển sang chế độ ngủ hoặc bị chặn.

(5). Thời gian thực và chia sẻ thời gian:

Hệ thống thời gian thực phải phản hồi với các sự kiện bên ngoài, chẳng hạn như ngắt, trong thời gian phản hồi tối thiểu, thường là vài mili giây.

Ngoài ra, hệ thống cũng có thể cần hoàn tất quá trình xử lý các sự kiện đó trong một khoảng thời gian nhất định. Trong hệ thống chia sẻ thời gian, mỗi

quy trình chạy với một khoảng thời gian được đảm bảo để tất cả các quy trình nhận được phần thời gian CPU hợp lý của chúng.

5.5.2 Mục tiêu, chính sách và thuật toán lập kế hoạch quy trình

Lập kế hoạch quy trình nhằm đạt được các mục tiêu sau.

.sử dụng cao tài nguyên hệ thống, đặc biệt là thời gian CPU, .phản hồi

nhanh với các quy trình tương tác hoặc thời gian thực, .thời

gian hoàn thành được đảm bảo của các quy trình thời gian

thực, .sự công bằng đối với tất cả các quy trình để có thông lượng tốt, v.v.

Dễ dàng nhận thấy rằng một số mục tiêu đang xung đột với nhau. Ví dụ: thời gian phản hồi nhanh và thông lượng cao thường không thể đạt được cùng một

lúc. Chính sách lập kế hoạch là một tập hợp các quy tắc, theo đó hệ thống cố gắng đạt được tất cả hoặc một số mục tiêu. Đối với hệ điều hành có mục đích

chung, chính sách lập lịch thường cố gắng đạt được hiệu suất tổng thể tốt của hệ thống bằng cách cố gắng cân bằng giữa các mục tiêu xung đột. Đối với các

hệ thống nhúng và thời gian thực, trọng tâm thường là phản ứng nhanh với các sự kiện bên ngoài và đảm bảo thời gian thực hiện quy trình. Thuật toán lập

lịch là một tập hợp các phương thức thực hiện chính sách lập lịch. Trong nhân hệ điều hành, các thành phần khác nhau, tức là cấu trúc dữ liệu và mã được

sử dụng để triển khai thuật toán lập lịch, được gọi chung là bộ lập lịch quy trình. Điều đáng chú ý là trong hầu hết nhân hệ điều hành không có một đoạn

mã hoặc mô-đun nào có thể được xác định là bộ lập lịch. Các chức năng của bộ lập lịch được triển khai ở nhiều nơi bên trong nhân hệ điều hành, ví dụ: khi

một quá trình đang chạy tự tạm dừng hoặc chấm dứt, khi một quá trình bị tạm dừng có thể chạy lại được và đáng chú ý nhất là trong bộ xử lý ngắt hẹn giờ.

5.5.3 Lập kế hoạch quy trình trong hệ thống nhúng

Trong hệ thống nhúng, các quy trình được tạo ra để thực hiện các tác vụ cụ thể. Tùy thuộc vào tầm quan trọng của nhiệm vụ, mỗi quy trình được chỉ định

mức độ ưu tiên, thường là tĩnh. Các tiến trình chạy theo định kỳ hoặc để đáp ứng với các sự kiện bên ngoài. Mục tiêu chính của việc lập kế hoạch quy

trình là đảm bảo phản hồi nhanh chóng với các sự kiện bên ngoài và đảm bảo thời gian thực hiện quy trình.

Việc sử dụng tài nguyên và thông lượng tương đối không quan trọng. Vì những lý do này, chính sách lập lịch quy trình thường dựa trên mức độ ưu tiên của

quy trình hoặc theo vòng tròn đối với các quy trình có cùng mức độ ưu tiên. Trong hầu hết các hệ thống nhúng đơn giản, các tiến trình thường thực thi

trong cùng một không gian địa chỉ. Trong trường hợp này, chính sách lập lịch thường không có tính ưu tiên. Mỗi tiến trình chạy cho đến khi nó tự nguyện

từ bỏ CPU, ví dụ như khi tiến trình chuyển sang chế độ ngủ, bị treo hoặc rõ ràng
Machine Translated by Google

126 5 Quản lý quy trình trong hệ thống nhúng

nhường quyền kiểm soát cho một tiến trình khác. Lập kế hoạch ưu tiên phức tạp hơn vì những lý do sau. Với quyền ưu tiên, nhiều tiến trình có

thể chạy đồng thời trong cùng một không gian địa chỉ. Nếu một quá trình đang trong quá trình sửa đổi một đối tượng dữ liệu được chia sẻ thì nó

không được quyền ưu tiên trừ khi đối tượng dữ liệu được chia sẻ được bảo vệ trong một khu vực quan trọng. Nếu không, đối tượng dữ liệu được

chia sẻ có thể bị hỏng bởi các tiến trình khác. Việc bảo vệ các vùng quan trọng sẽ được thảo luận trong phần tiếp theo về đồng bộ hóa quy trình.

5.6 Đồng bộ hóa quy trình

Khi nhiều tiến trình thực thi trong cùng một không gian địa chỉ, chúng có thể truy cập và sửa đổi các đối tượng dữ liệu được chia sẻ (toàn cầu).

Đồng bộ hóa quy trình đề cập đến các quy tắc và cơ chế được sử dụng để đảm bảo tính toàn vẹn của các đối tượng dữ liệu được chia sẻ trong môi

trường quy trình đồng thời. Có nhiều loại công cụ đồng bộ hóa quy trình. Để có danh sách chi tiết về các công cụ đó, cách triển khai và sử dụng

chúng, người đọc có thể tham khảo (Wang 2015). Sau đây, chúng ta sẽ thảo luận về một số công cụ đồng bộ hóa đơn giản phù hợp với các hệ thống

nhúng. Ngoài việc thảo luận về các nguyên tắc đồng bộ hóa quy trình, chúng tôi cũng sẽ chỉ ra cách áp dụng chúng vào việc thiết kế và triển

khai các hệ thống nhúng bằng các chương trình mẫu.

5.6.1 Ngủ và thức

Cơ chế đơn giản nhất để đồng bộ hóa tiến trình là các hoạt động ngủ/thức, được sử dụng trong nhân Unix gốc. Khi một tiến trình phải chờ một thứ

gì đó, ví dụ như một tài nguyên, hiện không có sẵn, nó sẽ chuyển sang chế độ ngủ để tự tạm dừng và giải phóng CPU, cho phép hệ thống chạy các

tiến trình khác. Khi tài nguyên cần thiết có sẵn, một quy trình khác hoặc trình xử lý ngắt sẽ đánh thức các quy trình đang ngủ, cho phép chúng

tiếp tục. Giả sử rằng mỗi cấu trúc PROC có một trường sự kiện được thêm vào. Các thuật toán ngủ/thức như sau.

ngủ (sự kiện int)

ghi lại giá trị sự kiện khi chạy PROC.event; thay đổi trạng thái

PROC đang chạy thành SLEEP; quá trình chuyển đổi;

thức dậy (sự kiện int)

cho mỗi PROC *p do{

if (p->status==SLEEP && p->event==event){

thay đổi trạng thái p-> thành SẴN SÀNG;

nhập p vào hàng đợi sẵn sàng;

Để cơ chế hoạt động, sleep() và Wakeup() phải được triển khai đúng cách. Đầu tiên, mỗi thao tác phải mang tính nguyên tử (không thể phân

chia) theo quan điểm quy trình. Ví dụ: khi một tiến trình thực thi chế độ ngủ (), nó phải hoàn thành thao tác ngủ trước khi người khác cố gắng

đánh thức nó. Trong hạt nhân UP không được ưu tiên, mỗi lần chỉ có một tiến trình chạy, do đó các tiến trình không thể can thiệp lẫn nhau. Tuy

nhiên, trong khi một tiến trình đang chạy, nó có thể bị chuyển hướng sang xử lý các ngắt, điều này có thể cản trở tiến trình. Để đảm bảo tính

nguyên tử của chế độ ngủ và thức, việc vô hiệu hóa các ngắt là đủ. Vì vậy, chúng ta có thể triển khai sleep() và Wakeup() như sau.

int ngủ (sự kiện int)

int SR = int_off(); // vô hiệu hóa IRQ và trả về CPSR

đang chạy->sự kiện = sự kiện;

đang chạy->trạng thái = SLEEP;


Machine Translated by Google

5.6 Đồng bộ hóa quy trình 127

tswitch(); // chuyển đổi quá trình

int_on(SR); // khôi phục CPSR gốc

int thức dậy (sự kiện int)

int SR = int_off(); // vô hiệu hóa IRQ và trả về CPSR

cho mỗi PROC *p do{

if (p->status==SLEEP && p->event==event){

p->trạng thái = SẴN SÀNG;

enqueue(&readyQueue, p);

int_on(SR); // khôi phục CPSR gốc

Lưu ý rằng Wakeup() đánh thức TẤT CẢ các tiến trình, nếu có, đang ngủ trong một sự kiện. Nếu không có tiến trình nào đang ngủ trong sự kiện

này thì việc đánh thức sẽ không có tác dụng, tức là nó tương đương với một NOP và không làm gì cả. Cũng cần lưu ý rằng trình xử lý ngắt không bao

giờ có thể ngủ hoặc chờ (Wang 2015). Họ chỉ có thể đưa ra các cuộc gọi đánh thức để đánh thức quá trình ngủ.

5.6.2 Trình điều khiển thiết bị sử dụng chế độ Ngủ/Thức

Trong chương. 3, chúng tôi đã phát triển một số trình điều khiển thiết bị sử dụng các ngắt. Việc tổ chức các trình điều khiển thiết bị này thể

hiện một mô hình chung. Mỗi trình điều khiển thiết bị được điều khiển bằng ngắt bao gồm ba phần; phần nửa dưới, là bộ xử lý ngắt, phần nửa trên,

được gọi bởi chương trình chính và vùng dữ liệu chứa bộ đệm I/O và các biến điều khiển, được chia sẻ bởi phần dưới và phần trên. Ngay cả khi có

các ngắt, chương trình chính vẫn phải sử dụng các vòng lặp chờ bận để chờ dữ liệu hoặc chỗ trống trong bộ đệm I/O, về cơ bản cũng giống như kiểm

tra vòng. Trong hệ thống đa nhiệm, I/O bằng cách thăm dò không sử dụng CPU một cách hiệu quả. Trong phần này, chúng tôi sẽ trình bày cách sử dụng

các tiến trình và trạng thái ngủ/thức để triển khai các trình điều khiển thiết bị được điều khiển bằng ngắt mà không có vòng lặp chờ bận.

5.6.2.1 Trình điều khiển thiết bị đầu

vào ở chương. 3, trình điều khiển KBD sử dụng các ngắt nhưng nửa trên sử dụng phép thăm dò. Khi một tiến trình cần một khóa đầu vào, nó sẽ thực

thi một vòng lặp chờ bận cho đến khi bộ xử lý ngắt đặt một khóa vào bộ đệm đầu vào. Mục tiêu của chúng tôi ở đây là thay thế vòng lặp chờ bận bằng

chế độ ngủ/thức. Đầu tiên, chúng tôi hiển thị mã trình điều khiển gốc bằng cách bỏ phiếu. Sau đó, chúng tôi sửa đổi nó để sử dụng chế độ ngủ/thức

để đồng bộ hóa.

(1). Cấu trúc KBD: Cấu trúc KBD là phần giữa của driver. Nó chứa bộ đệm đầu vào và các biến điều khiển, ví dụ dữ liệu = số khóa trong bộ đệm đầu

vào.

typedef cấu trúc kbd{ // cơ sở = 0x10006000

char *cơ sở; // địa chỉ cơ sở của KBD, dưới dạng char *

char buf[BUFSIZE]; // kích thước bộ đệm đầu vào=128 byte

int đầu, đuôi, dữ liệu; // biến điều khiển; dữ liệu = 0 ban đầu

}KBD; KBD kbd;

(2). kgetc(): Đây là (chức năng cơ bản) của nửa trên của trình điều khiển KBD.

int kgetc() // chương trình chính gọi kgetc() để trả về một char

ký tự c;

KBD *kp = &kbd;

mở khóa(); // kích hoạt ngắt IRQ // bận-chờ dữ

while(kp->data == 0); liệu;


Machine Translated by Google

128 5 Quản lý quy trình trong hệ thống nhúng

kho a(); // vô hiệu hóa các ngắt IRQ

c = kp->buf[kp->tail++];// lấy char và cập nhật chỉ mục tail

kp->tail %= BUFSIZE;

kp->dữ liệu--; // cập nhật dữ liệu với các ngắt TẮT

mở khóa(); // kích hoạt ngắt IRQ

trả lại c;

Chúng ta giả định rằng chương trình chính hiện đang chạy một tiến trình. Khi một tiến trình cần một khóa đầu vào, nó sẽ gọi kgetc(), cố gắng

lấy một khóa từ bộ đệm đầu vào. Không có bất kỳ phương tiện đồng bộ hóa nào, quy trình phải dựa vào vòng lặp chờ bận

trong khi (kp->dữ liệu == 0); // bận-chờ dữ liệu;

liên tục kiểm tra biến dữ liệu để tìm bất kỳ khóa nào trong bộ đệm đầu vào.

(3). kbd_handler(): Trình xử lý ngắt là nửa dưới của trình điều khiển KBD.

kbd_handler()

cấu trúc KBD *kp = &kbd;

char scode = *(kp->base+KDATA); // đọc mã quét trong thanh ghi dữ liệu

nếu (mã & 0x80) // bỏ qua các bản phát hành chính

trở lại;

nếu (dữ liệu == BUFSIZE) // nếu bộ đệm đầu vào ĐẦY

trở lại; // bỏ qua khóa hiện tại

c = unsh[scode]; // ánh xạ mã quét tới ASCII

kp->buf[kp->head++] = c; // nhập key vào CIRCULAR buf[ ]

kp->head %= BUFSIZE;

kp->dữ liệu++; // tăng bộ đếm dữ liệu lên 1

Đối với mỗi lần nhấn phím, trình xử lý ngắt sẽ ánh xạ mã quét thành ký tự ASCII (chữ thường), lưu ký tự đó vào đầu vào

đệm và cập nhật dữ liệu các biến đếm. Một lần nữa, không có bất kỳ phương tiện đồng bộ hóa nào, đó là tất cả trình xử lý ngắt

có thể làm được. Chẳng hạn, nó không thể thông báo trực tiếp quá trình xử lý các khóa có sẵn. Do đó, quá trình phải kiểm tra đầu vào

các phím bằng cách liên tục thăm dò biến dữ liệu của trình điều khiển. Trong một hệ thống đa nhiệm, vòng lặp bận-chờ là điều không mong muốn. Chúng ta có thể sử dụng

ngủ/thức dậy để loại bỏ vòng lặp chờ bận trong trình điều khiển KBD như sau.

(1). Cấu trúc KBD: không cần thay đổi.

(2). kgetc(): viết lại kgetc() để cho quá trình ngủ đối với dữ liệu nếu không có khóa nào trong bộ đệm đầu vào. Để ngăn chặn chủng tộc

điều kiện giữa tiến trình và bộ xử lý ngắt, trước tiên tiến trình sẽ vô hiệu hóa các ngắt. Sau đó nó kiểm tra biến dữ liệu

và sửa đổi bộ đệm đầu vào với các ngắt bị vô hiệu hóa, nhưng nó phải kích hoạt các ngắt trước khi chuyển sang chế độ ngủ. Ketc đã sửa đổi

() hàm là

int kgetc() // chương trình chính gọi kgetc() để trả về một char

ký tự c;

KBD *kp = &kbd;

trong khi(1){

kho a(); // vô hiệu hóa các ngắt IRQ

nếu (kp->dữ liệu==0){ // kiểm tra dữ liệu khi IRQ bị tắt

mở khóa(); // kích hoạt ngắt IRQ

ngủ(&kp->dữ liệu); // ngủ để lấy dữ liệu


Machine Translated by Google

5.6 Đồng bộ hóa quy trình 129

c = kp->buf[kp->tail++]; // lấy ac và cập nhật chỉ số đuôi kp->tail %= BUFSIZE;

kp->dữ liệu--; // cập nhật với các ngắt TẮT // cho phép ngắt IRQ

mở khóa();

trả lại c;

(3). kbd_handler(): viết lại trình xử lý ngắt KBD để đánh thức các tiến trình đang ngủ, nếu có, đang chờ dữ liệu. Vì tiến trình không thể can

thiệp vào trình xử lý ngắt nên không cần phải bảo vệ các biến dữ liệu bên trong trình xử lý ngắt.

kbd_handler()

cấu trúc KBD *kp = &kbd;

scode = *(kp->base+KDATA); // đọc mã quét trong thanh ghi dữ liệu

nếu (mã & 0x80) // bỏ qua các bản phát hành chính

trở lại;

if (kp->data == BUFSIZE) // bỏ qua khóa nếu bộ đệm đầu vào ĐẦY ĐỦ // ánh xạ mã quét sang ASCII // nhập

c = unsh[scode]; khóa vào CIRCULAR buf[ ]

kp->buf[kp->head++] = c; kp->head %=

BUFSIZE;

kp->dữ liệu++; // cập nhật bộ đếm

thức dậy(&kp->dữ liệu); // đánh thức quá trình ngủ, nếu có

5.6.2.2 Trình điều khiển thiết bị đầu ra

Trình điều khiển thiết bị đầu ra cũng bao gồm ba phần; nửa dưới, là bộ xử lý ngắt, nửa trên, được quy trình gọi để xuất dữ liệu và phần giữa

chứa bộ đệm dữ liệu và các biến điều khiển, được chia sẻ bởi nửa dưới và nửa trên. Sự khác biệt chính giữa trình điều khiển thiết bị đầu ra

và trình điều khiển thiết bị đầu vào là vai trò của trình xử lý quy trình và trình xử lý ngắt bị đảo ngược. Trong trình điều khiển thiết bị

đầu ra, quá trình ghi dữ liệu vào bộ đệm dữ liệu. Nếu bộ đệm dữ liệu đầy, nó sẽ chuyển sang chế độ ngủ để chờ chỗ trong bộ đệm dữ liệu. Trình

xử lý ngắt trích xuất dữ liệu từ bộ đệm và xuất chúng ra thiết bị. Sau đó, nó đánh thức bất kỳ tiến trình nào đang ngủ cho các phòng trong bộ

đệm dữ liệu. Điểm khác biệt thứ hai là đối với hầu hết các thiết bị đầu ra, trình xử lý ngắt phải vô hiệu hóa rõ ràng các ngắt của thiết bị

khi không còn dữ liệu để xuất.

Nếu không, thiết bị sẽ tiếp tục tạo ra các ngắt, dẫn đến một vòng lặp vô hạn. Điểm khác biệt thứ ba là việc một số quy trình chia sẻ cùng một

thiết bị đầu ra thường được chấp nhận, nhưng một thiết bị đầu vào chỉ có thể cho phép một quy trình hoạt động tại một thời điểm. Ngược lại,

các tiến trình có thể nhận đầu vào ngẫu nhiên từ cùng một thiết bị đầu vào.

5.7 Hệ thống nhúng theo hướng sự kiện sử dụng chế độ Ngủ/Thức

Với việc tạo quy trình động và đồng bộ hóa quy trình, chúng tôi có thể triển khai các hệ thống đa nhiệm theo hướng sự kiện mà không cần các

vòng lặp chờ bận. Chúng tôi chứng minh hệ thống như vậy bằng chương trình ví dụ C5.4.

Chương trình ví dụ C5.4: Chúng tôi giả định rằng phần cứng hệ thống bao gồm ba thiết bị; một bộ đếm thời gian, một UART và một bàn phím.

Phần mềm hệ thống bao gồm ba quy trình, mỗi quy trình điều khiển một thiết bị. Để thuận tiện, chúng tôi cũng trang bị một màn hình LCD để hiển

thị kết quả đầu ra từ quy trình hẹn giờ và bàn phím.

Khi khởi động, mỗi tiến trình sẽ đợi một sự kiện cụ thể. Một tiến trình chỉ chạy khi sự kiện được chờ đợi đã xảy ra. Trong trường hợp này,

các sự kiện là số lượng bộ đếm thời gian và các hoạt động I/O. Ở mỗi giây, quá trình hẹn giờ sẽ hiển thị đồng hồ treo tường trên màn hình LCD.

Bất cứ khi nào một dòng đầu vào được nhập từ UART, quy trình UART sẽ lấy dòng đó và lặp lại nó đến thiết bị đầu cuối nối tiếp.

Tương tự, bất cứ khi nào một dòng đầu vào được nhập từ KBD, quy trình KBD sẽ lấy dòng đó và phản hồi nó tới màn hình LCD. Như trước đây, hệ

thống chạy trên máy ảo ARM mô phỏng trong QEMU. Trình tự khởi động của hệ thống giống hệt với trình tự khởi động của C5.3. Chúng tôi sẽ chỉ

trình bày cách thiết lập hệ thống để chạy các quy trình được yêu cầu và phản ứng của chúng đối với các sự kiện. Hệ thống hoạt động như sau.
Machine Translated by Google

130 5 Quản lý quy trình trong hệ thống nhúng

(1). Khởi tạo: sao

chép vectơ, cấu hình VIC và SIC cho các ngắt; chạy quy

trình ban đầu P0, có mức ưu tiên thấp nhất là 0; khởi tạo trình
điều khiển cho LCD, hẹn giờ, UART và KBD; bắt đầu hẹn giờ; (2). Tạo tác

vụ: P0 gọi kfork(NAME_task, Priority) để tạo bộ đếm thời gian, các tiến trình UART và KBD và nhập chúng vào ReadyQueue. Mỗi quy trình

thực thi hàm NAME_task() của riêng nó với mức độ ưu tiên (tĩnh), nằm trong khoảng từ 3 đến 1.

(3). Sau đó, P0 thực thi vòng lặp while(1), trong đó nó chuyển tiến trình bất cứ khi nào hàng đợi sẵn sàng không trống.

(4). Mỗi tiến trình sẽ tiếp tục thực thi hàm NAME_code() của chính nó, đây là một vòng lặp vô hạn. Mỗi tiến trình gọi chế độ ngủ (sự

kiện) để ngủ trên một giá trị sự kiện duy nhất (địa chỉ của cấu trúc dữ liệu thiết bị).

(5). Khi một sự kiện xảy ra, trình xử lý ngắt của thiết bị sẽ gọi Wakeup(event) để đánh thức quá trình tương ứng. Khi thức dậy, mỗi

tiến trình sẽ tiếp tục chạy để xử lý sự kiện. Ví dụ: bộ xử lý ngắt hẹn giờ không còn hiển thị đồng hồ treo tường nữa. Nó được thực

hiện bởi quá trình hẹn giờ trên mỗi giây.

Sau đây liệt kê mã C của chương trình ví dụ C5.4.

/************ Mã C của Chương trình mẫu C5.4 ***********/

#include "type.h"

#include "vid.c" // Trình điều khiển LCD

#include "kbd.c" // trình điều khiển KBD

#include "uart.c" // trình điều khiển UART

#include "timer.c" // trình điều khiển hẹn giờ

#include "Exceptions.c" // trình xử lý ngoại lệ #include "queue.c"

// các hàm xếp hàng //

#include "kernel.c" kernel để quản lý tác vụ

int copy_vectors() { // sao chép vectơ như trước } int irq_chandler() { //

gọi trình xử lý IRQ của bộ đếm thời gian, UART, KBD }

int time_handler(){ // trên mỗi giây: kwakeup(&timer); }

int uart_handler() { // trên dòng đầu vào: kwakeup(&uart); }

int kbd_handler() { // trên dòng đầu vào: kwakeup(&kbd); }

int i, hh, mm, ss; // toàn cục cho time_handler

đồng hồ char[16] = {"00:00:00"};

int time_task() // mã của time_task

trong khi(1){

printf("timer_task %d đang chạy\n", đang chạy->pid);

ksleep((int)&timer);

// sử dụng đồng hồ bấm giờ để cập nhật ss, mm, hh; sau đó hiển thị đồng hồ treo tường

đồng hồ[7]='0'+(ss%10); đồng hồ[6]='0'+(ss/10);

đồng hồ[4]='0'+(mm%10); đồng hồ[3]='0'+(mm/10);

đồng hồ[1]='0'+(hh%10); đồng hồ[0]='0'+(hh/10);

cho (i=0; i<8; i++){

kpchar(đồng hồ[i], 0, 70+i);

int uart_task() // mã của uart_task

dòng char[128];

trong khi(1){

uprintf("uart_task %d ngủ cho dòng từ UART\n", Running->pid);

ksleep((int)&uart);
Machine Translated by Google

5.7 Hệ thống nhúng theo hướng sự kiện sử dụng chế độ Ngủ/Thức 131

uprintf("uart_task %d đang chạy\n", đang chạy->pid);

uget(dòng);

uprintf("line = %s\n", line);

int kbd_task() // mã của kbd_task

dòng char[128];

trong khi(1){

printf("KBD task %d ngủ cho một dòng từ KBD\n", Running->pid);

ksleep((int)&kbd);

printf("Tác vụ KBD %d đang chạy\n", đang chạy->pid); kgets(dòng);

printf("line =

%s\n", line);

int chính()

fbuf_init(); // Trình điều khiển LCD

uart_init(); // trình điều khiển UART

kbd_init(); // trình điều khiển KBD

printf("Chào mừng đến với Wanix trong ARM\n"); // khởi

tạo các ngắt VIC: giống như trước time_init(); // trình điều khiển

hẹn giờ

bộ đếm thời gian_start();

kernel_init(); // khởi tạo kernel và chạy P0

printf("P0 tạo task\n");

kfork((int)timer_task, 3); // nhiệm vụ hẹn giờ

kfork((int)uart_task, 2); // nhiệm vụ uart

kfork((int)kbd_task, 1); // nhiệm vụ kbd

while(1){ // P0 chạy bất cứ khi nào không có tác vụ nào có thể chạy được

nếu (sẵn sàng)

tswitch();

5.7.1 Trình diễn hệ thống nhúng theo sự kiện sử dụng chế độ Ngủ/Thức

Hình 5.7 hiển thị màn hình đầu ra khi chạy chương trình ví dụ C5.4, minh họa một hệ thống đa tác vụ theo hướng sự kiện.
Như Hình 5.7 hiển thị, tác vụ hẹn giờ sẽ hiển thị đồng hồ treo tường trên màn hình LCD trong mỗi giây. Tác vụ uart chỉ in
một dòng tới UART0 khi có dòng đầu vào từ cổng UART0 và tác vụ kbd chỉ in một dòng tới LCD khi có dòng đầu vào từ bàn
phím. Trong khi các tác vụ này đang ngủ cho các sự kiện được chờ đợi, hệ thống đang chạy tiến trình nhàn rỗi P0, tiến
trình này được chuyển hướng để xử lý tất cả các ngắt. Ngay khi một tác vụ được đánh thức và được đưa vào hàng đợi sẵn
sàng, P0 sẽ chuyển quy trình để chạy tác vụ mới được đánh thức.
Machine Translated by Google

132 5 Quản lý quy trình trong hệ thống nhúng

Hình 5.7 Hệ thống đa nhiệm hướng sự kiện sử dụng chế độ ngủ/thức

5.8 Quản lý tài nguyên bằng chế độ Ngủ/Thức

Ngoài việc thay thế vòng lặp chờ bận trong trình điều khiển thiết bị, chế độ ngủ/thức cũng có thể được sử dụng để đồng bộ hóa quy

trình chung. Cách sử dụng điển hình của chế độ ngủ/thức là để quản lý tài nguyên. Tài nguyên là thứ chỉ có thể được sử dụng bởi một

quy trình tại một thời điểm, ví dụ: vùng bộ nhớ để cập nhật, máy in, v.v. Mỗi tài nguyên được biểu thị bằng một biến res_status, giá

trị này bằng 0 nếu tài nguyên đó MIỄN PHÍ và khác 0 nếu tài nguyên đó là MIỄN PHÍ. BẬN. Quản lý tài nguyên bao gồm các chức năng sau

int Acacqui_resource(); // lấy tài nguyên để sử dụng độc quyền int


Release_resource(); // giải phóng tài nguyên sau khi sử dụng

Khi một tiến trình cần một tài nguyên, nó sẽ gọi Acacqui_resource(), cố gắng lấy một tài nguyên để sử dụng riêng. Trong

Acacqui_re-source(), quy trình sẽ kiểm tra res_status trước tiên. Nếu res_status bằng 0, quy trình sẽ đặt nó thành 1 và trả về OK

nếu thành công. Nếu không, nó sẽ chuyển sang chế độ ngủ, chờ tài nguyên trở thành MIỄN PHÍ. Trong khi tài nguyên BẬN, bất kỳ quá

trình nào khác gọi Acacqui_resource() cũng sẽ chuyển sang chế độ ngủ trên cùng một giá trị sự kiện. Khi quy trình chứa tài nguyên

gọi Release_recource(), nó sẽ xóa res_status thành 0 và đưa ra lệnh Wakeup(&res_status) để đánh thức TẤT CẢ các quy trình đang chờ

tài nguyên. Khi thức dậy, mỗi tiến trình phải cố gắng lấy lại tài nguyên. Điều này là do khi quá trình đánh thức chạy, tài nguyên có

thể không còn nữa. Đoạn mã sau đây hiển thị thuật toán quản lý tài nguyên bằng chế độ ngủ/thức.
Machine Translated by Google

5.8 Quản lý tài nguyên bằng chế độ Ngủ/Thức 133

int res_status = 0; // tài nguyên ban đầu MIỄN PHÍ

-------------------------------------------------- --------------------------------

int thu được_resource() | int phát hành_resource()

{ |{

trong khi(1){ |

int SR = int_off(); | nếu int SR = int_off();

(res_status==0){ |

res_status = 1; res_status = 0;

phá vỡ; | |

} |

ngủ(&res_status); | | thức dậy(&res_status);

int_on(SR); int_on(SR);

trả lại OK; trả lại OK;

} | | |}
-------------------------------------------------- ----

5.8.1 Những thiếu sót về Ngủ/Thức

Ngủ và thức là những công cụ đơn giản để đồng bộ hóa quy trình, nhưng chúng cũng có những nhược điểm sau.

. Một sự kiện chỉ là một giá trị. Nó không có bất kỳ vị trí bộ nhớ nào để ghi lại sự xuất hiện của một sự kiện. Quá trình phải đi đến

ngủ trước khi một tiến trình khác hoặc trình xử lý ngắt cố gắng đánh thức nó. Thứ tự ngủ trước dậy sau luôn có thể là

đạt được trong hệ thống UP, nhưng không nhất thiết phải trong hệ thống MP. Trong hệ thống MP, các tiến trình có thể chạy trên các CPU khác nhau

đồng thời (song song). Không thể đảm bảo thứ tự thực hiện của các tiến trình. Vì vậy, ngủ/thức là

chỉ thích hợp cho hệ thống UP.

. Khi được sử dụng để quản lý tài nguyên, nếu một tiến trình chuyển sang chế độ ngủ để chờ tài nguyên thì nó phải thử lại để lấy lại tài nguyên

sau khi thức dậy và có thể phải lặp lại chu kỳ ngủ-thức-thử lại nhiều lần mới thành công (nếu có). Các

các vòng thử lại lặp đi lặp lại có nghĩa là hiệu quả kém do chi phí chuyển đổi ngữ cảnh quá cao.

5.9 Ngữ nghĩa

Một cơ chế tốt hơn để đồng bộ hóa quy trình là semaphore, nó không có những nhược điểm nêu trên.

ngủ/thức dậy. Một semaphore (đếm) là một cấu trúc dữ liệu

semaphore cấu trúc typedef{

int spinlock; // khóa quay, chỉ cần trong hệ thống MP

giá trị int; // giá trị ban đầu của semaphore

hàng đợi PROC * // Hàng đợi FIFO của các tiến trình bị chặn

}SEMAPHORE;

Trong cấu trúc đèn hiệu, trường spinlock nhằm đảm bảo mọi thao tác trên đèn hiệu chỉ có thể được thực hiện dưới dạng một

hoạt động nguyên tử theo từng tiến trình một, ngay cả khi chúng có thể chạy song song trên các CPU khác nhau. Spinlock chỉ cần thiết cho

hệ thống đa bộ xử lý. Đối với hệ thống UP thì không cần thiết và có thể bỏ qua. Các hoạt động nổi tiếng nhất trên sema-phores là P và V, được

xác định (đối với hạt nhân UP) như sau.


Machine Translated by Google

134 5 Quản lý quy trình trong hệ thống nhúng

-------------------------------------------------- ------------------

int P(struct semaphore *s) | int V(struct semaphore *s)

{ |
{

int SR = int_off(); |
int SR = int_off();

s->giá trị--; |
s->giá trị++;

nếu (s->giá trị < 0) |


nếu (s->giá trị <= 0)

(các) khối; |
(các) tín hiệu;

int_on(SR); |
int_on(SR);

} |
}
-------------------------------------------------- ------------------

khối int(struct semaphore *s){ | tín hiệu int(struct semaphore *s){

đang chạy->trạng thái = KHỐI; | PROC *p = dequeue(&s->queue);

enqueue(&s->queue, đang chạy);| tswitch(); | p->trạng thái = SẴN SÀNG;

|} enqueue(&readyQueue, p);

}
-------------------------------------------------- ------------------

Một đèn hiệu nhị phân (Dijkstra 1965) là một đèn hiệu chỉ có thể nhận hai giá trị riêng biệt, 1 là MIỄN PHÍ và 0 là

CHIẾM LĨNH. Hoạt động P/V trên các ngữ nghĩa nhị phân được định nghĩa là

--------------------- P/V trên các ngữ nghĩa nhị phân -----------------

int P(struct semaphore *s) | int V(struct semaphore *s)

{ |{

int SR = int_off(); int SR = int_off();

nếu (s->giá trị == 1) nếu (s->hàng đợi == 0)

s->giá trị = 0; s->giá trị = 1;

khác khác

(các) khối; (các) tín hiệu;

int_on(SR); int_on(SR);

} | | | | | | |}

-------------------------------------------------- -------------

Các ngữ nghĩa nhị phân có thể được coi là một trường hợp đặc biệt của việc đếm các ngữ nghĩa. Vì việc đếm các ngữ nghĩa nhiều hơn

nói chung, chúng ta sẽ không sử dụng hoặc thảo luận về các ngữ nghĩa nhị phân.

5.10 Ứng dụng của Semaphores

Semaphores là công cụ đồng bộ hóa mạnh mẽ có thể được sử dụng để giải quyết tất cả các loại vấn đề đồng bộ hóa quy trình trong

cả hệ thống UP và MP. Sau đây liệt kê cách sử dụng phổ biến nhất của semaphores. Để đơn giản hóa các ký hiệu, chúng ta sẽ

biểu thị s.value = n bởi s = n, và P(&s)/V(&s) lần lượt là P(s)/V(s).

5.10.1 Khóa Semaphore

Vùng quan trọng (CR) là một chuỗi các hoạt động trên các đối tượng dữ liệu được chia sẻ mà chỉ có thể được thực thi bởi một quy trình tại một thời điểm.

thời gian. Semaphores có giá trị ban đầu = 1 có thể được sử dụng làm khóa để bảo vệ CR trong thời gian dài. Mỗi CR được liên kết với

một semaphore s = 1. Các tiến trình truy cập CR bằng cách sử dụng P/V làm khóa/mở khóa, như trong

cấu trúc ngữ nghĩa s = 1;


Quy trình: P (các); // lấy semaphore để khóa CR

// CR được bảo vệ bởi khóa semaphore s

V (các); // giải phóng semaphore để mở khóa CR

Với khóa semaphore, người đọc có thể xác minh rằng chỉ một tiến trình có thể ở bên trong CR bất kỳ lúc nào.
Machine Translated by Google

5.10 Ứng dụng của Semaphores 135

5.10.2 Khóa câm

Một mutex (Pthreads 2015) là một semaphore khóa có trường chủ sở hữu bổ sung, xác định chủ sở hữu hiện tại của khóa mutex. Khi một mutex được

tạo, chủ sở hữu của nó được khởi tạo bằng 0, tức là không có chủ sở hữu. Khi một tiến trình nhận được một mutex bởi mutex_lock(), nó sẽ trở thành

chủ sở hữu. Một mutex bị khóa chỉ có thể được mở khóa bởi chủ sở hữu của nó. Khi một tiến trình mở khóa một mutex, nó sẽ xóa trường chủ sở hữu

về 0 nếu không có tiến trình nào đang chờ trên mutex. Nếu không, nó sẽ bỏ chặn một quá trình đang chờ từ hàng đợi mutex, quá trình này sẽ trở

thành chủ sở hữu mới và mutex vẫn bị khóa. Việc mở rộng P/V trên các ẩn dụ để khóa/mở khóa mutex là chuyện nhỏ. Chúng tôi để nó như một bài tập

cho người đọc. Sự khác biệt chính giữa mutexes và semaphores là, trong khi mutexes chỉ dùng để khóa, thì semaphores có thể được sử dụng cho cả

khóa và hợp tác xử lý.

5.10.3 Quản lý tài nguyên bằng Semaphore

Một semaphore có giá trị ban đầu n > 0 có thể được sử dụng để quản lý n tài nguyên giống nhau. Mỗi tiến trình cố gắng lấy một tài nguyên duy nhất
để sử dụng riêng. Điều này có thể đạt được như sau.

struct semaphore s = n;

Quy trình: P(s);

sử dụng độc quyền một nguồn tài nguyên;

V (các);

Miễn là s > 0, một quá trình có thể thành công với P(s) để lấy được tài nguyên. Khi tất cả các tài nguyên được sử dụng, các tiến trình yêu

cầu sẽ bị chặn tại P(s). Khi một tài nguyên được giải phóng bởi V(s), một tiến trình bị chặn, nếu có, sẽ được phép tiếp tục sử dụng tài nguyên.

Tại bất kỳ thời điểm nào các bất biến sau đây vẫn giữ nguyên.

s >= 0 : s = số lượng tài nguyên vẫn có sẵn; s < 0 : |s| = số

tiến trình đang chờ trong hàng đợi

5.10.4 Chờ ngắt và tin nhắn

Một semaphore có giá trị ban đầu là 0 thường được sử dụng để chuyển đổi một sự kiện bên ngoài, chẳng hạn như ngắt phần cứng, tin nhắn đến, v.v.

để bỏ chặn một quá trình đang chờ sự kiện. Khi một tiến trình chờ một sự kiện, nó sử dụng P(s) để chặn chính nó trong hàng chờ của semaphore. Khi

sự kiện được chờ đợi xảy ra, một quy trình khác hoặc trình xử lý ngắt sử dụng V(s) để bỏ chặn một quy trình khỏi hàng đợi semaphore, cho phép nó

tiếp tục.

5.10.5 Hợp tác quy trình

Semaphores cũng có thể được sử dụng để hợp tác quy trình. Các trường hợp được trích dẫn rộng rãi nhất liên quan đến hợp tác quy trình là vấn đề

nhà sản xuất-người tiêu dùng và vấn đề người đọc-người viết (Silberschatz và cộng sự 2009; Stallings 2011; Tanenbaum và cộng sự 2006; Wang 2015).

5.10.5.1 Vấn đề của nhà sản xuất-người tiêu dùng Một

tập hợp các quy trình của nhà sản xuất và người tiêu dùng chia sẻ một số lượng bộ đệm hữu hạn. Mỗi bộ đệm chứa một mục duy nhất tại một thời điểm.

Ban đầu, tất cả các bộ đệm đều trống. Khi nhà sản xuất đặt một mục vào bộ đệm trống, bộ đệm sẽ đầy. Khi người tiêu dùng nhận được một mục từ bộ

đệm đầy, bộ đệm sẽ trống, v.v. Nhà sản xuất phải đợi nếu không có bộ đệm trống.

Tương tự, người tiêu dùng phải đợi nếu không có bộ đệm đầy. Hơn nữa, các tiến trình chờ đợi phải được phép tiếp tục khi các sự kiện chờ đợi của

chúng xảy ra. Hình 5.8 cho thấy giải pháp của vấn đề Nhà sản xuất-Người tiêu dùng bằng cách sử dụng các ẩn dụ.

Trong hình 5.8, các tiến trình sử dụng các ngữ nghĩa mutex để truy cập vào bộ đệm tròn dưới dạng CR. Các quy trình sản xuất và tiêu dùng hợp

tác với nhau bằng các ngữ nghĩa đầy đủ và trống rỗng.
Machine Translated by Google

136 5 Quản lý quy trình trong hệ thống nhúng

5.10.5.2 Vấn đề về trình đọc-ghi

Một tập hợp các tiến trình đọc và ghi chia sẻ một đối tượng dữ liệu chung, ví dụ như một biến hoặc một tệp. Yêu cầu là: một người viết

tích cực phải loại trừ tất cả những người khác. Tuy nhiên, người đọc sẽ có thể đọc đối tượng dữ liệu đồng thời nếu không có người ghi hoạt

động. Hơn nữa, cả người đọc và người viết không nên chờ đợi vô thời hạn (chết đói). Hình 5.9 cho thấy giải pháp cho Vấn đề Reader-Writer

bằng cách sử dụng các ẩn dụ.

Trong Hình 5.9, rwsem semaphore thực thi thứ tự FIFO của tất cả các trình đọc và trình ghi đến, điều này giúp ngăn chặn tình trạng đói.

Semaphore (lock) rsem dành cho người đọc cập nhật biến nreader trong vùng quan trọng. Trình đọc đầu tiên trong nhóm trình đọc khóa wsem

để ngăn bất kỳ người viết nào viết trong khi có trình đọc đang hoạt động. Về phía người viết, nhiều nhất một người viết có thể chủ động

viết hoặc chờ trong hàng đợi wsem. Trong cả hai trường hợp, người viết mới sẽ bị chặn trong hàng đợi rwsem. Giả sử rằng không có người

viết nào bị chặn tại rwsem. Tất cả các trình đọc mới có thể chuyển qua cả P(rwsem) và P(rsem), cho phép họ đọc dữ liệu đồng thời. Khi

trình đọc cuối cùng kết thúc, nó sẽ phát ra V(wsem) để cho phép bất kỳ trình soạn thảo nào bị chặn ở wsem tiếp tục. Khi người viết viết
xong, nó sẽ mở khóa cả wsem và rwsem. Ngay khi người viết đợi tại rwsem, tất cả những người mới đến cũng sẽ bị chặn tại rwsem. Điều này

giúp người đọc không bị đói nhà văn.

5.10.6 Ưu điểm của Semaphores

Là một công cụ đồng bộ hóa quy trình, đèn hiệu có nhiều ưu điểm so với chế độ ngủ/thức.

(1). Semaphores kết hợp một bộ đếm, kiểm tra bộ đếm và đưa ra quyết định dựa trên kết quả kiểm tra trong một hoạt động không thể phân chia

duy nhất. Thao tác V chỉ bỏ chặn một quy trình chờ, nếu có, khỏi hàng đợi semaphore. Sau khi thực hiện thao tác P trên semaphore, một tiến

trình được đảm bảo có tài nguyên. Nó không phải thử lại để lấy lại tài nguyên như trường hợp sử dụng chế độ ngủ và thức.

(2). Giá trị của semaphore ghi lại số lần một sự kiện đã xảy ra. Không giống như ngủ/thức dậy, phải tuân theo thứ tự ngủ trước-thức dậy

sau, các tiến trình có thể thực hiện các thao tác P/V trên các đèn hiệu theo bất kỳ thứ tự nào.

5.10.7 Thận trọng khi sử dụng Semaphores

Semaphores sử dụng giao thức khóa. Nếu một tiến trình không thể thu được một đèn hiệu trong P(s), thì nó sẽ bị chặn trong hàng đợi đèn

hiệu, chờ người khác bỏ chặn nó thông qua thao tác V(s). Việc sử dụng các ẩn dụ không đúng cách có thể dẫn đến nhiều vấn đề. Vấn đề được

biết đến nhiều nhất là sự bế tắc (Silberschatz và cộng sự 2009; Tanenbaum và cộng sự 2006). Bế tắc là tình trạng trong đó một

Hình 5.8 Giải quyết vấn đề nhà sản xuất-người tiêu dùng
Machine Translated by Google

5.10 Ứng dụng của Semaphores 137

Hình 5.9 Giải quyết vấn đề đầu đọc-ghi

tập hợp các tiến trình chờ đợi lẫn nhau mãi mãi, do đó không có tiến trình nào có thể tiếp tục. Trong các hệ thống đa nhiệm, bế tắc không được

phép xảy ra. Các phương pháp xử lý deadlock bao gồm phòng ngừa deadlock, tránh deadlock, phát hiện và phục hồi deadlock. Trong số các phương

pháp khác nhau, chỉ có phòng ngừa bế tắc là thực tế và được sử dụng trong các hệ điều hành thực. Một cách đơn giản nhưng hiệu quả để ngăn chặn

bế tắc là đảm bảo rằng các tiến trình yêu cầu các ngữ nghĩa khác nhau theo một thứ tự một chiều, sao cho việc khóa chéo hoặc khóa vòng không

bao giờ có thể xảy ra. Người đọc có thể tham khảo (Wang 2015) để biết cách giải quyết bế tắc nói chung.

5.10.8 Sử dụng Semaphores trong hệ thống nhúng

Chúng tôi chứng minh việc sử dụng ngữ nghĩa trong các hệ thống nhúng bằng các ví dụ sau.

5.10.8.1 Trình điều khiển thiết bị sử dụng Semaphores Trong

trình điều khiển bàn phím của Sect. 5.6.2.1, thay vì sử dụng chế độ ngủ/thức, chúng ta có thể sử dụng semaphore để đồng bộ hóa giữa các tiến

trình và trình xử lý ngắt. Để thực hiện điều này, chúng tôi chỉ cần xác định lại biến dữ liệu của trình điều khiển KBD là một semaphore có giá
trị ban đầu là 0.

typedef dễ bay hơi struct kbd{ // base = 0x10006000

char *cơ sở; // địa chỉ cơ sở của KBD, dưới dạng char *

char buf[BUFSIZE]; // bộ đệm đầu vào

int đầu, đuôi;

cấu trúc dữ liệu semaphore; // data.value=0; data.queue=0;

}KBD;

KBD kbd;

int kgetc() // chương trình chính gọi kgetc() để trả về một char

ký tự c;

KBD *kp = &kbd;


Machine Translated by Google

138 5 Quản lý quy trình trong hệ thống nhúng

P(&kp->dữ liệu); // P trên semaphore dữ liệu của KBD

kho a();

c = kp->buf[kp->tail++]; // lấy ac và cập nhật chỉ số đuôi kp->tail %= BUFSIZE;

mở khóa(); // kích hoạt ngắt IRQ

trả lại c;

(3). kbd_handler(): Viết lại trình xử lý ngắt KBD để bỏ chặn một tiến trình, nếu có. Vì tiến trình không thể can thiệp vào
trình xử lý ngắt nên không cần phải bảo vệ các biến dữ liệu bên trong trình xử lý ngắt.

kbd_handler()

cấu trúc KBD *kp = &kbd;

scode = *(kp->base+KDATA); // đọc mã quét trong thanh ghi dữ liệu

nếu (mã & 0x80) // bỏ qua các bản phát hành chính

trở lại;

if (kp->data.value==BUFSIZE) // bộ đệm đầu vào ĐẦY ĐỦ

trở lại; // bỏ qua khóa hiện tại

c = unsh[scode]; // ánh xạ mã quét tới ASCII

kp->buf[kp->head++] = c; // nhập key vào CIRCULAR buf[ ]

kp->head %= BUFSIZE;

V(&kp->dữ liệu);

Lưu ý rằng trình xử lý ngắt chỉ phát hành V() để bỏ chặn quá trình chờ nhưng nó không bao giờ chặn hoặc chờ. Nếu bộ đệm
đầu vào đầy, nó chỉ cần loại bỏ khóa đầu vào hiện tại và trả về. Có thể thấy, logic của driver mới sử dụng semaphore rõ ràng
hơn rất nhiều và kích thước mã cũng giảm đi đáng kể.

5.10.8.2 Hệ thống nhúng hướng sự kiện sử dụng Semaphore Chương trình ví


dụ C5.4 sử dụng chế độ ngủ/đánh thức để đồng bộ hóa quy trình. Trong chương trình ví dụ tiếp theo, C5.5, chúng ta sẽ sử dụng
P/V trên các đèn hiệu để đồng bộ hóa quy trình. Để ngắn gọn, chúng tôi chỉ hiển thị trình điều khiển KBD đã sửa đổi và mã xử
lý kbd. Để rõ ràng, các sửa đổi được hiển thị bằng các đường nét đậm.

cấu trúc kbd{

char *cơ sở;

char buf[BUFSIZE]; // #define BUFSIZE 128

int đầu, đuôi;

struct dữ liệu semaphore, dòng;

} kbd;

kbd_init()

cấu trúc kbd *kp = &kbd;

kp->cơ sở = 0x10006000; // Cơ sở KBD trong Versatilepb

kp->đầu = kp->đuôi = 0;

kp->data.value = 0; kp->data.queue = 0;

kp->line.value = 0; kp->line.queue = 0;

int kbd_handler()

{
Machine Translated by Google

5.10 Ứng dụng của Semaphores 139

cấu trúc kbd *kp = &kbd;

// cùng mã như trước: nhập khóa ASCII vào bộ đệm đầu vào;

V(&kp->dữ liệu);

nếu (c=='\r') // return key: có một dòng đầu vào

V(&kp->dòng);

int kgetc() // xử lý lệnh gọi kgetc() để trả về một char

ký tự c;

KBD *kp = &kbd;

P(&kp->dữ liệu);

kho a(); // vô hiệu hóa các ngắt IRQ

c = kp->buf[kp->tail++]; // lấy ac và cập nhật chỉ mục đuôi

kp->tail %= BUFSIZE;

mở khóa(); // kích hoạt ngắt IRQ

trả lại c;

int kgets(char *line) // lấy một chuỗi

ký tự c;

while((c= kgetc()) != '\r')

*dòng++ = c;

*dòng = 0;

int kbd_task()

dòng char[128];

cấu trúc kbd *kp = &kbd;

trong khi(1){

P(&kp->dòng); // chờ một dòng

kgets(dòng);

printf("line = %s\n", line);

int chính()

{ // mã khởi tạo CÙNG như trước

printf("P0 tạo task\n");

kfork((int)kbd_task, 1);

while(1){ // P0 chạy bất cứ khi nào không có tác vụ nào có thể chạy được

while(!readyQueue); // vòng lặp nếu hàng đợi trống

tswitch();

Hình 5.10 cho thấy kết quả đầu ra khi chạy chương trình C5.5.

5.11 Cơ chế đồng bộ hóa khác

Nhiều nhân hệ điều hành sử dụng các cơ chế khác để đồng bộ hóa tiến trình. Bao gồm các
Machine Translated by Google

140 5 Quản lý quy trình trong hệ thống nhúng

Hình 5.10 Hệ thống đa nhiệm hướng sự kiện sử dụng ngữ nghĩa

5.11.1 Cờ sự kiện trong OpenVMS

OpenVMS (trước đây là VAX/VMS) (OpenVMS 2014) sử dụng cờ sự kiện để đồng bộ hóa quy trình. Ở dạng đơn giản nhất, cờ sự kiện là một bit đơn, nằm

trong không gian địa chỉ của nhiều quy trình. Theo mặc định hoặc theo lệnh gọi chung rõ ràng, mỗi cờ sự kiện được liên kết với một tập hợp quy

trình cụ thể. OpenVMS cung cấp các chức năng dịch vụ cho các tiến trình để thao tác các cờ sự kiện liên quan của chúng bằng cách

set_event(b) : đặt b thành 1 và Wakeup Waiter(b) nếu có;

clear_event(b): xóa b về 0;

test_event(b): kiểm tra b cho 0 hoặc 1;

wait_event(b): đợi cho đến khi b được đặt;

Đương nhiên, quyền truy cập vào cờ sự kiện phải loại trừ lẫn nhau. Sự khác biệt giữa cờ sự kiện và sự kiện Unix là:

. Một sự kiện Unix chỉ là một giá trị, không có vị trí bộ nhớ để ghi lại sự xuất hiện của sự kiện. Một tiến trình phải ngủ trên một sự kiện trước

khi một tiến trình khác cố gắng đánh thức nó sau đó. Ngược lại, mỗi cờ sự kiện là một bit chuyên dụng, có thể ghi lại sự xuất hiện của một sự kiện.

Do đó, khi sử dụng cờ sự kiện, thứ tự set_event và wait_event không thành vấn đề.

Một điểm khác biệt nữa là các sự kiện Unix chỉ khả dụng cho các tiến trình ở chế độ kernel, các cờ sự kiện trong OpenVMS có thể được sử dụng bởi

các tiến trình ở chế độ người dùng.

. Cờ sự kiện trong OpenVMS nằm trong cụm 32 bit mỗi cờ. Một tiến trình có thể đợi một bit cụ thể, bất kỳ hoặc tất cả các sự kiện trong cụm sự kiện.

Trong Unix, một tiến trình chỉ có thể ngủ cho một sự kiện duy nhất.
. Giống như trong Unix, Wakeup(e) trong OpenVMS cũng đánh thức tất cả những người phục vụ trong một sự kiện.

5.11.2 Biến sự kiện trong MVS

MVS của IBM (2010) sử dụng các biến sự kiện để đồng bộ hóa quy trình. Một biến sự kiện là một cấu trúc
Machine Translated by Google

5.11 Cơ chế đồng bộ hóa khác 141

cấu trúc sự kiện_variable{

chút w; // cờ chờ ban đầu = 0 // cờ đăng ban

bit p; đầu = 0

struct proc *ptr; // con trỏ tới PROC đang chờ

} e1, e2,…, vi; // biến sự kiện

Mỗi biến sự kiện e có thể được chờ đợi bởi nhiều nhất một tiến trình tại một thời điểm. Tuy nhiên, một tiến trình có thể đợi bất kỳ số

lượng biến sự kiện nào. Khi một quá trình gọi wait(e) để chờ một sự kiện, nó sẽ không đợi nếu sự kiện đó đã xảy ra (post bit=1).

Nếu không, nó sẽ bật bit w và chờ sự kiện. Khi một sự kiện xảy ra, một quy trình khác sử dụng post(e) để đăng sự kiện bằng cách bật bit p.

Nếu bit w của sự kiện được bật, nó sẽ bỏ chặn quá trình chờ đợi nếu tất cả các sự kiện được chờ đợi của nó đã được đăng.

5.11.3 ENQ/DEQ trong MVS

Ngoài các biến sự kiện, MVS của IBM (2010) cũng sử dụng ENQ/DEQ để quản lý tài nguyên. Ở dạng đơn giản nhất, ENQ(tài nguyên) cho phép một

quy trình giành được quyền kiểm soát độc quyền đối với tài nguyên. Một tài nguyên có thể được chỉ định theo nhiều cách khác nhau, chẳng

hạn như vùng bộ nhớ, nội dung của vùng bộ nhớ, v.v. Một quy trình sẽ chặn nếu tài nguyên không khả dụng. Mặt khác, nó sẽ giành được quyền

kiểm soát độc quyền đối với tài nguyên cho đến khi nó được giải phóng bởi hoạt động DEQ (tài nguyên). Giống như các biến sự kiện, một tiến

trình có thể gọi ENQ(r1,r2,…rn) để chờ tất cả hoặc một tập hợp con của nhiều tài nguyên.

5.12 Cấu trúc đồng bộ hóa cấp cao

Mặc dù P/V trên các ẩn dụ là công cụ đồng bộ hóa mạnh mẽ nhưng việc sử dụng chúng trong các chương trình đồng thời vẫn còn rải rác. Bất kỳ

việc sử dụng sai P/V nào cũng có thể dẫn đến các vấn đề, chẳng hạn như bế tắc. Để giúp khắc phục vấn đề này, nhiều cơ chế đồng bộ hóa quy

trình cấp cao đã được đề xuất.

5.12.1 Biến điều kiện

Trong Pthreads (Buttlar et al. 1996; Pthreads 2015), các luồng có thể sử dụng các biến điều kiện để đồng bộ hóa. Để sử dụng biến điều

kiện, trước tiên hãy tạo một mutex, m, để khóa CR chứa các biến dùng chung, ví dụ như bộ đếm. Sau đó tạo một biến điều kiện, con, liên kết
với mutex. Khi một thread muốn truy cập vào biến được chia sẻ, trước tiên nó sẽ khóa mutex. Sau đó, nó kiểm tra biến. Nếu giá trị bộ đếm

không như mong đợi, luồng có thể phải chờ, như trong

số int // biến chia sẻ của thread

pthread_mutex_lock(m); // khóa mutex trước

if (số lượng không như mong đợi)

pthread_cond_wait(con, m); // đợi trong con và mở khóa mutex

pthread_mutex_unlock(m); // mở khóa mutex

pthread_cond_wait(con, m) chặn luồng gọi trên biến điều kiện, điều này sẽ tự động và nguyên tử mở khóa mutex m. Trong khi một luồng bị

chặn trên biến điều kiện, một luồng khác có thể sử dụng pthread_cond_signal (con) để bỏ chặn một luồng đang chờ, như trong

pthread_lock(m);

thay đổi số lượng biến được chia sẻ;

if (đếm đạt đến một giá trị nhất định)

pthread_cond_signal(con); // bỏ chặn một thread trong con

pthread_unlock(m);

Khi một luồng không bị chặn chạy, mutex m sẽ tự động bị khóa nguyên tử, cho phép luồng không bị chặn tiếp tục trong CR của mutex m.

Ngoài ra, một luồng có thể sử dụng pthread_cond_broadcast(con) để bỏ chặn tất cả các luồng
Machine Translated by Google

142 5 Quản lý quy trình trong hệ thống nhúng

chờ biến điều kiện tương tự, tương tự như đánh thức trong Unix. Vì vậy, mutex hoàn toàn dành cho việc khóa, các biến điều kiện có thể được

sử dụng để hợp tác các luồng.

5.12.2 Màn hình

Màn hình (Hoare 1974) là Kiểu dữ liệu trừu tượng (ADT), bao gồm các đối tượng dữ liệu được chia sẻ và tất cả các thủ tục hoạt động trên các

đối tượng dữ liệu được chia sẻ. Giống như ADT trong các ngôn ngữ lập trình hướng đối tượng (OOP), thay vì các mã phân tán trong các quy

trình khác nhau, tất cả các mã hoạt động trên các đối tượng dữ liệu dùng chung đều được gói gọn bên trong một màn hình. Không giống như ADT

trong OOP, màn hình là CR, chỉ cho phép một quá trình thực thi bên trong màn hình tại một thời điểm. Các tiến trình chỉ có thể truy cập các

đối tượng dữ liệu được chia sẻ của một màn hình bằng cách gọi các thủ tục giám sát, như trong

MONITOR m.procedure(tham số);

Trình biên dịch ngôn ngữ lập trình đồng thời dịch các lệnh gọi thủ tục giám sát khi nhập CR màn hình và tự động cung cấp tính năng bảo

vệ trong thời gian chạy. Khi một quy trình hoàn tất việc thực hiện quy trình giám sát, nó sẽ thoát khỏi màn hình, thao tác này sẽ tự động

mở khóa màn hình, cho phép một quy trình khác truy cập vào màn hình. Trong khi thực thi bên trong màn hình, nếu một quy trình bị chặn, trước

tiên nó sẽ tự động thoát khỏi màn hình. Như thường lệ, một quy trình bị chặn sẽ đủ điều kiện để vào lại màn hình khi nó được một quy trình

khác hỗ trợ. Màn hình tương tự như các biến điều kiện nhưng không có khóa mutex rõ ràng, điều này làm cho chúng có phần "trừu tượng" hơn các

biến điều kiện. Mục tiêu của màn hình và các cấu trúc đồng bộ hóa cấp cao khác là giúp người dùng viết các chương trình đồng thời "đồng bộ

hóa chính xác". Ý tưởng này tương tự như việc sử dụng các ngôn ngữ kiểm tra kiểu mạnh để giúp người dùng viết chương trình "đúng cú pháp".

Những công cụ đồng bộ hóa cấp cao này được sử dụng chủ yếu trong lập trình đồng thời nhưng hiếm khi được sử dụng trong các hệ điều hành thực.

5.13 Quá trình giao tiếp

Giao tiếp quy trình đề cập đến các sơ đồ hoặc cơ chế cho phép các quy trình trao đổi thông tin. Giao tiếp quy trình có thể được thực hiện

theo nhiều cách khác nhau, tất cả đều phụ thuộc vào sự đồng bộ hóa quy trình.

5.13.1 Bộ nhớ dùng chung

Cách đơn giản nhất để giao tiếp với các tiến trình là thông qua bộ nhớ dùng chung. Trong hầu hết các hệ thống nhúng, tất cả các tiến trình

đều chạy trong cùng một không gian địa chỉ. Bộ nhớ dùng chung vừa tự nhiên vừa dễ sử dụng để liên lạc trong quá trình. Để đảm bảo các tiến

trình chỉ truy cập vào bộ nhớ dùng chung, chúng tôi có thể sử dụng khóa semaphore hoặc mutex để bảo vệ bộ nhớ dùng chung như một vùng quan

trọng. Nếu một số quy trình chỉ đọc nhưng không sửa đổi bộ nhớ dùng chung, chúng tôi có thể sử dụng thuật toán trình đọc-ghi để cho phép các

trình đọc đồng thời. Khi sử dụng bộ nhớ dùng chung để giao tiếp quy trình, cơ chế này chỉ đảm bảo các quy trình đọc/ghi bộ nhớ dùng chung

một cách được kiểm soát. Người dùng hoàn toàn có quyền xác định và giải thích ý nghĩa của nội dung bộ nhớ dùng chung.

5.13.2 Ống

Ống là kênh liên lạc giữa các quá trình một chiều để các quá trình trao đổi luồng dữ liệu. Một pipe có một đầu đọc và một đầu ghi. Dữ liệu

được ghi vào đầu ghi của ống có thể được đọc từ đầu đọc của ống. Kể từ khi ra mắt lần đầu trong Unix ban đầu, các pipe đã được tích hợp vào

hầu hết các hệ điều hành, với nhiều biến thể. Một số hệ thống cho phép các đường ống hoạt động hai chiều, trong đó dữ liệu có thể được

truyền theo cả hai hướng. Ống thông thường dành cho các quá trình liên quan. Các đường ống được đặt tên là các kênh liên lạc FIFO giữa các

quy trình không liên quan. Ống đọc và ghi thường đồng bộ và chặn. Một số hệ thống hỗ trợ các hoạt động đọc/ghi không chặn và không đồng bộ

trên đường ống. Để đơn giản, chúng ta sẽ coi một đường ống là kênh liên lạc FIFO có kích thước hữu hạn giữa một tập hợp các quy trình. Quá

trình đọc và ghi của một đường ống được đồng bộ hóa theo cách sau. Khi đầu đọc đọc từ một ống, nếu ống có dữ liệu, đầu đọc sẽ đọc bao nhiêu

tùy ý (tối đa kích thước ống) và trả về số byte đã đọc. Nếu pipe không có dữ liệu nhưng vẫn có người ghi, thì
Machine Translated by Google

5.13 Quá trình giao tiếp 143

đầu đọc chờ dữ liệu. Khi người viết ghi dữ liệu vào một đường ống, nó sẽ đánh thức những người đọc đang chờ, cho phép họ tiếp tục. Nếu đường ống

không có dữ liệu và cũng không có bộ ghi, thì đầu đọc sẽ trả về 0. Vì đầu đọc chờ dữ liệu nếu đường ống vẫn có bộ ghi, giá trị trả về 0 chỉ có

nghĩa là một điều, đó là đường ống không có dữ liệu và cũng không có bộ ghi. Trong trường hợp đó, người đọc có thể ngừng đọc từ đường ống. Khi một

nhà văn viết vào một đường ống, nếu đường ống còn chỗ, nó sẽ viết nhiều như nó cần hoặc cho đến khi đường ống đầy. Nếu tẩu không còn chỗ mà vẫn có

đầu đọc thì người viết chờ chỗ. Khi một đầu đọc đọc dữ liệu từ đường ống để tạo thêm phòng, nó sẽ đánh thức những người ghi đang chờ, cho phép họ

tiếp tục. Tuy nhiên, nếu một đường ống không còn người đọc nữa, người ghi phải phát hiện đây là lỗi đường ống bị hỏng và hủy bỏ.

5.13.2.1 Đường ống trong Unix/Linux Trong

Unix/Linux, đường ống là một phần không thể thiếu của hệ thống tệp, giống như các thiết bị I/O, được coi là các tệp đặc biệt. Mỗi quy trình có ba

luồng tệp tiêu chuẩn; stdin cho đầu vào, stdout cho đầu ra và stderr để hiển thị thông báo lỗi, thường được liên kết với cùng một thiết bị với

thiết bị xuất chuẩn. Mỗi luồng tệp được xác định bằng một bộ mô tả tệp của quy trình, giá trị này là 0 cho stdin, 1 cho stdout và 2 cho stderr.

Về mặt khái niệm, một đường ống là một tệp FIFO hai đầu, kết nối thiết bị xuất chuẩn của quy trình ghi với thiết bị xuất chuẩn của quy trình trình

đọc. Điều này được thực hiện bằng cách thay thế bộ mô tả tệp 1 của quy trình ghi bằng đầu ghi của ống và thay thế bộ mô tả tệp 0 của quy trình đọc

bằng đầu đọc của ống. Ngoài ra, pipe sử dụng các biến trạng thái để theo dõi trạng thái của pipe, cho phép phát hiện các tình trạng bất thường như

không còn người ghi và ống bị hỏng, v.v.

5.13.2.2 Đường ống trong hệ thống nhúng Hầu hết các

hệ thống nhúng không hỗ trợ hệ thống tệp hoặc hệ thống tệp có thể không tương thích với Unix. Do đó, các quy trình trong hệ thống nhúng có thể

không mở tệp và mô tả tệp. Mặc dù vậy, chúng tôi vẫn có thể triển khai các đường ống để liên lạc quy trình trong các hệ thống nhúng. Về nguyên

tắc, các đường ống cũng tương tự như bài toán nhà sản xuất-người tiêu dùng, ngoại trừ những khác biệt sau.

. Trong vấn đề nhà sản xuất-người tiêu dùng, một quy trình sản xuất bị chặn chỉ có thể được báo hiệu bởi một quy trình tiêu dùng khác và ngược lại.

Các ống sử dụng các biến trạng thái để theo dõi số lượng tiến trình đọc và ghi. Khi trình ghi ống phát hiện ống không còn đầu đọc nữa, nó sẽ trả

về lỗi ống bị hỏng. Khi đầu đọc phát hiện ống không còn đầu ghi và cũng không có dữ liệu, nó trả về 0.

. Thuật toán nhà sản xuất-người tiêu dùng sử dụng ngữ nghĩa để đồng bộ hóa. Semaphores phù hợp cho các tiến trình ghi/đọc dữ liệu có cùng kích

thước. Ngược lại, đầu đọc và ghi ống không cần phải đọc/ghi dữ liệu có cùng kích thước. Ví dụ: người viết có thể viết dòng nhưng người đọc đọc ký

tự và ngược lại.
. Thao tác V trên semaphore sẽ bỏ chặn tối đa một quy trình đang chờ. Mặc dù hiếm gặp nhưng một ống có thể có nhiều đầu ghi và đầu đọc ở cả hai

đầu. Khi một tiến trình ở một trong hai đầu thay đổi trạng thái đường ống, nó sẽ bỏ chặn tất cả các tiến trình đang chờ ở đầu bên kia. Trong

trường hợp này, chế độ ngủ/thức phù hợp hơn P/V trên các ẩn dụ. Vì lý do này, các đường ống thường được triển khai bằng cách sử dụng chế độ ngủ/

thức để đồng bộ hóa.

Sau đây, chúng tôi sẽ trình bày cách triển khai một đường ống đơn giản hóa để giao tiếp trong quá trình. Đường ống đơn giản hóa hoạt động như

các đường ống được đặt tên trong Linux. Nó cho phép các tiến trình ghi/đọc một chuỗi byte thông qua đường ống, nhưng nó không kiểm tra hoặc xử lý

các tình trạng bất thường, chẳng hạn như đường ống bị hỏng. Việc triển khai đầy đủ các đường ống dưới dạng luồng tệp sẽ được trình bày sau trong

Chương. số 8 khi chúng ta thảo luận về các hệ điều hành nhúng có mục đích chung. Đường ống đơn giản hóa được thực hiện như sau.

(1). Đối tượng đường ống: đường ống là cấu trúc dữ liệu (toàn cầu)

ống cấu trúc typedef {

char buf[PSIZE]; // bộ đệm dữ liệu vòng tròn

int đầu, đuôi; // chỉ số buf tròn

dữ liệu int, phòng; // số lượng dữ liệu & phòng trong pipe

trạng thái int; // MIỄN PHÍ hoặc BẬN

}ĐƯỜNG ỐNG;

Ống PIPE[NPIPE]; // đối tượng PIPE toàn cục

Khi hệ thống khởi động, tất cả các đối tượng đường ống được khởi tạo thành MIỄN PHÍ.
Machine Translated by Google

144 5 Quản lý quy trình trong hệ thống nhúng

(1). PIPE *create_pipe(): điều này tạo ra một đối tượng PIPE trong không gian địa chỉ (được chia sẻ) của tất cả các quy trình. Nó cấp phát một

đối tượng PIPE trống, khởi tạo nó và trả về một con trỏ tới đối tượng PIPE đã tạo.

(2). Đường ống đọc/ghi: Đối với mỗi đường ống, người dùng phải chỉ định một quy trình là người ghi hoặc người đọc, nhưng không phải cả hai.

Writer xử lý cuộc gọi.

int write_pipe(PIPE *pipePtr, char buf[ ], int n);

để ghi n byte từ buf[] vào pipe. Giá trị trả về là số byte được ghi vào pipe. Trình đọc xử lý cuộc gọi

int read_pipe(PIPE *pipePtr, char buf[], int n);

cố gắng đọc n byte từ đường ống. Giá trị trả về là số byte thực tế được đọc. Sau đây cho thấy

thuật toán đọc/ghi ống, sử dụng chế độ ngủ/thức để đồng bộ hóa quy trình.

/*---------- Thuật toán pipe_read -------------*/

int read_pipe(PIPE *p, char *buf, int n)

{ int r = 0;

nếu (n<=0)

trả về 0;

xác thực con trỏ PIPE p; // p->trạng thái không được MIỄN PHÍ

trong khi(n){

while(p->data){

*buf++ = p->buf[p->tail++] // đọc một byte vào buf

đuôi %= PSIZE;

p->dữ liệu--; p->phòng++; r++; N--;

nếu (n==0)

phá vỡ;

thức dậy(&p->phòng); // đánh thức nhà văn

nếu (r) // nếu đã đọc một số dữ liệu

trả lại r;

// pipe không có dữ liệu

sleep(&p->data); // ngủ để lấy dữ liệu

/*---------- Thuật toán write_pipe -----------*/ int write_pipe(PIPR *p, char *buf,

int n) { int r = 0;

nếu (n<=0)

trả về 0;

xác thực con trỏ PIPE p; // p->trạng thái không được MIỄN PHÍ

trong khi(n){

while(p->room) p->buf[p-

>head++] = *buf++; // ghi một byte vào pipe; p->đầu %= PSIZE; p->dữ liệu++; p->phòng--;

r++; N--;

nếu (n==0)

phá vỡ;

thức dậy(&p->dữ liệu); // đánh thức độc giả, nếu có.

nếu (n==0)

trả lại r; // ghi xong n byte

// vẫn còn dữ liệu để ghi nhưng pipe không còn chỗ trống
Machine Translated by Google

5.13 Quá trình giao tiếp 145

ngủ(&p->phòng); // ngủ nhận phòng

Lưu ý rằng khi một tiến trình cố gắng đọc n byte từ một pipe, nó có thể trả về ít hơn n byte. Nếu đường ống có dữ liệu, nó sẽ
đọc n byte hoặc số byte có sẵn trong đường ống, tùy theo giá trị nào nhỏ hơn. Nó chỉ chờ nếu đường ống không có dữ liệu. Do đó,
mỗi lần đọc trả về nhiều nhất là PSIZE byte.

(3). Khi một pipe không còn cần thiết nữa, nó có thể được giải phóng bởi destroy_pipe(PIPE *pipePtr), giải phóng đối tượng PIPE và
đánh thức tất cả các tiến trình đang ngủ trên pipe.

5.13.2.3 Trình diễn đường ống Hệ thống


mẫu C5.6 trình diễn đường ống trong một hệ thống nhúng với các quy trình tĩnh.
Khi hệ thống khởi động, mã khởi tạo sẽ tạo một đường ống được trỏ bởi kpipe. Khi quy trình ban đầu P0 chạy, nó tạo ra hai quy
trình, P1 là trình ghi ống và P2 là trình đọc ống. Với mục đích trình diễn, chúng tôi đặt kích thước bộ đệm của ống thành một giá
trị khá nhỏ, PSIZE=16, để nếu người viết cố gắng ghi nhiều hơn 16 byte, nó sẽ đợi phòng. Sau khi đọc từ ống, người đọc đánh thức
người viết, cho phép nó tiếp tục. Trong chương trình trình diễn, đầu tiên P1 nhận được một đường truyền từ cổng UART0. Sau đó, nó
cố gắng ghi dòng vào đường ống. Nó chờ chỗ nếu đường ống đầy. P2 đọc từ đường ống và hiển thị các byte đã đọc. Mặc dù mỗi lần P2
cố gắng đọc 20 byte nhưng số byte thực tế được đọc nhiều nhất là PSIZE. Để cho ngắn gọn, chúng tôi chỉ hiển thị tệp tc của chương
trình mẫu.

/************ tệp tc của Chương trình ống C5.6 ***********/

ỐNG *kpipe; // con trỏ PIPE toàn cục

#include "queue.c"

#include "pv.c"

#include "kbd.c"

#include "uart.c"

#include "vid.c"

#include "ngoại lệ.c"

#include "kernel.c"

#include "timer.c"

#include "pipe.c" // triển khai đường ống

int pipe_writer() // mã tác vụ của người viết ống

struct uart *up = &uart[0];

dòng char[128];

trong khi(1){

uprintf("Nhập một dòng cho task1 để được: ");

printf("task%d đợi dòng từ UART0\n", Running->pid);

ugets(lên, dòng);

uprints(lên, "\r\n");

printf("task%d ghi line=[%s] vào pipe\n", Running->pid, line);

write_pipe(kpipe, line, strlen(line));

int pipe_reader() // mã tác vụ của trình đọc ống

dòng char[128];

int tôi, n;

trong khi(1){

printf("task%d đọc từ pipe\n", Running->pid); n = read_pipe(kpipe, line, 20);

printf("task%d đọc n=%d byte từ pipe : [",

Running->pid, n);
Machine Translated by Google

146 5 Quản lý quy trình trong hệ thống nhúng

cho (i=0; i<n; i++)

kputc(dòng[i]);

printf("]\n");

int chính()

fbuf_init();

kprintf("Chào mừng đến với Wanix trong ARM\n");

uart_init();

kbd_init();

pipe_init(); kpipe // khởi tạo PIPE

= create_pipe(); // tạo kpipe kernel_nit toàn cầu(); kprintf("P0

nhiệm vụ kfork\n");

kfork((int)pipe_writer, 1); // quy trình

ghi ống kfork((int)pipe_reader, 1); // tiến trình đọc ống

trong khi(1){

nếu (sẵn sàng)

tswitch();

Hình 5.11 cho thấy kết quả đầu ra mẫu khi chạy chương trình pipe C5.6.

Hình 5.11 Trình diễn đường ống


Machine Translated by Google

5.13 Quá trình giao tiếp 147

5.13.3 Tín hiệu

Giống như các ngắt đối với CPU, các tín hiệu là các ngắt (phần mềm) đối với một quy trình (Unix 1990), làm chuyển hướng quy trình khỏi các thực thi thông

thường của nó để thực hiện xử lý tín hiệu. Trong một hệ điều hành thông thường, các tiến trình thực thi ở một trong hai chế độ riêng biệt; chế độ kernel

hoặc chế độ người dùng. CPU kiểm tra các ngắt đang chờ xử lý ở cuối mỗi lệnh, điều này không thể nhìn thấy đối với các tiến trình đang thực thi trên CPU.

Tương tự, một quy trình chỉ kiểm tra các tín hiệu đang chờ xử lý ở chế độ kernel, chế độ này không thể nhìn thấy được đối với quy trình ở chế độ người

dùng. Trong hầu hết các hệ thống nhúng, tất cả các tiến trình đều thực thi trong cùng một không gian địa chỉ, do đó chúng không có chế độ người dùng riêng.

Nếu chúng ta sử dụng tín hiệu để liên lạc với quá trình, thì mỗi quá trình phải kiểm tra rõ ràng các tín hiệu đang chờ xử lý trong vòng xử lý quy trình,

tương đương với việc thăm dò các sự kiện. Do đó, đối với các hệ thống nhúng chỉ có một không gian địa chỉ duy nhất, tín hiệu không phù hợp để xử lý giao

tiếp.

5.13.4 Truyền tin nhắn

Truyền tin nhắn cho phép các tiến trình giao tiếp bằng cách trao đổi tin nhắn. Việc truyền tin nhắn có rất nhiều ứng dụng. Trong các hệ điều hành, nó là

một dạng chung của Giao tiếp giữa các quá trình (IPC) (Accetta et al. 1986). Trong mạng máy tính, nó là nền tảng của lập trình hướng máy chủ-máy khách.

Trong điện toán phân tán, nó được sử dụng cho các quy trình song song để trao đổi dữ liệu và đồng bộ hóa. Trong thiết kế hệ điều hành, nó là cơ sở của

cái gọi là vi nhân, v.v. Trong phần này, chúng ta sẽ trình bày việc thiết kế và triển khai một số sơ đồ truyền thông báo bằng cách sử dụng các ngữ nghĩa.

Mục tiêu của việc truyền tin nhắn là cho phép các tiến trình giao tiếp bằng cách trao đổi tin nhắn. Nếu các tiến trình có không gian địa chỉ (chế độ

người dùng) riêng biệt, chúng không thể truy cập trực tiếp vào vùng bộ nhớ của nhau. Trong trường hợp đó, việc truyền tin nhắn phải đi qua kernel. Nếu

tất cả các quy trình chỉ thực thi trong cùng một không gian địa chỉ của hạt nhân, thì việc truyền thông báo cho phép các quy trình trao đổi thông tin

theo cách được kiểm soát nhưng ẩn chi tiết đồng bộ hóa khỏi các quy trình. Nội dung của tin nhắn có thể được thiết kế để phù hợp với nhu cầu của quá

trình giao tiếp. Để đơn giản, chúng ta giả sử rằng nội dung tin nhắn là các chuỗi văn bản có độ dài hữu hạn, ví dụ 128 byte. Để hỗ trợ việc truyền tin

nhắn, chúng tôi giả sử rằng kernel có một bộ đệm tin nhắn hữu hạn, được định nghĩa là

typedef struct mbuf{

struct mbuf *next; // con trỏ tới mbuf tiếp theo

int pid; // pid người gửi

ưu tiên int; // mức độ ưu tiên của tin nhắn

nội dung char[128]; // nội dung tin nhắn

}MBUF;

MBUF mbuf[NMBUF]; // NMBUF = số lượng mbuf

Ban đầu, tất cả bộ đệm tin nhắn đều nằm trong mbufList miễn phí. Để gửi tin nhắn, trước tiên một tiến trình phải nhận được mbuf miễn phí. Sau khi

nhận được tin nhắn, nó sẽ giải phóng mbuf để sử dụng lại. Vì mbufList được nhiều tiến trình truy cập nên đây là vùng quan trọng (CR), phải được bảo vệ.

Vì vậy, chúng tôi xác định semaphore mlock = 1 cho các tiến trình truy cập riêng vào mbufList. Thuật toán của get_mbuf() và put_mbuf() là

MBUF *get_mbuf() // trả về một con trỏ mbuf miễn phí hoặc NULL nếu không có

P(mlock);

MBUF *mp = dequeue(mbuflList); // trả về con trỏ mbuf đầu tiên

V(mlock);

trả lại mp;

int put_mbuf(MBUF *mp) // giải phóng mbuf đã sử dụng vào mbuflist

P(mlock);

enqueue(mbufList)

V(mlock);

}
Machine Translated by Google

148 5 Quản lý quy trình trong hệ thống nhúng

Thay vì hàng đợi tin nhắn tập trung, chúng tôi giả định rằng mỗi PROC có một hàng đợi tin nhắn riêng chứa mbufs

được gửi tới nhưng chưa được nhận bởi tiến trình. Ban đầu, mọi mqueue của PROC đều trống. Mqueue của mỗi quá trình là

cũng là CR vì nó được truy cập bởi tất cả các quy trình của người gửi cũng như chính quy trình đó. Vì vậy, chúng tôi xác định một semaphore khác

PROC.mlock = 1 để bảo vệ hàng đợi thông báo quá trình.

5.13.4.1 Truyền tin nhắn không đồng bộ


Trong sơ đồ truyền tin nhắn không đồng bộ, cả hoạt động gửi và nhận đều không bị chặn. Nếu một tiến trình không thể gửi

hoặc nhận được một tin nhắn, nó sẽ trả về trạng thái không thành công, trong trường hợp đó quá trình có thể thử lại thao tác sau đó. Không đồng bộ

giao tiếp chủ yếu dành cho các hệ thống kết nối lỏng lẻo, trong đó giao tiếp giữa các quá trình không thường xuyên, tức là

các quy trình không trao đổi thông điệp một cách có kế hoạch hoặc thường xuyên. Đối với các hệ thống như vậy, việc truyền tin nhắn không đồng bộ sẽ phức tạp hơn.

phù hợp do tính linh hoạt cao hơn của nó. Các thuật toán của hoạt động gửi-nhận không đồng bộ như sau.

int a_send(char *msg, int pid) // gửi tin nhắn tới pid đích

MBUF *mp;

// xác thực pid mục tiêu, ví dụ: proc[pid] phải là một quy trình hợp lệ

nếu (!(mp = get_mbuf())) // cố gắng nhận mbuf miễn phí

trả về -1; // trả về -1 nếu không có mbuf

mp->pid = đang chạy->pid; // chạy proc là người gửi

mp->ưu tiên = 1; // giả định mức độ ưu tiên CÙNG cho tất cả các tin nhắn

sao chép(mp->nội dung, tin nhắn); // sao chép tin nhắn vào mbuf

// gửi mbuf tới hàng đợi tin nhắn của Proc đích

P(proc[pid].mlock); // nhập CR

nhập mp vào PROC[pid].mqueue theo mức độ ưu tiên

V(proc[pid].lock); // thoát CR

V(proc[pid].message); // V semaphore tin nhắn của Proc mục tiêu

trả về 1; // trả về 1 cho THÀNH CÔNG

int a_recv(char *msg) // nhận tin nhắn từ mqueue của chính Proc

MBUF *mp;

P(đang chạy->mlock); // nhập CR

if (running->mqueue==0){ // kiểm tra mqueue của Proc

V(đang chạy->mlock); // nhả khóa CR

trả về -1;

mp = dequeue(running->mqueue); // xóa mbuf đầu tiên khỏi mqueue

V(đang chạy->mlock); // giải phóng mlock

sao chép(tin nhắn, mp->nội dung); // chép nội dung vào tin nhắn

int người gửi=mp->pid; // ID người gửi

put_mbuf(mp); // phát hành mbuf miễn phí

trả lại người gửi;

Các thuật toán trên hoạt động trong điều kiện bình thường. Tuy nhiên, nếu tất cả các tiến trình chỉ gửi mà không bao giờ nhận, hoặc một

Quá trình độc hại liên tục gửi tin nhắn, hệ thống có thể hết bộ đệm tin nhắn trống. Khi điều đó xảy ra,

cơ sở tin nhắn sẽ dừng lại vì không có quá trình nào có thể gửi được nữa. Một điều tốt về giao thức không đồng bộ

là không thể có bất kỳ bế tắc nào vì nó không bị chặn.

5.13.4.2 Truyền tin nhắn đồng bộ


Trong sơ đồ truyền tin nhắn đồng bộ, cả hoạt động gửi và nhận đều bị chặn. Quá trình gửi phải "chờ"

nếu không có mbuf miễn phí. Tương tự, quá trình nhận phải "chờ" nếu không có tin nhắn nào trong hàng đợi tin nhắn của nó. Nói chung,

giao tiếp đồng bộ hiệu quả hơn giao tiếp không đồng bộ. Nó rất phù hợp với các hệ thống liên kết chặt chẽ
Machine Translated by Google

5.13 Quá trình giao tiếp 149

trong đó các tiến trình trao đổi thông điệp một cách có kế hoạch hoặc thường xuyên. Trong một hệ thống như vậy, các tiến trình có thể mong đợi các thông điệp

đến khi cần thiết và việc sử dụng bộ đệm tin nhắn được lên kế hoạch cẩn thận. Vì vậy, các tiến trình có thể đợi

tin nhắn hoặc bộ đệm tin nhắn miễn phí thay vì dựa vào số lần thử lại. Để hỗ trợ truyền tin nhắn đồng bộ, chúng tôi xác định

các ngữ nghĩa bổ sung để đồng bộ hóa quy trình và thiết kế lại thuật toán gửi-nhận như sau.

SEMAPHORE nmbuf = NMBUF; // số lượng mbuf miễn phí

SEMAPHORE PROC.nmsg = 0; // cho Proc chờ tin nhắn

MBUF *get_mbuf() // trả về một con trỏ mbuf miễn phí

P(nmbuf); // chờ mbuf miễn phí

P(mlock);

MBUF *mp = dequeue(mbufList)

V(mlock);

trả lại mp;

int put_mbuf(MBUF *mp) // giải phóng mbuf đã sử dụng vào freembuflist

P(mlock);

enqueue(mbufList, mp);

V(mlock);

V(nmbuf);

int s_send(char *msg, int pid)// gửi tin nhắn đồng bộ tới pid đích

// xác thực pid mục tiêu, ví dụ: proc[pid] phải là một quy trình hợp lệ

MBUF *mp = get_mbuf(); // CHẶN: return mp phải hợp lệ

mp->pid = đang chạy->pid; // chạy proc là người gửi

sao chép(mp->nội dung, tin nhắn); // sao chép tin nhắn từ không gian người gửi sang mbuf

// gửi tin nhắn tới mqueue của Proc đích

P(proc[pid].mlock); // nhập CR

enqueue(proc[pid].mqueue, mp);

V(proc[pid].lock); // thoát CR

V(proc[pid].nmsg); // V semaphore nmsg của proc đích

int s_recv(char *msg) // nhận đồng bộ từ mqueue của chính Proc

P(đang chạy->nmsg); // chờ tin nhắn

P(đang chạy->mlock); // khóa PROC.mqueue

MBUF *mp = dequeue(running->mqueue); // nhận một tin nhắn

V(đang chạy->mlock); // giải phóng mlock

sao chép(mp->nội dung, tin nhắn); // sao chép nội dung vào Umode

put_mbuf(mp); // mbuf miễn phí

Thuật toán s_send/s_recv ở trên đúng về mặt đồng bộ hóa quy trình, nhưng có những vấn đề khác.

Bất cứ khi nào một giao thức chặn được sử dụng đều có khả năng xảy ra bế tắc. Thật vậy, thuật toán s_send/s_recv có thể dẫn đến

sau những tình huống bế tắc.

(1). Nếu các tiến trình chỉ gửi mà không nhận, tất cả các tiến trình cuối cùng sẽ bị chặn tại P(nmbuf) khi không còn
mbuf miễn phí.

(2). Nếu không có quá trình nào gửi nhưng tất cả đều cố gắng nhận, thì mọi quá trình sẽ bị chặn tại tín hiệu nmsg của chính nó.
Machine Translated by Google

150 5 Quản lý quy trình trong hệ thống nhúng

(3). Một tiến trình Pi gửi một thông điệp đến một tiến trình Pj khác và chờ đợi phản hồi từ Pj, điều này thực hiện hoàn toàn ngược

lại. Khi đó Pi và Pj sẽ đợi nhau, đó là tình trạng bế tắc chéo khóa quen thuộc.

Về cách xử lý bế tắc khi truyền tin nhắn, bạn đọc có thể tham khảo Chap. 6 của (Wang 2015), cũng

chứa một giao thức truyền tin nhắn dựa trên máy chủ-máy khách.

5.13.4.3 Trình diễn việc truyền tin nhắn Chương trình mẫu

C5.7 thể hiện việc truyền tin nhắn đồng bộ.

(1). Tệp Type.h: đã thêm loại MBUF

cấu trúc ngữ nghĩa{

giá trị int;

struct proc * hàng đợi;

};

typedef struct mbuf{

struct mbuf *next;

ưu tiên int; int pid;

nội dung char[128];

}MBUF;

quy trình cấu trúc typedef{

// giống như trước, nhưng được thêm vào

MBUF *mQueue;

cấu trúc semaphore mQlock;

cấu trúc semaphore nmsg;

int kstack[SSIZE];

}THỦ TỤC;

(2). Tệp tin nhắn.c

/********* tập tin tin nhắn.c *************/

#xác định NMBUF 10

struct semaphore nmbuf, mlock;

MBUF mbuf[NMBUF], *mbufList; // bộ đệm mbufs và mbufList

int menqueue(MBUF **queue, MBUF *p){// nhập p vào hàng đợi theo mức độ ưu tiên}

MBUF *mdequeue(MBUF **queue){// trả về phần tử hàng đợi đầu tiên}

int msg_init()

int tôi; MBUF *mp;

printf("mesg_init()\n");

mbufList = 0;

cho (i=0; i<NMBUF; i++) // khởi tạo mbufList

menqueue(&mbufList, &mbuf[i]); // tất cả mức độ ưu tiên=0, vì vậy hãy sử dụng menqueue()

nmbuf.value = NMBUF; nmbuf.queue = 0; // đếm semaphore mlock.queue = 0; // khóa semaphore

mlock.value = 1;

MBUF *get_mbuf() // cấp phát một mbuf

P(&nmbuf);

P(&mlock);

MBUF *mp = mdequeue(&mbufList);

V(&mlock);

trả lại mp;


Machine Translated by Google

5.13 Quá trình giao tiếp 151

int put_mbuf(MBUF *mp) // giải phóng mbuf

P(&mlock);

menqueue(&mbufList, mp);

V(&mlock);

V(&nmbuf);

int send(char *msg, int pid) // gửi tin nhắn tới partet pid

nếu (checkPid()<0) // xác thực nhận pid

trả về -1;

PROC *p = &proc[pid];

MBUF *mp = get_mbuf();

mp->pid = đang chạy->pid;

mp->ưu tiên = 1;

strcpy(mp->nội dung, tin nhắn);

P(&p->mQlock);

menqueue(&p->mQueue, mp);

V(&p->mQlock);

V(&p->nmseg);

trả về 0;

int recv(char *msg) // tin nhắn recv từ hàng đợi tin nhắn của chính mình

P(&running->nmsg);

P(&running->mQlock);

MBUF *mp = mdequeue(&running->mQueue);

V(&running->mQlock);

strcpy(tin nhắn, mp->nội dung);

int người gửi = mp->pid;

put_mbuf(mp);

trả lại người gửi;

(3). tập tin tc:

#include "type.h"

#include "tin nhắn.c"

int sender() // gửi mã tác vụ

struct uart *up = &uart[0];

dòng char[128];

trong khi(1){

ugets(lên, dòng);

printf("task%d got a line=%s\n", Running->pid, line);

gửi (dòng, 4);

printf("task%d gửi %s tới pid=4\n", Running->pid,line);

int người nhận() // mã tác vụ của người nhận

dòng char[128];

int pid;

trong khi(1){
Machine Translated by Google

152 5 Quản lý quy trình trong hệ thống nhúng

printf("task%d cố gắng nhận tin nhắn\n", đang chạy->pid);

pid = recv(dòng);

printf("tác vụ%d đã nhận: [%s] từ tác vụ%d\n",

đang chạy->pid, dòng, pid);

int chính()

tin nhắn_init();

kprintf("P0 nhiệm vụ kfork\n");

kfork((int)sender, 1); // quá trình gửi

bộ thu kfork((int), 1); // quá trình nhận

while(1){ if

(readyQueue) tswitch();

Hình 5.12 cho thấy kết quả đầu ra mẫu khi chạy Chương trình C5.7.

5.14 Hạt nhân hệ thống nhúng bộ xử lý đơn (UP)

Hạt nhân hệ thống nhúng bao gồm các tiến trình động, tất cả đều thực thi trong cùng một không gian địa chỉ của hạt nhân. Hạt nhân cung cấp các

chức năng để quản lý quy trình, chẳng hạn như tạo quy trình, đồng bộ hóa, giao tiếp và kết thúc. Trong phần này, chúng tôi sẽ trình bày cách

thiết kế và triển khai các hạt nhân hệ thống nhúng bộ xử lý đơn (UP). Có hai loại hạt nhân riêng biệt; không có tính ưu tiên và có tính ưu tiên.

Trong hạt nhân không có quyền ưu tiên, mỗi tiến trình sẽ chạy cho đến khi nó tự nguyện từ bỏ CPU. Trong hạt nhân ưu tiên, một tiến trình đang

chạy có thể được ưu tiên theo mức độ ưu tiên hoặc theo lát cắt thời gian.

5.14.1 Hạt nhân UP không ưu tiên

Hạt nhân Uniprocessor (UP) không có quyền ưu tiên nếu mỗi tiến trình chạy cho đến khi nó tự nguyện từ bỏ CPU. Trong khi một tiến trình đang chạy,

nó có thể được chuyển hướng để xử lý các ngắt, nhưng việc điều khiển luôn quay trở lại điểm gián đoạn trong cùng một tiến trình khi kết thúc quá

trình xử lý ngắt. Điều này ngụ ý rằng trong kernel UP không được ưu tiên chỉ có một tiến trình chạy tại một thời điểm. Do đó, không cần thiết

phải bảo vệ các đối tượng dữ liệu trong kernel khỏi việc thực thi đồng thời của các tiến trình. Tuy nhiên, trong khi một tiến trình đang chạy,

nó có thể bị chuyển hướng sang thực thi một trình xử lý ngắt, điều này có thể gây trở ngại cho tiến trình nếu cả hai đều cố gắng truy cập vào

cùng một đối tượng dữ liệu. Để ngăn chặn sự can thiệp từ các trình xử lý ngắt, việc vô hiệu hóa các ngắt khi một tiến trình thực thi một đoạn mã

quan trọng là đủ. Điều này đơn giản hóa việc thiết kế hệ thống.

Chương trình ví dụ C5.8 trình bày cách thiết kế và triển khai hạt nhân không được ưu tiên cho các hệ thống nhúng bộ xử lý đơn. Chúng tôi giả

định rằng phần cứng hệ thống bao gồm hai bộ định thời, có thể được lập trình để tạo ra các ngắt hẹn giờ với các tần số khác nhau, UART, bàn phím

và màn hình LCD. Phần mềm hệ thống bao gồm một tập hợp các quy trình đồng thời, tất cả đều thực thi trong cùng một không gian địa chỉ nhưng có

mức độ ưu tiên khác nhau. Lập kế hoạch quy trình là theo mức độ ưu tiên không được ưu tiên. Mỗi tiến trình chạy cho đến khi nó chuyển sang chế

độ ngủ, tự chặn hoặc chấm dứt. Hẹn giờ0 duy trì thời gian trong ngày và hiển thị đồng hồ treo tường trên màn hình LCD. Do nhiệm vụ hiển thị đồng

hồ treo tường ngắn nên nó được thực hiện trực tiếp bởi bộ xử lý ngắt clock0. Hai quy trình định kỳ, time_task1 và time_task2, mỗi quy trình gọi

hàm tạm dừng (t) để tự tạm dừng trong một số giây. Sau khi đăng ký thời gian tạm dừng trong cấu trúc PROC, quá trình sẽ thay đổi trạng thái thành

PAUSE, tự đưa vào danh sách tạm dừng và từ bỏ CPU. Trên mỗi giây, Hẹn giờ 2 giảm thời gian tạm dừng của mọi quy trình trong Danh sách tạm dừng

xuống 1. Khi thời gian về 0, nó làm cho quy trình bị tạm dừng sẵn sàng chạy lại. Mặc dù điều này có thể được thực hiện bằng cơ chế ngủ-thức,

nhưng nó nhằm mục đích chứng tỏ rằng các nhiệm vụ định kỳ có thể được thực hiện trong cơ thể.
Machine Translated by Google

5.14 Hạt nhân hệ thống nhúng bộ xử lý đơn (UP) 153

Hình 5.12 Minh họa việc truyền tin nhắn

khuôn khổ chung của các chức năng dịch vụ hẹn giờ. Ngoài ra, hệ thống còn hỗ trợ hai bộ quy trình hợp tác, thực hiện bài toán
nhà sản xuất-người tiêu dùng để thể hiện sự đồng bộ hóa quy trình bằng cách sử dụng các ẩn dụ. Mỗi quy trình sản xuất đều cố
gắng lấy dòng đầu vào từ UART0. Sau đó, nó gửi các ký tự vào bộ đệm dùng chung, char pcbuffer[N] có kích thước N byte. Mỗi quy
trình tiêu dùng cố gắng lấy một char từ pcbuff[N] và hiển thị nó trên màn hình LCD. Các quy trình của nhà sản xuất và người tiêu
dùng chia sẻ bộ đệm dữ liệu chung dưới dạng một đường ống. Để ngắn gọn, chúng tôi chỉ hiển thị các đoạn mã có liên quan của hệ thống.

(1). /********* tập tin time.c của C5.8 **********/

PROC *danh sách tạm dừng; // danh sách các tiến trình đang tạm dừng

bộ đếm thời gian cấu trúc typedef {

u32 *cơ sở; // địa chỉ cơ sở của bộ định thời;

int tích tắc, hh, mm, ss; // mỗi vùng dữ liệu hẹn giờ

đồng hồ char[16];

}Đồng hồ hẹn giờ; Hẹn giờ hẹn giờ [4]; // 4 bộ định thời;

làm trống bộ đếm thời gian_init()

printf("timer_init()\n");

danh sách tạm dừng = 0; // danh sách tạm dừng ban đầu 0

// khởi tạo cả 4 bộ định thời

void time_handler(int n) // n=đơn vị hẹn giờ

int tôi; THỜI GIAN *t = &timer[n];

t->đánh dấu++; t->ss = t->tích;

nếu (t->ss == 60){

t->ss=0; tp->mm++;

nếu (t->mm == 60){

t->mm = 0; t->hh++;

}
Machine Translated by Google

154 5 Quản lý quy trình trong hệ thống nhúng

if (n==0){ // time0: hiển thị trực tiếp đồng hồ treo tường

cho (i=0; i<8; i++) // xóa vùng đồng hồ cũ

unkpchar(t->clock[i], n, 70+i);

t->clock[7]='0'+(t->ss%10); t->clock[6]='0'+(t->ss/10);

t->clock[4]='0'+(t->mm%10); t->đồng hồ[3]='0'+(t->mm/10);

t->clock[1]='0'+(t->hh%10); t->clock[0]='0'+(t->hh/10);

cho (i=0; i<8; i++) // hiển thị đồng hồ treo tường mới

kpchar(t->clock[i], n, 70+i);

if (n==2){//timer2: xử lý các PROC bị TẠM DỪNG trong danh sách tạm dừng

PROC *p, *tempList = 0;

trong khi ( p = dequeue(&pauseList) ){

p->tạm dừng--;

if (p->pause == 0){ // hết thời gian tạm dừng

p->trạng thái = SẴN SÀNG;

enqueue(&readyQueue, p);

khác

enqueue(&tempList, p);

tạm dừngList = tempList; // danh sách tạm dừng được cập nhật

hẹn giờ_clearInterrupt(n);

int time_start(int n)

THỜI GIAN *tp = &timer[n];

kprintf("timer_start %d base=%x\n", n, tp->base); *(tp->base+TCNTL) |= 0x80; //

đặt bit kích hoạt 7

int time_clearInterrupt(int n) {

THỜI GIAN *tp = &timer[n]; *(tp-

>base+TINTCLR) = 0xFFFFFFFF;

typedef cấu trúc uart{

char *cơ sở; // địa chỉ cơ sở; như ký tự *

id u32; // số uart 0-3

char inbuf[BUFSIZE];

int đầu, chi tiết;

struct semaphore indata, uline;

char outbuf[BUFSIZE];

int outhead, outtail;

cấu trúc phòng ngoài semaphore;

int txon; // 1=Ngắt TX đang bật

}UART; UART uart[4]; // 4 cấu trúc UART

int uart_init()

int tôi; UART *lên; for

(i=0; i<4; i++){ // chỉ sử dụng UART0

lên = &uart[i]; up-

>base = (char *)(0x101f1000 + i*0x1000); *(lên->cơ sở+0x2C) &=

*0x10; // vô hiệu hóa FIFO *(up->base+0x38) |= 0x30;

lên->id = i;
Machine Translated by Google

5.14 Hạt nhân hệ thống nhúng bộ xử lý đơn (UP) 155

lên->inhead = up->intail = 0;

lên->outhead = up->outtail = 0;

lên->txon = 0;

lên->indata.value = 0; lên->indata.queue = 0; lên->uline.value =

0; lên->uline.queue = 0;

up->outroom.value = BUFSIZE; lên->outroom.queue = 0;

void uart_handler(UART *up)

u8 mis = *(up->base + MIS); // đọc thanh ghi MIS if (mis & 0x10) do_rx(up);

if (mis & 0x20) do_tx(up);

int do_rx(UART *up) // Trình xử lý ngắt UART rx

ký tự c;

c = *(up->base+UDR);

up->inbuf[up->inhead++] = c;

lên->đầu vào %= BUFSIZE;

V(&up->indata);

nếu (c=='\r'){

V(&up->uline);

int do_tx(UART *up) // Trình xử lý ngắt UART tx

ký tự c; mặt nạ u8;

if (up->outroom.value >= SBUFSIZE){ // outbuf[ ] trống

// vô hiệu hóa ngắt TX; trả về *(up->base+IMSC)

= 0x10; lên->txon = 0; // che dấu ngắt TX

trở lại;

c = up->outbuf[up->outtail++];

lên->outtail %= BUFSSIZE;

*(up->base + UDR) = (int)c;

V(&up->outroom);

int ugetc(UART *up)

ký tự c;

P(&up->indata);

kho a();

c = up->inbuf[up->intail++];

up->intail %= BUFSIZE;

mở khóa();

trả lại c;

int uget(UART *up, char *s)

while ((*s = (char)ugec(up)) != '\r'){

uputc(up, *s);

s++;

}
Machine Translated by Google

156 5 Quản lý quy trình trong hệ thống nhúng

*s = 0;

int uputc(UART *up, char c)

if (up->txon){ // nếu TX được bật => nhập c vào outbuf[]

P(&up->outroom); //đợi phòng

kho a();

up->outbuf[up->outhead++] = c; lên->đầu ra %=

128;

mở khóa();

trở lại;

int i = *(up->base+UFR); // đọc FR

while( *(up->base+UFR) & 0x20 ); // lặp trong khi FR=TXF

*(up->base + UDR) = (int)c; // ghi c vào DR

*(lên->cơ sở+IMSC) |= 0x30;

lên->txon = 1;

(3). /****************** tệp tc của C5.8******************/

#include "type.h" #include

"string.c"

#include "queue.c"

#include "pv.c"

#include "vid.c"

#include "kbd.c"

#include "uart.c"

#include "timer.c"

#include "ngoại lệ.c"

#include "kernel.c"

// IRQ ngắt điểm vào của trình xử lý

khoảng trống irq_chandler()

int vicstatus, sicstatus;

// đọc các thanh ghi trạng thái VIC SIV để tìm ra ngắt nào

nạn nhân = VIC_STATUS;

sicstatus = SIC_STATUS;

nếu (vicstatus & (1<<4)) // bit4: bộ đếm thời gian0

bộ đếm thời gian_handler(0);

nếu (vicstatus & (1<<5)) // bit5: bộ đếm thời gian2

bộ đếm thời gian_handler(2);

if (vicstatus & (1<<12)) // Bit12: uart0

uart_handler(&uart[0]);

if (vicstatus & (1<<13)) // bit13: uart1

uart_handler(&uart[1]);

if (vicstatus & (1<<31)){ // SIC ngắt=bit_31=>KBD ở bit 3

nếu (sicstatus & (1<<3))

kbd_handler();

int tạm dừng(int t)

kho a(); // vô hiệu hóa các ngắt IRQ

đang chạy->tạm dừng = t;

đang chạy->trạng thái = TẠM DỪNG;


Machine Translated by Google

5.14 Hạt nhân hệ thống nhúng bộ xử lý đơn (UP) 157

enqueue(&pauseList, đang chạy);

tswitch(); mở

khóa(); // kích hoạt ngắt IRQ

int hẹn giờ1_task()

int t = 5;

printf("timer1_task %d start\n", Running->pid);

trong khi(1){

tạm dừng(t);

printf("Proc%d chạy một lần trong %d giây\n", Running->pid, t);

int hẹn giờ2_task()

int t = 7;

printf("timer2_task %d start\n", Running->pid);

trong khi(1){

tạm dừng(t);

printf("Proc%d chạy một lần trong %d giây\n", Running->pid, t);

/****** Bộ đệm CHIA SẺ của Nhà sản xuất và Người tiêu dùng *****/

#define PRSIZE 16

char pcbuf[PRSIZE];

int đầu, đuôi;

cấu trúc semaphore đầy đủ, trống rỗng, mutex;

nhà sản xuất int()

ký tự c, *cp;

struct uart *up = &uart[0];

dòng char[128];

trong khi(1){

ugets(lên, dòng); cp =

dòng; trong

khi(*cp){

printf("Nhà sản xuất %d P(empty=%d)\n", Running->pid, Empty.value);

P(&trống);

P(&mutex);

pcbuf[head++] = *cp++;

đầu %= PRSIZE;

V(&mutex);

printf("Nhà sản xuất %d V(full=%d)\n", Running->pid, full.value);

// hiển thị đầy đủ.queue

V(&đầy đủ);

người tiêu dùng int()

ký tự c;

trong khi(1){

printf("Người tiêu dùng %d P(full=%d)\n", Running->pid, full.value);

P(&đầy đủ);
Machine Translated by Google

158 5 Quản lý quy trình trong hệ thống nhúng

P(&mutex);

c = pcbuf[tail++];

đuôi %= PRSIZE;

V(&mutex);

printf("Người tiêu dùng %d V(empty=%d) ", Running->pid, Empty.value);

// hiển thị trống.queue

V(&trống);

int chính()

fbuf_init();

uart_init();

kbd_init();

printf("Chào mừng đến với Wanix trong ARM\n");

// Định cấu hình VIC cho các ngắt IRQ: giống như trước

kernel_init();

bộ đếm thời gian_init();

bộ đếm thời gian_start(0);

bộ đếm thời gian_start(2);

/*khởi tạo bộ đệm dữ liệu cho nhà sản xuất-người tiêu dùng */

đầu = đuôi = 0;

đầy đủ.value = 0; đầy đủ.queue = 0;

trống.value = PRSIZE; trống.queue = 0;

mutex.value = 1; mutex.queue = 0;

printf("P0 kfork task\n");

kfork((int)timer1_task, 1);

kfork((int)timer2_task, 1); nhà sản xuất

kfork((int), 2);

người tiêu dùng kfork((int), 2);

nhà sản xuất kfork((int), 3);

người tiêu dùng kfork((int), 3);

printQ(readyQueue);

trong khi(1){

nếu (sẵn sàng)

tswitch();

5.14.2 Trình diễn hạt nhân UP không ưu tiên

Hình 5.13 cho thấy kết quả đầu ra khi chạy chương trình C5.8, thể hiện hạt nhân UP không được ưu tiên.

5.14.3 Hạt nhân UP ưu tiên

Trong hạt nhân UP ưu tiên, trong khi một tiến trình đang chạy, CPU có thể được lấy ra khỏi nó để chạy một tiến trình khác. Quyền ưu tiên

xử lý được kích hoạt bởi các sự kiện yêu cầu sắp xếp lại quy trình. Ví dụ: khi một quy trình có mức độ ưu tiên cao hơn sẵn sàng để chạy

hoặc nếu sử dụng lập lịch quy trình theo thời gian, khi một quy trình đã cạn kiệt lượng thời gian của nó. Chính sách ưu tiên có thể là

hạn chế hoặc không hạn chế (hoàn toàn ưu tiên). Trong quyền ưu tiên hạn chế, trong khi một tiến trình đang thực thi một đoạn mã quan

trọng mà các tiến trình khác không thể can thiệp, kernel có thể vô hiệu hóa các ngắt hoặc bộ lập lịch tiến trình để ngăn chặn việc chuyển

đổi tiến trình, do đó trì hoãn quyền ưu tiên của tiến trình cho đến khi an toàn để thực hiện điều đó. Trong quyền ưu tiên không hạn chế, quy trình
Machine Translated by Google

5.14 Hạt nhân hệ thống nhúng bộ xử lý đơn (UP) 159

Hình 5.13 Trình diễn kernel UP không ưu tiên

switch diễn ra ngay lập tức, bất kể tiến trình đang chạy hiện tại đang làm gì. Điều này ngụ ý rằng, trong kernel UP hoàn toàn có quyền

ưu tiên, các tiến trình chạy song song một cách hợp lý. Do đó, tất cả các cấu trúc dữ liệu kernel dùng chung phải được bảo vệ dưới dạng

các vùng quan trọng. Điều này làm cho hạt nhân UP có quyền ưu tiên hoàn toàn tương đương về mặt logic với hạt nhân MP vì cả hai đều phải

hỗ trợ thực thi đồng thời nhiều tiến trình. Sự khác biệt duy nhất giữa hạt nhân MP và hạt nhân UP được ưu tiên hoàn toàn là ở chỗ, trong

khi các tiến trình trước đây có thể chạy song song trên các CPU khác nhau, thì các tiến trình trong MP sau chỉ có thể chạy đồng thời trên

cùng một CPU, nhưng hành vi logic của chúng giống nhau. Sau đây, chúng ta sẽ chỉ xem xét các hạt nhân UP có quyền ưu tiên hoàn toàn. Hạt

nhân MP sẽ được đề cập trong Chap. 9 trên các hệ thống đa bộ xử lý.

Chúng tôi minh họa việc thiết kế và triển khai hạt nhân UP có quyền ưu tiên hoàn toàn bằng một ví dụ. Các thành phần phần cứng của hệ

thống giống như trong chương trình mẫu C5.8. Phần mềm hệ thống bao gồm một tập hợp các tiến trình đồng thời với các mức độ ưu tiên khác

nhau, tất cả đều thực thi trong cùng một địa chỉ của kernel. Chính sách lập lịch trình được thực hiện theo mức độ ưu tiên hoàn toàn. Để

hỗ trợ quyền ưu tiên hoàn toàn, trước tiên chúng tôi xác định cấu trúc dữ liệu dùng chung trong kernel phải được bảo vệ.
Bao gồm các

(1). PROC *freeList: được sử dụng để tạo và chấm dứt tác vụ động.

(2). PROC *readyQueue: được sử dụng để lập lịch trình quy trình.

(3). PROC *sleepList, *pauseList: được sử dụng cho các hoạt động ngủ/thức dậy.

Đối với mỗi cấu trúc dữ liệu hạt nhân được chia sẻ, chúng tôi triển khai các chức năng truy cập của nó dưới dạng các vùng quan trọng, mỗi cấu trúc được bảo vệ bởi một mutex

kho a. Ngoài ra, chúng tôi xác định các biến toàn cục sau để kiểm soát quyền ưu tiên của quy trình.

(4). int swflag: chuyển cờ tiến trình, bị xóa thành 0 khi một tiến trình được lên lịch chạy, được đặt thành 1 bất cứ khi nào xảy ra sự

kiện lên lịch lại, ví dụ như khi một tiến trình sẵn sàng được thêm vào

hàng đợi sẵn sàng. (5). int intnest: IRQ ngắt bộ đếm lồng nhau, ban đầu là 0, tăng 1 khi nhập trình xử lý IRQ, giảm 1 khi thoát khỏi

trình xử lý IRQ. Chúng tôi giả sử các ngắt IRQ được xử lý trực tiếp trong các trình xử lý IRQ, tức là không phải bằng các tác vụ xử lý

ngắt giả. Chuyển đổi quy trình chỉ có thể xảy ra khi kết thúc quá trình xử lý ngắt. Đối với các ngắt lồng nhau, quá trình chuyển đổi được

trì hoãn cho đến khi kết thúc tất cả quá trình xử lý các ngắt lồng nhau.
Machine Translated by Google

160 5 Quản lý quy trình trong hệ thống nhúng

Hạt nhân UP được ưu tiên hoàn toàn bao gồm các thành phần sau.

(1). tập tin ts.s của C5.9:

/*************** tệp ts.s của Chương trình C5.9 ****************/

.set vectorAddr, 0x10140030

reset_handler: // GIỐNG NHƯ TRƯỚC

irq_handler:

phụ lr, lr, #4 // điều chỉnh lr

stmfd sp!, {r0-r12, lr} // lưu ngữ cảnh trong ngăn xếp chế độ IRQ

bà r12, spsr stmfd sp!, // sao chép spsr vào r12

{r12} msr cpsr, #0x93 // lưu spsr vào ngăn xếp IRQ

// sang chế độ SVC

ldr r1, =vectorAddr // đọc vectorAddr tới ngắt ACK

ldr r0, [r1]

msr cpsr, #0x13 // Chế độ SVC có bật IRQ

bl irq_chandler // xử lý IRQ ở chế độ SVC: trả về 1 cho tswitch

msr cpsr, #0x92

ldr r1, =vectorAddr // phát hành EOI

str r0, [r1]

cmp r0, #0

bgt do_switch

ldmfd sp!, {r12} // lấy spsr

msr spsr, r12 // khôi phục spsr

ldmfd sp!, {r0-r12, pc}^

do_switch:

msr cpsr, #0x93

bl tswitch // chuyển tác vụ ở chế độ SVC

msr cpsr, #0x92

ldmfd sp!, {r12} // lấy spsr

msr spsr, r12 // khôi phục spsr

ldmfd sp!, {r0-r12, pc}^

Giải thích về tệp ts.s của Chương trình C5.9:

Reset_handler: Như thường lệ, reset_handler là điểm vào. Nó đặt con trỏ ngăn xếp chế độ SVC lên đầu cao của proc[0] và

sao chép bảng vectơ tới địa chỉ 0. Tiếp theo, nó chuyển sang chế độ IRQ để đặt con trỏ ngăn xếp chế độ IRQ. Sau đó nó gọi main() trong

Chế độ SVC Trong quá trình vận hành hệ thống, tất cả các tiến trình đều chạy ở chế độ SVC trong cùng một địa chỉ của kernel.

Irq_handler: Chuyển đổi quy trình thường được kích hoạt bởi các ngắt, có thể đánh thức các quy trình đang ngủ, khiến một quy trình bị chặn

quá trình đã sẵn sàng để chạy, v.v. Do đó, irq_handler là đoạn mã lắp ráp quan trọng nhất có liên quan đến quyền ưu tiên xử lý. Vì thế

chúng tôi chỉ hiển thị mã irq_handler. Như đã chỉ ra ở Chap. 3, CPU ARM không thể xử lý các ngắt lồng nhau ở chế độ IRQ.

Để xử lý các ngắt lồng nhau, việc xử lý ngắt phải được thực hiện ở chế độ đặc quyền khác. Để hỗ trợ quá trình

ưu tiên do bị gián đoạn, chúng tôi chọn xử lý các ngắt IRQ ở chế độ SVC. Bạn đọc có thể tham khảo Chap. 3 để biết cách

xử lý các ngắt lồng nhau. Mã irq_handler tương tác với irq_chnadler() trong C để hỗ trợ quyền ưu tiên.

int irq_chandler()

{
void *(*f)(); // f là con trỏ hàm

intnest++; f // ngắt bộ đếm tổ ++

=(void *)*((int *)(VIC_BASE_ADDR+0x30)); // đọc địa chỉ ISR

(*f)(); // gọi hàm ISR

intnest--; // ngắt bộ đếm tổ--

if (intnest==0 && swflag){ // nếu phần cuối của IRQ lồng nhau & swflag được đặt

swflag = 0;
Machine Translated by Google

5.14 Hạt nhân hệ thống nhúng bộ xử lý đơn (UP) 161

trả về 1; // trả về 1 để chuyển tác vụ

trả về 0; // trả về 0 nếu không có chuyển đổi tác vụ

Thuật toán của trình xử lý IRQ lồng nhau để giành quyền ưu tiên hoàn toàn như sau.

/********* Thuật toán xử lý IRQ để giành quyền ưu tiên hoàn toàn **********/

(1). Khi vào, điều chỉnh return lr; lưu các thanh ghi làm việc, lr và spsr trong ngăn xếp IRQ

(2). Chuyển sang chế độ SVC, đọc bộ điều khiển ngắt vector thành ngắt ACK (3). Gọi

irq_chandler() ở chế độ SVC có bật ngắt IRQ

/*************** irq_chandler() trong C *******************/

(4). Tăng bộ đếm lồng nhau ngắt lên 1; (5). Gọi ISR ở

chế độ SVC; ISR có thể đặt cờ tác vụ chuyển đổi bằng V()

(6). Giảm bộ đếm lồng ghép ngắt đi 1 (7). Trả về 1 để

chuyển đổi tác vụ nếu kết thúc IRQ và swflag được đặt, nếu không thì chạy lại 0

/*************** quay lại irq_handler trong tập hợp *******/

(số 8). Phát hành EOI cho ngắt

(9). Nếu irq_chandler() trả về 0: trả về bình thường; nếu không thì chuyển tác vụ ở chế độ

SVC (10). Khi tác vụ đã tắt tiếp tục, hãy quay lại qua ngăn xếp IRQ

(2). Các hàm kernel đã được sửa đổi để giành quyền ưu tiên

Để ngắn gọn, chúng tôi chỉ hiển thị các hàm kernel đã sửa đổi để hỗ trợ quyền ưu tiên xử lý.

Chúng bao gồm kwakeup, V trên semaphore và mutex_unlock, tất cả đều có thể làm cho quá trình ngủ hoặc bị chặn sẵn sàng chạy và thay đổi hàng

đợi sẵn sàng. Ngoài ra, kfork cũng có thể tạo một quy trình mới có mức độ ưu tiên cao hơn quy trình đang chạy hiện tại. Tất cả các hàm này đều

gọi reschedule(), có thể chuyển đổi tiến trình ngay lập tức hoặc trì hoãn việc chuyển đổi tiến trình cho đến khi kết thúc quá trình xử lý ngắt

IRQ.

int sắp xếp lại()

int SR = int_off();

if (readyQueue &&readyQueue->priority >= đang chạy->ưu tiên){

if (intnest==0){ // không có trong trình xử lý IRQ: ưu tiên ngay lập tức

printf("%d PREEMPT %d NOW\n",readyQueue->pid, Running->pid);

tswitch();

khác{ // vẫn còn trong trình xử lý IRQ: trì hoãn quyền ưu tiên

printf("%d TRÌ HOÃN TRƯỚC %d\n", ReadyQueue->pid, Running->pid);

swflag = 1; // đặt nhu cầu chuyển cờ nhiệm vụ

int_on(SR);

int kwakeup(int sự kiện)

PROC *p, *tmp=0;

int SR = int_off(); while((p

= dequeue(&sleepList))!=0){ if (p->event==event){

p->trạng thái = SẴN SÀNG;


Machine Translated by Google

162 5 Quản lý quy trình trong hệ thống nhúng

enqueue(&readyQueue, p);

khác{

enqueue(&tmp, p);

danh sách ngủ = tmp;

sắp xếp lại(); // nếu đánh thức bất kỳ tiến trình nào có mức độ ưu tiên cao hơn

int_on(SR);

int V(struct semaphore *s)

PROC *p; int cpsr;

int SR = int_off();

s->giá trị++;

nếu (s->giá trị <= 0){

p = dequeue(&s->queue);

p->trạng thái = SẴN SÀNG;

enqueue(&readyQueue, p);

printf("timer: V up task%d pri=%d; đang chạy pri=%d\n",

p->pid, p->ưu tiên, chạy->ưu tiên);

sắp xếp lại();

int_on(SR);

int mutex_unlock(MUTEX *s)

PROC *p;

int SR = int_off();

printf("task%d đang mở khóa mutex\n", đang chạy->pid); if (s->lock==0 || s-

>owner != đang chạy){ // lỗi mở khóa

int_on(SR); trả về -1;

// mutex bị khóa và tác vụ đang chạy là chủ sở hữu if (s->queue ==

0){ // mutex không có người phục vụ

s->khóa = 0; // xóa khóa

s->chủ sở hữu = 0; // xóa chủ sở hữu

else{ // mutex có người phục vụ: bỏ chặn một người với tư cách là chủ sở hữu mới

p = dequeue(&s->queue);

p->trạng thái = SẴN SÀNG;

s->chủ sở hữu = p;

printf("%d mutex_unlock: new owner=%d\n", Running->pid,p->pid);

enqueue(&readyQueue, p);

sắp xếp lại();

int_on(SR);

trả về 0;

int kfork(int func, int ưu tiên)

// tạo tác vụ mới với mức độ ưu tiên như trước

mutex_lock(&readyQueuelock);

enqueue(&readyQueue, p);

mutex_unlock(&readyQueuelock);
Machine Translated by Google

5.14 Hạt nhân hệ thống nhúng bộ xử lý đơn (UP) 163

sắp xếp lại();

trả về p-pid;

(3). tập tin tc:

/****************** file tc của Chương trình C5.9***************/

#include "type.h"

MUTEX *mp; // mutex toàn cầu

cấu trúc ngữ nghĩa s1; // ngữ nghĩa toàn cục

#include "queue.c" #include

"pv.c" // P/V trên các ngữ nghĩa

#include "mutex.c" // hàm mutex

#include "kbd.c"

#include "uart.c"

#include "vid.c"

#include "ngoại lệ.c"

#include "kernel.c"

#include "timer.c" // trình điều khiển hẹn giờ

int copy_vectors(){ // giống như trước }

int irq_chandler(){ // hiển thị ở trên }

nhiệm vụ int3()

printf("PROC%d đang chạy: ", Running->pid);

mutex_lock(mp);

printf("PROC%d bên trong CR\n", đang chạy->pid);

mutex_unlock(mp);

printf("PROC%d exit\n", Running->pid);

kexit();

nhiệm vụ int2()

printf("PROC%d đang chạy\n", đang chạy->pid); kfork((int)task3,

3); // tạo P3 với mức độ ưu tiên=3 printf("PROC%d exit:", Running->pid);

kexit();

nhiệm vụ int1()

printf("Proc%d start\n", Running->pid);

trong khi(1){

P(&s1); // task1 được cập nhật theo định kỳ bởi bộ đếm thời gian

mutex_lock(mp);

kfork((int)task2, 2); // tạo P2 với mức độ ưu tiên=2

printf("Proc%d bên trong CR\n", đang chạy->pid);

mutex_unlock(mp);

printf("proc%d đã hoàn thành vòng lặp\n", đang chạy->pid);

int chính()

fbuf_init(); // Trình điều khiển LCD

uart_init(); // trình điều khiển UART

kbd_init(); // trình điều khiển KBD


Machine Translated by Google

164 5 Quản lý quy trình trong hệ thống nhúng

kprintf("Chào mừng đến với Wanix trong ARM\n");

// cấu hình VIC cho các ngắt có vector định thời_init();

bộ đếm thời

gian_start(0); bộ đếm //timer0: đồng hồ treo tường

thời gian_start(2); //timer2: sự kiện định thời gian

mp = mutex_create(); // tạo mutex toàn cục

s1.giá trị = 0; // khởi tạo semaphore s1

s1.queue = (PROC*)0;

kernel_init(); // khởi tạo kernel và chạy P0

kfork((int)task1, 1); // tạo P1 với mức độ ưu tiên=1

trong khi(1){ // Vòng lặp P0

nếu (sẵn sàng)

tswitch();

5.14.4 Trình diễn hạt nhân UP ưu tiên

Hệ thống mẫu C5.9 thể hiện khả năng lập lịch trình quy trình hoàn toàn ưu tiên. Khi hệ thống khởi động, nó tạo và chạy quy trình ban đầu P0, quy

trình này có mức ưu tiên thấp nhất là 0. P0 tạo quy trình P1 mới với mức ưu tiên = 1. Vì P1 có mức ưu tiên cao hơn P0 nên nó ngay lập tức chiếm ưu

thế trước P0, điều này thể hiện sự ưu tiên trực tiếp mà không có bất kỳ độ trễ nào. Khi P1 chạy trong task1(), đầu tiên nó chờ một sự kiện hẹn giờ

bằng P(s1=0), sự kiện này được bộ hẹn giờ Ved up định kỳ (4 giây một lần). Trong khi P1 chờ semaphore, P0 tiếp tục chạy. Khi bộ xử lý ngắt bộ định

thời V lên P1, nó sẽ cố gắng ưu tiên P0 bằng P1. Vì việc chuyển đổi tác vụ không được phép bên trong trình xử lý ngắt, nên quyền ưu tiên bị trì

hoãn, điều này chứng tỏ việc ưu tiên có thể bị trì hoãn do quá trình xử lý ngắt. Ngay khi quá trình xử lý ngắt kết thúc, P1 sẽ ưu tiên P0 chạy lại.

Để minh họa quyền ưu tiên của quá trình do bị chặn, trước tiên P1 sẽ khóa mutex mp. Trong khi giữ khóa mutex, P1 tạo ra một quy trình P2 với mức

độ ưu tiên cao hơn=2, ngay lập tức ưu tiên P1. Chúng tôi giả định rằng P2 không cần mutex. Nó tạo ra một tiến trình P3 với mức ưu tiên cao hơn=3,

ngay lập tức ưu tiên P2. Trong mã task3(), P3 cố gắng khóa cùng một mp mutex mà P1 vẫn giữ. Do đó, P3 bị chặn trên mutex và chuyển sang chạy P2. Khi

P2 kết thúc, nó gọi kexit() để kết thúc, khiến P1 tiếp tục chạy. Khi P1 mở khóa mutex, nó sẽ mở khóa P3, P3 có mức độ ưu tiên cao hơn P1, do đó, nó

ngay lập tức chiếm ưu thế trước P1. Sau khi P3 kết thúc, P1 tiếp tục chạy lại và chu trình lặp lại. Hình 5.14 thể hiện kết quả mẫu khi chạy chương

trình C5.9.

Cần lưu ý rằng, trong một hệ thống có mức độ ưu tiên nghiêm ngặt, quy trình đang chạy hiện tại phải luôn là quy trình có mức độ ưu tiên cao nhất.

Tuy nhiên, trong hệ thống mẫu C5.9, khi quy trình P3, có mức ưu tiên cao nhất, cố gắng khóa mutex đã được P1 giữ, có mức ưu tiên thấp hơn, thì nó

sẽ bị chặn trên mutex và chuyển sang chạy quá trình có thể chạy tiếp theo. Trong hệ thống mẫu, chúng tôi giả định rằng quy trình P2 không cần mutex,

vì vậy nó sẽ trở thành quy trình đang chạy khi P3 bị chặn trên mutex. Trong trường hợp này, hệ thống đang chạy P2, không có mức ưu tiên cao nhất.

Điều này vi phạm nguyên tắc ưu tiên nghiêm ngặt, dẫn đến cái được gọi là đảo ngược mức độ ưu tiên (Lampson và Redell 1980), trong đó quy trình có

mức độ ưu tiên thấp có thể chặn quy trình có mức độ ưu tiên cao hơn. Nếu P2 tiếp tục chạy hoặc nó chuyển sang một quy trình khác có cùng mức ưu tiên

hoặc thấp hơn, quy trình P3 sẽ bị chặn trong một khoảng thời gian không xác định, dẫn đến đảo ngược mức ưu tiên không giới hạn. Trong khi việc đảo

ngược mức độ ưu tiên đơn giản có thể được coi là tự nhiên bất cứ khi nào các quy trình được phép cạnh tranh để giành quyền kiểm soát độc quyền các

tài nguyên, thì việc đảo ngược mức độ ưu tiên không giới hạn có thể gây bất lợi cho các hệ thống có yêu cầu về thời gian tuần hoàn. Chương trình mẫu

C5.9 thực sự triển khai một sơ đồ được gọi là kế thừa ưu tiên, giúp ngăn chặn việc đảo ngược mức ưu tiên không giới hạn. Chúng ta sẽ thảo luận về

việc đảo ngược mức độ ưu tiên chi tiết hơn ở phần sau của Chương. 10 trên các hệ thống thời gian thực.
Machine Translated by Google

5.15 Tóm tắt 165

Hình 5.14 Trình diễn quyền ưu tiên của quy trình

5.15 Tóm tắt

Chương này đề cập đến quản lý quy trình. Nó giới thiệu khái niệm quy trình, nguyên tắc và kỹ thuật đa nhiệm bằng cách chuyển đổi ngữ

cảnh. Nó cho thấy cách tạo các quy trình một cách linh hoạt và thảo luận về các nguyên tắc lập kế hoạch quy trình. Nó bao gồm việc

đồng bộ hóa quy trình và các loại cơ chế đồng bộ hóa quy trình khác nhau. Nó cho thấy cách sử dụng đồng bộ hóa quy trình để triển khai

các hệ thống nhúng trình điều khiển sự kiện. Nó thảo luận về các loại sơ đồ giao tiếp quy trình khác nhau, bao gồm bộ nhớ dùng chung,

đường ống và truyền thông điệp. Nó cho thấy cách tích hợp các khái niệm và kỹ thuật này để triển khai hạt nhân bộ xử lý đơn (UP) hỗ

trợ quản lý quy trình bằng cả lập lịch trình quy trình không ưu tiên và ưu tiên. Nhân UP sẽ là nền tảng để phát triển các hệ điều hành

hoàn chỉnh ở các chương sau.

Danh sách các chương trình mẫu

C5.1: Chuyển ngữ cảnh

C5.2: Đa nhiệm C5.3.

Quy trình động C5.4. Hệ

thống đa nhiệm theo sự kiện sử dụng chế độ ngủ/thức C5.5: Hệ thống

đa nhiệm theo hướng chẵn sử dụng ngữ nghĩa C5.6: Ống C5.7.

Thông báo

truyền C5.8: Kernel UP

không ưu tiên C5.9: Kernel UP ưu tiên


Machine Translated by Google

166 5 Quản lý quy trình trong hệ thống nhúng

Các vấn đề

1. Trong chương trình ví dụ C5.2, hàm tswitch lưu tất cả các thanh ghi CPU trong tiến trình kstack và nó khôi phục tất cả các thanh ghi lưu của quá

trình tiếp theo khi nó tiếp tục. Vì tswitch() được gọi là hàm nên rõ ràng không cần thiết phải lưu/khôi phục R0. Giả sử rằng hàm tswitch được triển

khai như

chuyển đổi:

// vô hiệu hóa các ngắt IRQ stmfd

sp!, {r4-r12, lr}

LDR r0, = đang chạy // r0=&running // r1-

LDR r1, [r0, #0] >runningPROC // đang chạy-

str sp, [r1, #4] >ksp = sp

lịch trình bl

LDR r0, = đang chạy

LDR r1, [r0, #0] // r1->runningPROC

lDR sp, [r1, #4]

// kích hoạt ngắt IRQ

ldmfd sp!, {r4-r12, pc}

(1). Chỉ ra cách khởi tạo kstack của một tiến trình mới để nó bắt đầu thực thi hàm body().

(2). Giả sử hàm body() được viết dưới dạng int body(int

dummy, int pid, int ppid){ }

trong đó các tham số pid, ppid là id tiến trình và id tiến trình gốc của quy trình mới. Hiển thị cách sửa đổi hàm kfork() để thực hiện điều này.

2. Viết lại trình điều khiển UART ở Chap. 3 bằng cách sử dụng chế độ ngủ/thức để đồng bộ hóa các quy trình và trình xử lý ngắt.

3. Trong chương trình ví dụ C5.3, tất cả các tiến trình được tạo với cùng mức độ ưu tiên (để chúng lần lượt chạy).

(1). Điều gì sẽ xảy ra nếu các quy trình được tạo với mức độ ưu tiên khác nhau?

(2). Triển khai hàm Change_priority(int new_priority) để thay đổi mức độ ưu tiên của tác vụ đang chạy thành new_priority.

Chuyển đổi tiến trình nếu tiến trình đang chạy hiện tại không còn mức ưu tiên cao nhất.

4. Với các tiến trình động, một tiến trình có thể kết thúc khi nó đã hoàn thành nhiệm vụ của mình. Triển khai hàm kexit() để kết thúc tác vụ.

5. Trong tất cả các chương trình ví dụ, mỗi cấu trúc PROC có một kstack cấp phát tĩnh 4KB.

(1). Triển khai trình quản lý bộ nhớ đơn giản để phân bổ/phân bổ bộ nhớ một cách linh hoạt. Khi hệ thống khởi động, hãy đặt trước một

một phần bộ nhớ, ví dụ vùng 1MB bắt đầu từ 4MB, là vùng bộ nhớ trống. Chức năng

char *malloc(kích thước int)

phân bổ một phần bộ nhớ trống có kích thước byte. Khi một vùng bộ nhớ không còn cần thiết nữa, nó sẽ được giải phóng trở lại vùng bộ nhớ trống bằng

cách

void mfree(char *địa chỉ, kích thước int)

Thiết kế cấu trúc dữ liệu để thể hiện bộ nhớ trống hiện có. Sau đó triển khai các hàm malloc() và mfree().
Machine Translated by Google

5.15 Tóm tắt 167

(2). Sửa đổi trường kstack của cấu trúc PROC thành một con trỏ nguyên int *kstack;

và sửa đổi hàm kfork() thành

int kfork(int func, int ưu tiên, int stack_size)

phân bổ động vùng bộ nhớ stack_size (tính bằng đơn vị 1KB) cho quy trình mới.

(3). Khi một tiến trình kết thúc, vùng ngăn xếp của nó phải được giải phóng. Làm thế nào để thực hiện điều này?

6. Người ta biết rằng trình xử lý ngắt không bao giờ được chuyển sang chế độ ngủ, bị chặn hoặc chờ đợi. Giải thích vì sao?

7. Việc sử dụng sai ngữ nghĩa có thể dẫn đến bế tắc. Khảo sát các tài liệu để tìm ra cách giải quyết bế tắc bằng cách ngăn ngừa bế tắc,

tránh bế tắc, giải quyết và phục hồi bế tắc.

8. Chương trình ống dẫn C5.6 tương tự như các ống dẫn có tên (FIFO) trong Linux. Đọc trang Linux trên fifo để tìm hiểu cách sử dụng các

đường dẫn có tên để liên lạc giữa các quá trình.

9. Sửa đổi chương trình ví dụ C5.8 bằng cách thêm một bộ quy trình hợp tác sản xuất-tiêu dùng khác vào hệ thống. Hãy để nhà sản xuất lấy

dòng từ KBD, xử lý ký tự và chuyển chúng đến người tiêu dùng, từ đó xuất ký tự đến thiết bị đầu cuối UART thứ hai.

10. Sửa đổi chương trình mẫu C5.9 để xử lý các ngắt IRQ lồng nhau trong chế độ SYS nhưng vẫn cho phép ưu tiên tác vụ khi kết thúc quá trình

xử lý ngắt lồng nhau.

11. Giả sử rằng tất cả các tiến trình đều có mức độ ưu tiên như nhau. Sửa đổi chương trình mẫu C5.9 để hỗ trợ lập kế hoạch quy trình theo
lát thời gian.

Người giới thiệu

Accetta, M. và cộng sự, "Mach: Nền tảng hạt nhân mới để phát triển UNIX", Hội nghị kỹ thuật - USENIX, 1986.
Chuỗi công cụ ARM: http://gnutoolchains.com/arm-eabi, 2016.
Bach, MJ, "Thiết kế hệ điều hành Unix", Prentice Hall, 1990.
Buttlar, D, Farrell, J, Nichols, B., "Lập trình PThreads, Tiêu chuẩn POSIX để xử lý đa nhiệm tốt hơn", O'Reilly Media, 1996.
Dijkstra, EW, "Các quy trình tuần tự hợp tác", trong Ngôn ngữ lập trình, Nhà xuất bản học thuật, 1965.
Hoare, CAR, "Màn hình: Khái niệm cấu trúc hệ điều hành", CACM, Tập. 17, 1974.
Hướng dẫn dịch vụ trình biên dịch chương trình MVS của IBM, Oz/OS V1R11.0, IBM, 2010.
Lampson, B; Redell, D. (tháng 6 năm 1980). "Kinh nghiệm với các quy trình và màn hình trong MESA". Truyền thông của ACM (CACM) 23 (2): 105–
117, 1980.
OpenVMS: Tài liệu về hệ thống HP OpenVMS, http://www.hp.com/go/openvms/doc, 2014.
Pthread: https://computing.llnl.gov/tutorials/pthreads/, 2015.
Trình giả lập QEMU: "Tài liệu người dùng Trình giả lập QEMU", http://wiki.qemu.org/download/qemu-doc.htm, 2010.
Silberschatz, A., PA Galvin, PA, Gagne, G, "Khái niệm hệ điều hành, Phiên bản thứ 8 ", John Wiley & Sons, Inc. 2009.
Stallings, W. "Hệ điều hành: Nguyên tắc thiết kế và nội bộ ( Ấn bản thứ 7)", Prentice Hall, 2011.
Tanenbaum, AS, Woodhull, AS, "Hệ điều hành, Thiết kế và Triển khai, Ấn bản thứ ba", Prentice Hall, 2006.
Versatilepb: Tấm nền ứng dụng đa năng cho ARM926EJ-S, Trung tâm thông tin ARM, 2016.
Wang, KC, "Thiết kế và triển khai Hệ điều hành MTX, Nhà xuất bản Quốc tế Springer AG, 2015.
Machine Translated by Google

Quản lý bộ nhớ trong ARM


6

6.1 Không gian địa chỉ quy trình

Sau khi bật nguồn hoặc đặt lại, bộ xử lý ARM bắt đầu thực thi mã xử lý đặt lại ở chế độ Giám sát (SVC). Trình xử lý đặt lại trước
tiên sao chép bảng vectơ sang địa chỉ 0, khởi tạo các ngăn xếp của các chế độ đặc quyền khác nhau để xử lý các ngắt và ngoại lệ,
đồng thời cho phép các ngắt IRQ. Sau đó, nó thực thi chương trình điều khiển hệ thống, chương trình này tạo và khởi động các quy
trình hoặc tác vụ. Trong mô hình quy trình tĩnh, tất cả các tác vụ chạy ở chế độ SVC trong cùng một không gian địa chỉ của nhân hệ thống.
Nhược điểm chính của sơ đồ này là thiếu khả năng bảo vệ bộ nhớ. Trong khi thực thi trong cùng một không gian địa chỉ, các tác vụ chia

sẻ cùng các đối tượng dữ liệu chung và có thể gây nhiễu lẫn nhau. Một tác vụ được thiết kế kém hoặc hoạt động sai có thể làm hỏng

không gian địa chỉ dùng chung, khiến các tác vụ khác không thành công. Để có độ tin cậy và bảo mật hệ thống tốt hơn, mỗi tác vụ phải

chạy trong không gian địa chỉ riêng, được tách biệt và bảo vệ khỏi các tác vụ khác. Trong kiến trúc ARM, các tác vụ có thể chạy ở chế

độ Người dùng không có đặc quyền. Rất dễ dàng để chuyển CPU ARM từ chế độ đặc quyền sang chế độ Người dùng. Tuy nhiên, khi ở chế độ

Người dùng, cách duy nhất để vào chế độ đặc quyền là bằng một trong các cách sau.

Ngoại lệ: khi xảy ra ngoại lệ, CPU sẽ chuyển sang chế độ đặc quyền tương ứng để xử lý ngoại lệ đó
Ngắt: ngắt khiến CPU chuyển sang chế độ FIQ hoặc IRQ
SWI: lệnh SWI khiến CPU vào chế độ Giám sát hoặc SVC

Trong kiến trúc ARM, Chế độ hệ thống là một chế độ đặc quyền riêng biệt, chia sẻ cùng một bộ thanh ghi CPU với chế độ
Người dùng, nhưng nó không giống với chế độ hệ thống hoặc nhân như trong hầu hết các bộ xử lý khác. Để tránh nhầm lẫn,
chúng ta sẽ gọi chế độ ARM SVC là chế độ Kernel. SWI có thể được sử dụng để thực hiện các cuộc gọi hệ thống, cho phép quy
trình Chế độ người dùng vào chế độ Kernel, thực thi các chức năng của kernel và quay lại chế độ Người dùng với kết quả
mong muốn. Để phân tách và bảo vệ các vùng bộ nhớ của từng tác vụ riêng lẻ, cần phải kích hoạt phần cứng quản lý bộ nhớ,
phần cứng này cung cấp cho mỗi tác vụ một không gian địa chỉ ảo riêng biệt. Trong chương này, chúng ta sẽ đề cập đến đơn
vị quản lý bộ nhớ ARM (MMU) và trình diễn việc ánh xạ địa chỉ ảo và bảo vệ bộ nhớ bằng các chương trình ví dụ.

6.2 Đơn vị quản lý bộ nhớ (MMU) trong ARM

Đơn vị quản lý bộ nhớ ARM (MMU) (ARM926EJ-S 2008) thực hiện hai chức năng chính: Thứ nhất, nó dịch địa chỉ ảo thành địa
chỉ vật lý. Thứ hai, nó kiểm soát quyền truy cập bộ nhớ bằng cách kiểm tra quyền. Phần cứng MMU thực hiện các chức năng
này bao gồm Bộ đệm tra cứu dịch (TLB), logic điều khiển truy cập và logic đi bộ của bảng dịch. ARM MMU hỗ trợ truy cập bộ
nhớ dựa trên Phần hoặc Trang. Quản lý bộ nhớ theo từng phần là sơ đồ phân trang một cấp. Bảng trang cấp 1 chứa các bộ mô
tả phần, mỗi phần chỉ định một khối bộ nhớ 1 MB. Quản lý bộ nhớ bằng cách phân trang là một sơ đồ phân trang hai cấp. Bảng
trang cấp 1 chứa các bộ mô tả bảng trang, mỗi bộ mô tả một bảng trang cấp 2. Bảng trang cấp 2 chứa các bộ mô tả trang, mỗi
bộ mô tả một khung trang trong bộ nhớ và các bit điều khiển truy cập. Sơ đồ phân trang ARM hỗ trợ hai kích thước trang
khác nhau. Các trang nhỏ bao gồm các khối bộ nhớ 4 KB và các trang lớn bao gồm các khối bộ nhớ 64 KB. Mỗi trang gồm có 4
trang con. Kiểm soát truy cập có thể được mở rộng đến các trang con 1 KB trong các trang nhỏ và tới các trang con 16 KB
trong các trang lớn. ARM MMU cũng hỗ trợ khái niệm miền. Miền là vùng bộ nhớ có thể được xác định bằng quyền truy cập
riêng lẻ. Thanh ghi kiểm soát truy cập miền (DACR) chỉ định quyền truy cập cho tối đa 16 miền khác nhau

© Springer International Publishing AG 2017 169


KC Wang, Hệ điều hành nhúng và thời gian thực, DOI
10.1007/978-3-319-51517-5_6
Machine Translated by Google

170 6 Quản lý bộ nhớ trong ARM

tên miền, 0–15. Khả năng truy cập của mỗi miền được chỉ định bằng quyền 2 bit, trong đó 00 cho không có quyền truy cập, 01
cho chế độ máy khách, kiểm tra các bit Quyền truy cập (AP) của tên miền hoặc các mục trong bảng trang và 11 cho chế độ người
quản lý. không kiểm tra các bit AP trong miền.

TLB chứa 64 mục dịch trong bộ đệm. Trong hầu hết các truy cập bộ nhớ, TLB cung cấp thông tin dịch sang logic điều khiển
truy cập. Nếu TLB chứa mục nhập được dịch cho địa chỉ ảo thì logic kiểm soát truy cập sẽ xác định xem có được phép truy cập
hay không. Nếu quyền truy cập được cho phép, MMU sẽ xuất địa chỉ vật lý thích hợp tương ứng với địa chỉ ảo. Nếu quyền truy
cập không được phép, MMU sẽ báo hiệu cho CPU hủy bỏ. Nếu TLB không chứa mục nhập đã dịch cho địa chỉ ảo thì phần cứng đi lại
bảng dịch sẽ được gọi để lấy thông tin dịch từ bảng dịch trong bộ nhớ vật lý. Sau khi được truy xuất, thông tin dịch sẽ được
đặt vào TLB, có thể ghi đè mục nhập hiện có. Mục nhập cần ghi đè được chọn bằng cách luân chuyển tuần tự qua các vị trí TLB.

Khi MMU bị tắt, ví dụ như trong quá trình thiết lập lại, không có bản dịch địa chỉ. Trong trường hợp này mọi địa chỉ ảo đều
là địa chỉ vật lý. Dịch địa chỉ chỉ có hiệu lực khi MMU được kích hoạt.

6.3 Thanh ghi MMU

Bộ xử lý ARM coi MMU là bộ đồng xử lý. MMU chứa một số thanh ghi 32 bit điều khiển hoạt động của MMU. Hình 6.1 thể hiện định
dạng của các thanh ghi MMU. Các thanh ghi MMU có thể được truy cập bằng cách sử dụng các lệnh MRC và MCR.

Sau đây là mô tả ngắn gọn về các thanh ghi ARM MMU c0 đến c10.

Đăng ký c0 dùng để truy cập vào Thanh ghi ID, Thanh ghi loại bộ đệm và Thanh ghi trạng thái TCM. Việc đọc từ thanh ghi này
trả về ID thiết bị, loại bộ đệm hoặc trạng thái TCM tùy thuộc vào giá trị của Opcode_2 được sử dụng.
Thanh ghi c1 là Thanh ghi điều khiển, xác định cấu hình của MMU. Cụ thể, việc thiết lập bit M (bit 0) sẽ kích hoạt MMU và xóa
bit M sẽ vô hiệu hóa MMU. Bit V của c1 chỉ định liệu bảng vectơ có được ánh xạ lại trong quá trình đặt lại hay không. Vị trí
bảng vectơ mặc định là 0x00. Nó có thể được ánh xạ lại thành 0xFFFF0000 trong khi đặt lại.
Thanh ghi c2 là Thanh ghi cơ sở bảng dịch (TTBR). Nó chứa địa chỉ vật lý của bảng dịch cấp một, phải nằm trên ranh giới 16kB
trong bộ nhớ chính. Việc đọc từ c2 trả về con trỏ tới bảng dịch cấp một hiện đang hoạt động. Việc ghi vào thanh ghi c2 cập
nhật con trỏ tới bảng dịch cấp một.
Đăng ký c3 là Đăng ký kiểm soát truy cập tên miền. Nó bao gồm 16 trường hai bit, mỗi trường xác định quyền truy cập cho một
trong mười sáu Miền (D15-D0).
Đăng ký c4 hiện không được sử dụng.

Hình 6.1 Thanh ghi ARM MMU


Machine Translated by Google

6.3 Thanh ghi MMU 171

Thanh ghi c5 là Thanh ghi trạng thái lỗi (FSR). Nó cho biết tên miền và loại quyền truy cập đang được thử khi việc hủy bỏ
xảy ra. Bit 7:4 chỉ định miền nào trong số mười sáu miền (D15–D0) đang được truy cập và Bit 3:1 cho biết loại truy cập đang
được thử. Việc ghi vào thanh ghi này sẽ xóa TLB.
Thanh ghi c6 truy cập vào Thanh ghi địa chỉ lỗi (FAR). Nó giữ địa chỉ ảo của quyền truy cập khi xảy ra lỗi.
Việc ghi vào thanh ghi này khiến dữ liệu được ghi được coi là một địa chỉ và nếu nó được tìm thấy trong TLB thì mục nhập sẽ được đánh dấu là

không hợp lệ. Hoạt động này được gọi là thanh lọc TLB. Thanh ghi trạng thái lỗi và Thanh ghi địa chỉ lỗi chỉ được cập nhật cho các lỗi dữ

liệu, không phải cho các lỗi tìm nạp trước.

Thanh ghi c7 kiểm soát bộ đệm và bộ đệm ghi. Thanh


ghi c8 là Thanh ghi hoạt động TLB. Nó được sử dụng chủ yếu để vô hiệu hóa các mục TLB. TLB được chia thành hai phần: phần
liên kết tập hợp và phần liên kết đầy đủ. Phần liên kết đầy đủ, còn được gọi là phần khóa của TLB, được sử dụng để lưu trữ
các mục cần khóa. Các mục được giữ trong phần khóa của TLB được bảo toàn trong quá trình hoạt động TLB không hợp lệ. Các
mục nhập có thể được xóa khỏi TLB khóa bằng cách sử dụng thao tác nhập một TLB không hợp lệ. Các hoạt động TLB không hợp
lệ sẽ làm mất hiệu lực tất cả các mục không được bảo tồn trong TLB. Các hoạt động nhập đơn TLB không hợp lệ sẽ làm mất hiệu
lực bất kỳ mục nhập TLB nào tương ứng với địa chỉ ảo.
Đăng ký c9 truy cập Khóa bộ nhớ đệm và Đăng ký khu vực TCM trên một số bo mạch ARM được trang bị TCM.
Thanh ghi c10 là Thanh ghi khóa TLB. Nó kiểm soát vùng khóa trong TLB.

6.4 Truy cập các thanh ghi MMU

Các thanh ghi của CP15 có thể được truy cập bằng các lệnh MRC và MCR ở chế độ đặc quyền. Định dạng lệnh được hiển thị trong
Hình 6.2.

MCR{cond} p15,<Opcode_1>,<Rd>,<CRn>,<CRm>,<Opcode_2>
MCR{cond} p15,<Opcode_1>,<Rd>,<CRn>,<CRm>,<Opcode_2>

Trường CRn chỉ định thanh ghi bộ đồng xử lý để truy cập. Trường CRm và trường Opcode_2 chỉ định một trường cụ thể
hoạt động khi đánh địa chỉ các thanh ghi. Bit L phân biệt các lệnh MRC (L = 1) và MCR (L = 0).

6.4.1 Kích hoạt và vô hiệu hóa MMU

MMU được kích hoạt bằng cách ghi bit M, bit 0, của Thanh ghi điều khiển CP15 c1. Khi đặt lại, bit này bị xóa thành 0, vô
hiệu hóa MMU.

6.4.1.1 Kích hoạt MMU


Trước khi kích hoạt MMU, hệ thống phải thực hiện các bước sau:

1. Lập trình tất cả các thanh ghi CP15 có liên quan. Điều này bao gồm việc thiết lập các bảng dịch phù hợp trong bộ nhớ.

2. Vô hiệu hóa và vô hiệu hóa Bộ nhớ đệm lệnh. Bộ đệm lệnh có thể được bật khi bật MMU.

Hình 6.2 Định dạng lệnh MCR và MRC


Machine Translated by Google

172 6 Quản lý bộ nhớ trong ARM

Để kích hoạt MMU hãy tiến hành như sau:

1. Lập trình các thanh ghi kiểm soát truy cập miền và cơ sở bảng dịch.

2. Lập trình các bảng trang mô tả cấp một và cấp hai nếu cần.

3. Kích hoạt MMU bằng cách đặt bit 0 trong Thanh ghi điều khiển CP15 c1.

6.4.1.2 Vô hiệu hóa MMU Để vô

hiệu hóa MMU, hãy tiến hành như sau:

(1). Xóa bit 2 trong Thanh ghi điều khiển CP15 c1. Bộ nhớ đệm dữ liệu phải bị vô hiệu hóa trước hoặc cùng lúc với việc MMU bị vô hiệu hóa bằng cách

xóa bit 2 của Thanh ghi điều khiển.

Nếu MMU được bật, sau đó bị tắt và sau đó được bật lại, nội dung của TLB sẽ được giữ nguyên. Nếu TLB

các mục nhập hiện không hợp lệ, chúng phải bị vô hiệu hóa trước khi MMU được kích hoạt lại.

(2). Xóa bit 0 trong Thanh ghi điều khiển CP15 c1.

Khi MMU bị vô hiệu hóa, việc truy cập bộ nhớ được xử lý như sau:

• Tất cả các quyền truy cập dữ liệu đều được coi là không thể lưu vào bộ nhớ đệm. Giá trị của bit C, bit 2, của Thanh ghi điều khiển CP15 c1 phải

bằng 0. • Tất cả các quyền truy cập lệnh được coi là Có thể lưu vào bộ nhớ đệm nếu bit I (bit 12) của Thanh ghi điều khiển CP15 c1 được đặt thành 1
và Không thể lưu vào bộ đệm nếu bit I được

đặt thành 0. • Tất cả các quyền truy cập rõ ràng đều có thứ tự mạnh. Giá trị của bit W, bit 3, của Thanh ghi điều khiển CP15 c1 bị bỏ qua. • Không

có hoạt động kiểm tra quyền truy cập bộ nhớ nào được thực hiện và MMU không tạo ra lệnh hủy bỏ nào. • Địa chỉ vật lý cho

mỗi lần truy cập bằng Địa chỉ ảo của nó. Điều này được gọi là ánh xạ địa chỉ phẳng. • FCSE PID sẽ bằng 0 khi MMU bị vô hiệu hóa. Đây là giá
trị đặt lại của FCSE PID. Nếu MMU được

bị vô hiệu hóa FCSE PID phải được xóa.

• Tất cả các hoạt động của CP15 MMU và bộ đệm hoạt động bình thường khi MMU bị tắt. • Lệnh và thao tác

tìm nạp trước dữ liệu hoạt động như bình thường. Tuy nhiên, không thể bật Bộ nhớ đệm dữ liệu khi MMU bị tắt. Do đó, thao tác tìm nạp trước dữ liệu

không có hiệu lực. Các thao tác tìm nạp trước lệnh không có hiệu lực nếu Bộ nhớ đệm lệnh bị tắt. Không có quyền truy cập bộ nhớ nào được thực

hiện và địa chỉ được ánh xạ phẳng.


• Quyền truy cập vào TCM hoạt động bình thường nếu TCM được bật.

6.4.2 Kiểm soát truy cập tên miền

Truy cập bộ nhớ được kiểm soát chủ yếu bởi các miền. Có 16 miền, mỗi miền được xác định bởi 2 bit trong thanh ghi Kiểm soát truy cập miền. Mỗi miền

hỗ trợ hai loại người dùng.

Khách hàng: Khách hàng sử dụng tên miền

Người quản lý: Người quản lý kiểm soát hành vi của miền.

Các miền được xác định trong Đăng ký kiểm soát truy cập miền. Trong hình 6.1, hàng 3 minh họa cách 32 bit của

thanh ghi được phân bổ để xác định 16 miền 2-bit.

Bảng 6.1 cho thấy ý nghĩa của các bit truy cập miền.

Bảng 6.1 Bit truy cập trong thanh ghi kiểm soát truy cập miền
Machine Translated by Google

6.4 Truy cập các thanh ghi MMU 173

Bảng 6.2 Mã hóa trường trạng thái FSR

6.4.3 Thanh ghi cơ sở bảng dịch

Thanh ghi c2 là Thanh ghi cơ sở bảng dịch (TTBR), dành cho địa chỉ cơ sở của bảng dịch cấp một. Đọc
từ c2 trả về con trỏ tới bảng dịch cấp một hiện đang hoạt động theo bit [31:14] và một giá trị Không thể đoán trước trong
bit [13:0]. Việc ghi vào thanh ghi c2 cập nhật con trỏ tới bảng dịch cấp một từ giá trị tính bằng bit [31:14] của
giá trị bằng văn bản. Bit [13:0] phải bằng 0. TTBR có thể được truy cập bằng các hướng dẫn sau.

MRC p15, < Rd > ,c2, c0, 0 đọc TTBR


MRC p15, < Rd > ,c2, c0, 0 ghi TTBR

Các trường CRm và Opcode_2 là SBZ (Should-Be-Zero) khi ghi vào c2.

6.4.4 Đăng ký kiểm soát truy cập tên miền

Thanh ghi c3 là Thanh ghi kiểm soát truy cập miền bao gồm 16 trường hai bit. Mỗi trường hai bit xác định quyền truy cập
quyền cho một trong 16 tên miền, D15–D0. Đọc từ c3 trả về giá trị của Thanh ghi kiểm soát truy cập miền.
Việc ghi vào c3 sẽ ghi giá trị của Thanh ghi kiểm soát truy cập miền. Các bit kiểm soát truy cập miền 2 bit được định nghĩa là

Giá trị Nghĩa Sự miêu tả

00 Không truy cập Mọi quyền truy cập đều tạo ra lỗi miền

01 Khách hàng Các quyền truy cập được kiểm tra dựa trên các bit quyền truy cập trong bộ mô tả phần hoặc trang

10 Kín đáo Hiện tại hoạt động giống như chế độ không có quyền truy cập

11 Giám đốc Các quyền truy cập không được kiểm tra dựa trên các bit quyền truy cập nên không thể tạo ra lỗi quyền

Đăng ký kiểm soát truy cập tên miền có thể được truy cập bằng các hướng dẫn sau:

MRC p15, 0, < Rd > , c3, c0, 0 ; đọc quyền truy cập tên miền
MCR p15, 0, < Rd > , c3, c0, 0 ; viết quyền truy cập tên miền

6.4.5 Thanh ghi trạng thái lỗi

Thanh ghi c5 là Thanh ghi trạng thái lỗi (FSR). Các FSR chứa nguồn của lệnh cuối cùng hoặc lỗi dữ liệu. Các
FSR phía lệnh chỉ nhằm mục đích gỡ lỗi. FSR được cập nhật cho các lỗi căn chỉnh và các thao tác hủy bỏ bên ngoài
xảy ra khi MMU bị vô hiệu hóa. FSR được truy cập được xác định bởi giá trị của trường Opcode_2:
Machine Translated by Google

174 6 Quản lý bộ nhớ trong ARM

Opcode_2 = 0 Thanh ghi trạng thái lỗi dữ liệu (DFSR).


Opcode_2 = 1 Thanh ghi trạng thái lỗi lệnh (IFSR).

FSR có thể được truy cập bằng các hướng dẫn sau.

MRC p15, 0, < Rd > , c5, c0, 0 ;đọc DFSR


MCR p15, 0, < Rd > , c5, c0, 0 ;viết DFSR
MRC p15, 0, < Rd > , c5, c0, 1 ;đọc IFSR
MCR p15, 0, < Rd > , c5, c0, 1 ;viết IFSR

Định dạng của Thanh ghi trạng thái lỗi (FSR) là

|31 9| 8 |7 6 5 4 |3 2 1 0 |
————————————————————————————————————————————————————— —————

| UNP/SBZ | 0 | Tên miền | Trạng thái |


————————————————————————————————————————————————————— —————

Phần sau đây mô tả các trường bit trong FSR.

Chút ít
Sự miêu tả

[31:9] UNP/SBZP

[số 8] Luôn luôn đọc là số không. Viết bị bỏ qua

[7:4] Chỉ định miền (D15–D0) đang được truy cập khi xảy ra lỗi dữ liệu

[3:0] Loại lỗi phát sinh. Bảng 6.2 hiển thị mã hóa của trường trạng thái trong FSR và nếu trường Miền chứa giá trị hợp lệ
thông tin

6.4.6 Đăng ký địa chỉ lỗi

Thanh ghi c6 là Thanh ghi địa chỉ lỗi (FAR). Nó chứa Địa chỉ ảo đã sửa đổi của quyền truy cập đang được thử
khi xảy ra việc hủy bỏ dữ liệu. FAR chỉ được cập nhật cho Hủy bỏ dữ liệu, không dành cho Hủy bỏ tìm nạp trước. FAR được cập nhật cho
lỗi căn chỉnh và việc hủy bỏ bên ngoài xảy ra khi MMU bị vô hiệu hóa. FAR có thể được truy cập bằng cách sử dụng như sau
hướng dẫn.

MRC p15, 0, < Rd > , c6, c0, 0 ; đọc XA

MCR p15, 0, < Rd > , c6, c0, 0 ; viết XA

Viết c6 đặt FAR thành giá trị của dữ liệu được ghi. Điều này rất hữu ích cho trình gỡ lỗi để khôi phục giá trị của FAR về
trạng thái trước đó. Các trường CRm và Opcode_2 là SBZ (Nên bằng 0) khi đọc hoặc ghi CP15 c6.

6.5 Dịch địa chỉ ảo

MMU dịch các địa chỉ ảo do CPU tạo ra thành địa chỉ vật lý để truy cập bộ nhớ ngoài, đồng thời
xuất phát và kiểm tra quyền truy cập. Thông tin dịch thuật, bao gồm cả dữ liệu dịch địa chỉ và
dữ liệu về quyền truy cập nằm trong bảng dịch nằm trong bộ nhớ vật lý. MMU cung cấp logic cần thiết để
duyệt qua bảng dịch, lấy địa chỉ đã dịch và kiểm tra quyền truy cập. Quá trình dịch thuật bao gồm
của các bước sau.
Machine Translated by Google

6.5 Dịch địa chỉ ảo 175

6.5.1 Cơ sở bảng dịch

Thanh ghi cơ sở bảng dịch (TTB) trỏ tới cơ sở của bảng dịch trong bộ nhớ vật lý chứa các bộ mô tả Phần và/hoặc Trang.

6.5.2 Bảng dịch

Bảng dịch là bảng trang cấp một. Nó chứa 4096 mục 4 byte và nó phải nằm trên ranh giới 16 KB trong bộ nhớ vật lý. Mỗi mục là
một bộ mô tả, xác định nền bảng trang cấp 2 hoặc nền phần. Hình 6.3 cho thấy định dạng của các mục trong trang cấp một.

6.5.3 Bộ mô tả cấp một

Bộ mô tả cấp một là Bộ mô tả bảng trang hoặc Bộ mô tả phần và định dạng của nó thay đổi tương ứng.
Hình 6.3 cho thấy định dạng của bộ mô tả cấp một. Loại mô tả được chỉ định bởi hai bit có trọng số thấp nhất.

6.5.3.1 Bộ mô tả bảng trang Bộ mô tả


bảng trang (hàng thứ hai trong Hình 6.3) xác định bảng trang cấp 2. Chúng ta sẽ thảo luận về phân trang 2 cấp trong Phần. 6.6.

6.5.3.2 Bộ mô tả phần Bộ mô tả
phần (hàng thứ ba trong Hình 6.3) có địa chỉ cơ sở 12 bit, trường AP 2 bit, trường miền 4 bit, bit C và B và mã định danh loại
(b10) . Các trường bit được định nghĩa như sau.

Bit 31:20: địa chỉ cơ sở của phần 1 MB trong bộ nhớ.


Bit 19:12: luôn là 0.
Bits 11:10 (AP): quyền truy cập của phần này. Việc diễn giải chúng phụ thuộc vào bit S và R (bit 8–9 của thanh ghi điều khiển
MMU C1). Cài đặt AP và SR được sử dụng phổ biến nhất như sau.

AP SR -Người giám sát- -Người dùng - -------------- Ghi chú--------------------

00 xx Không có quyền truy cập Không truy cập

01 xx Đọc/Ghi Không truy cập Chỉ cho phép truy cập ở chế độ Người giám sát Chỉ đọc Ghi

10 xx Đọc/Ghi ở chế độ Người dùng gây ra lỗi quyền Đọc/Ghi Cho phép truy cập ở cả hai chế độ.

11 xx Đọc/Ghi

Bit 8:5: chỉ định một trong mười sáu miền có thể (trong Thanh ghi kiểm soát truy cập miền) tạo thành miền chính
điều khiển truy cập.

Bit 4: phải là 1.

Bit 3:2 (C và B) kiểm soát bộ đệm và ghi các chức năng liên quan đến bộ đệm như sau: C—

Có thể lưu vào bộ đệm: dữ liệu tại địa chỉ này sẽ được đặt trong bộ đệm (nếu bộ đệm được bật).
B—Có thể đệm: dữ liệu tại địa chỉ này sẽ được ghi thông qua bộ đệm ghi (nếu bộ đệm ghi được bật).

Hình 6.3 Bộ mô tả cấp một


Machine Translated by Google

176 6 Quản lý bộ nhớ trong ARM

Hình 6.4 Bản dịch tài liệu tham khảo phần

6.6 Bản dịch tài liệu tham khảo phần

Trong kiến trúc ARM, loại sơ đồ phân trang đơn giản nhất là các phần có kích thước 1 MB, chỉ sử dụng bảng trang cấp một.
Vì vậy, trước tiên chúng ta thảo luận về việc quản lý bộ nhớ theo từng phần. Khi MMU dịch một địa chỉ ảo sang địa chỉ vật lý,
nó sẽ tra cứu các bảng trang. Quá trình dịch thuật thường được gọi là bước đi trong bảng trang. Khi sử dụng các phần, bản dịch
bao gồm các bước sau, được mô tả trong Hình 6.4.

(1). Địa chỉ ảo (VA) bao gồm chỉ mục Bảng 12 bit và chỉ mục Phần 20 bit, là phần bù trong phần.
MMU sử dụng chỉ mục Bảng 12 bit để truy cập bộ mô tả phần trong bảng dịch được chỉ ra bởi TTBR.
(2). Bộ mô tả phần chứa địa chỉ cơ sở 12 bit, trỏ đến phần 1 MB trong bộ nhớ, trường AP (2 bit) và số miền (4 bit). Đầu tiên,
nó kiểm tra quyền truy cập miền trong sổ đăng ký Kiểm soát truy cập miền. Sau đó, nó kiểm tra các bit AP để biết khả năng truy
cập vào Phần.
(3). Nếu quá trình kiểm tra quyền thành công, nó sẽ sử dụng địa chỉ cơ sở của phần 12 bit và chỉ mục Phần 20 bit để tạo địa chỉ
vật lý như

(32-bit)PA = ((12-bit)Section_base_address << 20) + (20-bit)Section_index

6.7 Bản dịch tài liệu tham khảo trang

Khi sử dụng phân trang 2 cấp, các bảng trang bao gồm những điều sau đây.

6.7.1 Bảng trang cấp 1

Bảng trang cấp 1 chứa các bộ mô tả Bảng Trang (Hàng thứ hai trong Hình 6.3). Nội dung của bộ mô tả Bảng trang là

Bit 31:10 : địa chỉ cơ sở của Bảng trang cấp 2 chứa các bộ mô tả trang cấp 2.
Bit 8:5 : số miền; kiểm soát truy cập vào miền này được chỉ định trong Thanh ghi kiểm soát truy cập miền.

6.7.2 Bộ mô tả trang cấp 2

Định dạng của Bộ mô tả bảng trang cấp 2 được hiển thị trong Hình 6.5.
Trong bộ mô tả trang cấp 2, hai bit có ý nghĩa nhỏ nhất biểu thị kích thước và tính hợp lệ của trang. Các bit khác được hiểu
như sau.
Machine Translated by Google

6.7 Bản dịch tài liệu tham khảo trang 177

Hình 6.5 Mục nhập bảng trang (Mô tả cấp độ hai)

Bit 31:16 (trang lớn) hoặc bit 31:12 (trang nhỏ) chứa địa chỉ vật lý của khung trang trong bộ nhớ. Kích thước trang
lớn là 64 KB và kích thước trang nhỏ là 4 KB.
Bit 11:4 chỉ định quyền truy cập (ap3–ap0) của bốn trang con. Điều này cho phép kiểm soát truy cập tốt hơn trong một
trang, nhưng nó hiếm khi được sử dụng trong thực tế.

Bit 3 C—Có thể lưu vào bộ đệm: cho biết dữ liệu tại địa chỉ này sẽ được đặt trong IDC (nếu bộ đệm được bật).
Bit 2 B—Có thể đệm: chỉ ra rằng dữ liệu tại địa chỉ này sẽ được ghi thông qua bộ đệm ghi (nếu bộ đệm ghi được bật).

6.7.3 Dịch tài liệu tham khảo trang nhỏ

Dịch trang bao gồm một bước bổ sung ngoài bước dịch phần: bộ mô tả Cấp 1 là bộ mô tả Bảng Trang, trỏ đến bảng trang Cấp 2
chứa các bộ mô tả trang Cấp 2. Mỗi bộ mô tả trang Cấp 2 trỏ tới một khung trang trong bộ nhớ vật lý. Việc dịch các tài
liệu tham khảo trang nhỏ bao gồm các bước sau, được mô tả trong Hình 6.6.

(1). Địa chỉ ảo VA bao gồm Chỉ mục bảng cấp 1 12 bit, Chỉ mục bảng cấp 2 8 bit và Chỉ mục trang 12 bit, là độ lệch byte
trong trang.
(2). Sử dụng Chỉ mục bảng cấp 1 12 bit để truy cập bộ mô tả cấp 1 trong bảng dịch được chỉ định bởi thanh ghi cơ sở bảng
dịch (TTBR).
(3). Kiểm tra quyền truy cập tên miền trong bộ mô tả cấp 1 như sau: 00 = hủy bỏ, 01 = kiểm tra AP trong bảng trang cấp 2,
11 = không kiểm tra AP của bảng trang.
(4). 22-bit hàng đầu của bộ mô tả Cấp 1 chỉ định địa chỉ (vật lý) của Bảng trang Cấp 2 chứa 256 mục nhập trang. Sử dụng
Chỉ mục Bảng Cấp 2 8 bit để truy cập bộ mô tả trang Cấp 2 trong bảng Trang Cấp 2.

Hình 6.6 Dịch các trang nhỏ


Machine Translated by Google

178 6 Quản lý bộ nhớ trong ARM

(5). Nếu bit truy cập miền = 01, hãy kiểm tra các bit AP cho phép truy cập trang (ap3-ap0) trong bộ mô tả Cấp 2.
(6). 20 bit đầu của bộ mô tả trang Cấp 2 chỉ định PageFrameAddress của khung trang trong bộ nhớ vật lý. Nếu quá trình kiểm tra
quyền truy cập vượt qua, Địa chỉ vật lý (PA) được tạo sẽ là

(32-bit)PA = ((20-bit)PageFrameAddress << 12) + (12-bit)Chỉ mục trang

6.7.4 Dịch tài liệu tham khảo trang lớn

Dịch các tài liệu tham khảo trang lớn cũng tương tự như dịch các trang nhỏ, ngoại trừ những khác biệt sau.

(1). Đối với các trang lớn, VA = [Chỉ mục L1 12 bit| Chỉ số L2 8 bit | Chỉ mục trang 16-bit].
(2). Do bốn bit trên của Chỉ mục Trang và bốn bit bậc thấp của chỉ mục Bảng Trang Cấp 2 chồng lên nhau, nên mỗi mục nhập bảng
trang cho một Trang Lớn phải được sao chép 16 lần trong các vị trí bộ nhớ liên tiếp trong Bảng Trang Cấp 2. Đây là một thuộc tính
khá đặc biệt của bảng phân trang ARM dành cho các trang lớn. Vì các trang lớn hiếm khi được sử dụng trong thực tế nên chúng tôi
chỉ xem xét các trang nhỏ có kích thước trang 4 KB.

6.8 Chương trình ví dụ về quản lý bộ nhớ

Phần này trình bày một số ví dụ lập trình minh họa cách định cấu hình ARM MMU để quản lý bộ nhớ.

6.8.1 Phân trang một cấp sử dụng các phần 1 MB

Trong chương trình ví dụ đầu tiên, được ký hiệu là C6.1, chúng ta sẽ sử dụng các phần 1 MB để ánh xạ không gian VA sang không
gian PA. Chương trình bao gồm các thành phần sau: Tệp ts.s trong mã hợp ngữ và tệp tc trong C, được liên kết biên dịch (chéo) với
tệp thực thi nhị phân t.bin. Khi chạy trên bảng Versatilepb mô phỏng trong QEMU, nó sẽ được tải về 0x10000 và chạy từ đó. Chương
trình hỗ trợ các thiết bị I/O sau: Màn hình LCD để hiển thị, bàn phím cho đầu vào, UART cho I/O cổng nối tiếp và cả bộ hẹn giờ.
Vì mục tiêu ở đây là thể hiện việc quản lý bộ nhớ nên chúng ta sẽ tập trung vào cách thiết lập MMU cho không gian địa chỉ ảo. Bo
mạch ARM Versatilepb hỗ trợ RAM 256 MB và dung lượng I/O 2 MB bắt đầu từ 256 MB. Trong chương trình này, chúng tôi sẽ sử dụng các
phần 1 MB để tạo ánh xạ nhận dạng của không gian địa chỉ ảo 258 MB thấp đến không gian địa chỉ vật lý thấp 258 MB. Sau đây liệt
kê mã của chương trình C6.1.

1. tập tin ts.s:

//—————————— file ts.s của Chương trình C6.1 ——————————————————

.chữ

.code 32

.vector toàn cầu_bắt đầu, vectơ_end

.global reset_handler, mkptable

.global get_fault_status, get_fault_addr, get_spsr

reset_handler:

LDR sp, =svc_stack_top // đặt ngăn xếp SVC

BL fbuf_init // khởi tạo LCD để hiển thị // sao chép bảng

BL sao chép_vector_table vectơ sang PA 0


Machine Translated by Google

6.8 Chương trình ví dụ về quản lý bộ nhớ 179

//(m1): xây dựng bảng trang cấp 1 sử dụng các phần 1 MB trong mã C

BL mkptable

// (m2): đặt thanh ghi TTB thành 0x4000

mov r0, #0x4000

mcr p15, 0, r0, c2, c0, 0 // đặt thanh ghi TTB

mcr p15, 0, r0, c8, c7, 0 // tuôn ra TLB

//(m3): set domain0 01=client(kiểm tra quyền) 11=master(không kiểm tra)

chuyển r0,#1 // 01 cho chế độ máy khách

mcr p15, 0, r0, c3, c0, 0

//(m4): bật MMU

mrc p15, 0, r0, c1, c0, 0 // lấy c1 vào r0

hoặc r0, r0, #0x00000001 // đặt bit0 thành 1

mcr p15, 0, r0, c1, c0, 0 // ghi vào c1

không // thời gian cho phép MMU kết thúc

không

không

mrc p15, 0, r2, c2, c0, 0 // đọc TLB base reg c2 vào r2

chuyển động r2, r2

// vào chế độ ABT để thiết lập ngăn xếp ABT

MSR cpsr, #0x97

LDR sp, =abt_stack_top

// vào chế độ UND để thiết lập ngăn xếp UND

MSR cpsr, #0x9B

LDR sp, =und_stack_top

// chuyển sang chế độ IRQ để đặt ngăn xếp IRQ và kích hoạt các ngắt IRQ

MSR cpsr, #0x92 // ghi vào cspr, vì vậy bây giờ ở chế độ IRQ // đặt con

LDR sp, =irq_stack_top trỏ ngăn xếp IRQ

// quay lại chế độ SVC

MSR cpsr, #0x13 // Chế độ SVC có bật IRQ

// gọi main() ở chế độ SVC

BL chính

B .

swi_handler: // swi_handler giả, chưa được sử dụng

data_handler:

phụ lr, lr, #4

stmfd sp!, {r0-r12, lr}

bl data_chandler // gọi data_chandler() trong C

ldmfd sp!, {r0-r12, pc}^

irq_handler:

phụ lr, lr, #4

stmfd sp!, {r0-r12, lr}

bl irq_chandler

ldmfd sp!, {r0-r12, pc}^

vectơ_start:

PC LDR, reset_handler_addr
Machine Translated by Google

180 6 Quản lý bộ nhớ trong ARM

Máy tính LDR, undef_handler_addr

Máy tính LDR, swi_handler_addr

PC LDR, tìm nạp trước_abort_handler_addr

PC LDR, dữ liệu_abort_handler_addr

B .

Máy tính LDR, irq_handler_addr

PC LDR, fiq_handler_addr

đặt lại_handler_addr: .word reset_handler

undef_handler_addr: .word undef_handler

swi_handler_addr: .word swi_handler

tìm nạp trước_abort_handler_addr: .word tìm nạp trước_abort_handler

dữ liệu_abort_handler_addr: .word data_handler

irq_handler_addr: .word irq_handler

fiq_handler_addr: .word fiq_handler

vectơ_end:

get_fault_status: // đọc và trả về MMU reg 5

MRC p15,0,r0,c5,c0,0 // đọc DFSR c5

di chuyển máy tính, lr

get_fault_addr: // đọc và trả về MMU reg 6

MRC p15,0,r0,c6,c0,0 // đọc DFAR

di chuyển máy tính, lr

get_spsr: // lấy SPSR

bà r0, spsr

di chuyển máy tính, lr

// ----------- kết thúc tệp ts.s của Chương trình C6.1 ------------

Giải thích về tệp ts.s: Vì chương trình được liên kết biên dịch mà không sử dụng địa chỉ ảo nên mã chương trình có thể là

được thực hiện trực tiếp trong quá trình thiết lập lại khi MMU tắt. Vì vậy, chúng ta có thể gọi các hàm trong mã C khi chương trình bắt đầu

hướng lên. Khi truy cập reset_handler, trước tiên nó sẽ đặt con trỏ ngăn xếp chế độ SVC. Sau đó nó gọi các hàm trong C để khởi tạo LCD cho

hiển thị và sao chép bảng vectơ đến địa chỉ 0. Sau đó, nó thiết lập bảng trang và kích hoạt MMU cho địa chỉ VA đến PA

dịch. Các bước được đánh dấu là (m1) đến (m4), được giải thích chi tiết hơn bên dưới.

(m1): Nó gọi mkptable() trong C để thiết lập bảng trang cấp 1 sử dụng các phần 1 MB. Bảng Versatilepb được mô phỏng bên dưới

QEMU hỗ trợ RAM 256 MB và dung lượng I/O 2 MB ở mức 256 MB. Bảng trang cấp 1 được thiết lập để tạo danh tính

ánh xạ 258 MB VA thấp tới PA, bao gồm 256 MB RAM và 2 MB dung lượng I/O. Thuộc tính của mỗi

bộ mô tả phần được đặt thành 0x412 cho AP = 01 (máy khách), miền = 0000 CB = 00 (bộ đệm D và bộ đệm W bị tắt) và

type = 01 cho các phần 1 MB.

(m2): Nó thiết lập thanh ghi Cơ sở bảng dịch (TTBR) để trỏ vào bảng trang.

(m3): Nó đặt các bit truy cập của miền 0 đến 01 (máy khách) để đảm bảo rằng miền chỉ có thể được truy cập ở chế độ đặc quyền.

Ngoài ra, chúng tôi cũng có thể đặt bit truy cập miền thành 11 (chế độ người quản lý) để cho phép truy cập ở bất kỳ chế độ nào không có miền

kiểm tra sự cho phép.

(m4): Nó cho phép MMU dịch địa chỉ.

Sau các bước này, mọi địa chỉ ảo (VA) sẽ được ánh xạ tới địa chỉ vật lý (PA). Trong trường hợp này, cả hai địa chỉ đều là

tương tự do ánh xạ nhận dạng. Các phần còn lại của mã ts.s thực hiện như sau. Chương trình chạy ở chế độ SVC nhưng

nó có thể vào chế độ IRQ để xử lý các ngắt IRQ. Nó cũng có thể vào chế độ hủy bỏ dữ liệu để xử lý các trường hợp ngoại lệ hủy bỏ dữ liệu. Do đó, nó

khởi tạo con trỏ ngăn xếp cho các chế độ khác nhau. Sau đó, nó gọi main() ở chế độ SVC có bật ngắt IRQ.

(2). Tệp tc:

/************** tc file của Chương trình C6.1 ****************/

#include "type.h"

#include "string.c"

#include "uart.c"

#include "kbd.c"
Machine Translated by Google

6.8 Chương trình ví dụ về quản lý bộ nhớ 181

#include "timer.c"

#include "vid.c"

#include "ngoại lệ.c"

/* thiết lập MMU sử dụng các phần 1 MB để ánh xạ ID VA sang PA */

// Versatilepb: RAM 256 MB + phần I/O 2 MB ở mức 256 MB

/*************** Mô tả phần L1 ****************

|31 20|19 12|- -|9|8765|4|32|10| | phần addr |

|AP |0|DOMN|1|CB|10| | |000000000|

01 |0|0000|1|00|10| = 0x412 | |

KRW dom0

**************************************************** ***/

int mkptable() // xây dựng pgtable cấp 1 sử dụng các phần 1 MB

int i, pentry, *ptable; printf("1.

build pgtable cấp 1 ở 16 KB\n"); ptable = (int *)0x4000; for (i = 0; i <

4096; i ++){ // loại bỏ pgtable

ptable[i] = 0;

printf("2. điền 258 mục của pgtable vào bản đồ ID 258 MB VA tới PA\n");

phòng đựng thức ăn = 0x412; // AP = 01,domain = 0000, CB = 00, type = 02 cho phần

for (i = 0; i < 258; i ++){ // 258 mục trong bảng trang cấp 1

ptable[i] = pentry;

phòng đựng thức ăn += 0x100000;

printf("3. đã hoàn thành việc xây dựng bảng trang cấp 1\n");

printf("4. quay lại đặt TTB, tên miền và bật MMU\n");

int data_chandler() //trình xử lý hủy dữ liệu

u32 error_status, error_addr, tên miền, trạng thái;

int spsr = get_spsr();

printf("data_abort ngoại lệ trong "); if ((spsr & 0x1F)

==0x13) printf("Chế độ SVC\n");

lỗi_status = get_fault_status();

lỗi_addr = get_fault_addr();

tên miền = (fault_status & 0xF0) 4;

trạng thái = error_status & 0xF;

printf("trạng thái = %x: miền = %x trạng thái = %x (0x5 = Chuyển không hợp lệ)\n",

error_status, tên miền, trạng thái);

printf("VA addr = %x\n", error_addr);

int copy_vector_table(){

u32 *vectors_src = &vectors_start;

u32 *vectors_dst = (u32 *)0;

while(vectors_src < &vectors_end)

*vector_dst ++ = *vector_src ++;

int irq_chandler() // bộ xử lý ngắt IRQ

// đọc các thanh ghi trạng thái VIC, SIC để tìm ra ngắt nào

int vicstatus = VIC_STATUS;

int sicstatus = SIC_STATUS;

nếu (vicstatus & (1 << 4))


Machine Translated by Google

182 6 Quản lý bộ nhớ trong ARM

hẹn giờ0_handler();

nếu (vicstatus & (1 << 12))

uart0_handler();

if (vicstatus & (1 << 31) && sicstatus & (1 << 3))

kbd_handler();

int chính()

int tôi, *p;

dòng char[128];

kbd_init();

uart_init();

VIC_INTENABLE | = (1 << 4); // hẹn giờ0 lúc 4 giờ

VIC_INTENABLE | = (1 << 12); // UART0 lúc 12 giờ

VIC_INTENABLE = 1 << 31; // SIC tới IRQ31 của VIC

UART0_IMSC = 1 << 4; // kích hoạt ngắt UART RX

SIC_ENSET = 1 << 3; // KBD int = 3 trên SIC

SIC_PICENSET = 1 << 3; // KBD int = 3 trên SIC

kbd-> control = 1 << 4;

bộ đếm thời gian_init(); bộ đếm thời gian_start(0);

printf("kiểm tra khả năng bảo vệ của MMU: thử truy cập VA = 0x00200000\n");

p = (int *)0x002000000; *p = 123;

printf("kiểm tra khả năng bảo vệ của MMU: thử truy cập VA = 0x02000000\n");

p = (int *)0x020000000; *p = 123;

printf("kiểm tra khả năng bảo vệ của MMU: thử truy cập VA = 0x20000000\n"); p = (int *)0x20000000; *p

= 123;

trong khi(1){

printf("chạy chính Nhập một dòng: "); kgets(dòng);

printf("line =

%s\n", line);

Tệp tc chứa chức năng chính của chương trình. Đầu tiên nó khởi tạo trình điều khiển thiết bị và bộ xử lý ngắt IRQ.
Sau đó, nó thể hiện khả năng bảo vệ bộ nhớ bằng cách cố gắng truy cập các địa chỉ ảo không hợp lệ, tạo ra các ngoại lệ data_abort.
Trong trình xử lý hủy bỏ dữ liệu, data_chandler(), nó đọc thanh ghi lỗi dữ liệu c5 và thanh ghi địa chỉ lỗi c6 của MMU để hiển thị
lý do ngoại lệ (tên miền không hợp lệ) cũng như VA gây ra ngoại lệ. Cần lưu ý rằng khi xảy ra ngoại lệ hủy bỏ dữ liệu, PC-8 sẽ trỏ
tới lệnh gây ra ngoại lệ. Trong trình xử lý hủy bỏ dữ liệu, nếu chúng ta điều chỉnh thanh ghi liên kết thêm 8, nó sẽ quay trở
lại cùng một lệnh sai, dẫn đến một vòng lặp vô hạn. Vì lý do này, chúng tôi điều chỉnh PC trả về 4 để cho phép tiếp tục thực thi.

(4). Tệp tập lệnh liên kết và mk t.ld: Đây là tập lệnh liên kết tiêu chuẩn. Nó xác định điểm vào của chương trình và phân bổ các
vùng bộ nhớ dưới dạng ngăn xếp chế độ đặc
quyền (5) Lệnh liên kết biên dịch: Đây là tập lệnh sh được sử dụng để (chéo) biên dịch liên kết các tệp .s và.c. Địa chỉ ảo bắt
đầu của chương trình là 0x10000.

arm-none-eabi-as -mcpu = arm926ej-s -g ts.s -o ts.o arm-none-eabi-gcc -c -mcpu

= arm926ej-s -g tc -o to

arm-none-eabi-ld -T t.ld ts.o to –Ttext = 0x10000 -o t.elf

arm-none-eabi-objcopy -O nhị phân t.elf t.bin


Machine Translated by Google

6.8 Chương trình ví dụ về quản lý bộ nhớ 183

(6). Chạy chương trình theo QEMU

qemu-system-arm -M multiplenpb -m 256 M -kernel t.bin -serial mon:stdio

(7). Kết quả đầu ra mẫu: Hình 6.7 cho thấy kết quả đầu ra mẫu khi chạy chương trình C6.1. Như hình minh họa, việc cố gắng truy cập bất

kỳ VA không hợp lệ nào sẽ tạo ra ngoại lệ hủy bỏ dữ liệu.

6.8.2 Phân trang hai cấp sử dụng trang 4 KB

Chương trình ví dụ MMU thứ hai, C6.2, sử dụng phân trang 2 cấp độ. Nó bao gồm các thành phần sau.

1. Tệp ts.s: Tệp này giống hệt tệp ts của Chương trình C6.1. Trong quá trình khởi động, nó gọi mkptable() trong C để thiết lập bảng

trang hai cấp. Sau đó, nó đặt các bit quyền truy cập TTB và miền và kích hoạt MMU. Sau đó nó gọi main() ở chế độ SVC. 2. Tệp tc: Tệp này

giống với tệp tc của Chương trình C6.1, ngoại trừ hàm mkptable() đã được sửa đổi. Thay vì xây dựng bảng trang cấp 1 sử dụng các phần 1

MB, nó xây dựng bảng trang cấp 1 và các bảng trang cấp 2 liên quan cho phân trang 2 cấp. Để cho ngắn gọn, chúng tôi chỉ hiển thị hàm

mkptable() đã sửa đổi.

int mkptable() // xây dựng pgtable 2 cấp cho phân trang 2 cấp

int i, j, pentry, *ptable, *pgtable, paddr; printf("Chào mừng đến

với Wanix trong ARM\n"); ptable = (int *)0x4000; phòng

đựng thức ăn = 0x412; printf("1. build

pgtable cấp 1 ở 16 KB để

ánh xạ 258 MB VA tới PA\n");

for (i = 0; i < 4096; i ++){ // loại bỏ 4096 mục

ptable[i] = 0;

printf("2. điền vào 258 mục trong pgdir cấp 1 với 258 pgtables\n");

for (i = 0; i < 258; i ++){ // ĐÁNH GIÁ 256 MB RAM + 2 MB dung lượng I/O ở mức 256 MB

Hình 6.7 Quản lý bộ nhớ sử dụng các phần 1 MB


Machine Translated by Google

184 6 Quản lý bộ nhớ trong ARM

ptable[i] = (0x500000 + i*1024) | 0x11; // tên miền = 0,CB = 00,loại = 01

printf("3. build 258 pgtable cấp 2 với dung lượng 5 MB\n"); vì (i = 0; i <

258; i ++){ // 258 bảng trang

pgtable = (u32 *)((u32)0x500000 + (u32)i*1024); paddr = i*0x100000 | 0x55E; //

tất cả AP = 01|01|01|01|CB = 11|type = 10 for (j = 0; j < 256; j ++){ pgtable[j] = paddr + j*4096; // tăng thêm 4 KB

// 256 mục, mỗi mục trỏ tới 4 KB PA

printf("4. đã hoàn thành việc xây dựng Bảng Trang hai cấp\n"); printf("5. quay lại

hội để đặt TTB, tên miền và bật MMU\n");

Hình 6.8 cho thấy kết quả đầu ra mẫu khi chạy chương trình C6.2, thể hiện phân trang hai cấp độ. Khi chương trình bắt
đầu, đầu tiên nó sẽ xây dựng các bảng trang hai cấp độ trong 5 bước. Sau đó, nó kiểm tra khả năng bảo vệ bộ nhớ bằng cách
thử truy cập vào một số vị trí VA. Như hình minh họa, các nỗ lực truy cập VA = 0x00200000 (2 MB) và VA = 0x02000000 (16 MB),
không gây ra bất kỳ ngoại lệ hủy bỏ dữ liệu nào vì cả hai đều nằm trong không gian VA 258 MB của hạt nhân. Tuy nhiên, đối
với VA = 0xA0000000, nó tạo ra ngoại lệ data_abort vì VA nằm ngoài khoảng trống VA 258 MB của hạt nhân.

6.8.3 Phân trang một cấp với không gian VA cao

Chương trình MMU thứ ba, C6.3, sử dụng các phần 1 MB để ánh xạ không gian địa chỉ ảo thành 0x80000000 (2 GB). Chương trình
sẽ được QEMU tải tới địa chỉ vật lý 0x10000. Nó được liên kết biên dịch với địa chỉ ảo bắt đầu 0x80010000.
Do chương trình được biên dịch bằng địa chỉ ảo nên chúng ta không thể gọi bất kỳ hàm nào trong mã C của chương trình trước
khi thiết lập bảng trang và cho phép MMU ánh xạ VA tới PA. Vì lý do này, bảng trang ban đầu phải được xây dựng bằng mã hợp
ngữ trong quá trình đặt lại trong khi MMU tắt. Khi chương trình bắt đầu, trước tiên chúng tôi thiết lập bảng trang ban đầu
để ánh xạ ID 1 MB VA thấp nhất tới PA. Điều này là do bảng vectơ nằm ở địa chỉ vật lý 0 và điểm vào của trình xử lý ngoại lệ
nằm trong phạm vi 4 KB tính từ bảng vectơ. Ngoài ánh xạ ID mức thấp 1 MB, chúng tôi cũng điền các mục trong bảng trang 2048–
2295 để ánh xạ không gian địa chỉ ảo VA = (0x80000000, 0x80000000 + 258 MB) tới mức thấp 258 MB PA. Tiếp theo, chúng tôi cho
phép MMU bắt đầu dịch địa chỉ VA sang PA. Sau đó, chúng tôi gọi main() trong C bằng cách sử dụng VA của nó tại 0x80000000 + main.
Vì toàn bộ chương trình nằm trong bộ nhớ vật lý 1 MB thấp nhất nên chúng ta cũng có thể gọi hàm main() bằng PA của nó. Sau
đây liệt kê mã lắp ráp.

Hình 6.8 Quản lý bộ nhớ bằng phân trang 2 cấp


Machine Translated by Google

6.8 Chương trình ví dụ về quản lý bộ nhớ 185

1. tập tin ts.s:

// ———————————————————— file ts.s của Chương trình C6.3 ————————————————————— —————

.chữ

.code 32

.global reset_handler, vectơ_start, vectơ_end

.global get_fault_status, get_fault_addr

reset_handler: // điểm vào

// Versatilepb: RAM 256 MB, 2 phần I/O 1 MB ở mức 256 MB

// xóa ptable ở 0x4000 (16 KB) về 0

mov r0, #0x4000 // ptable ở 0x4000 = 16 KB

mov r1, #4096 // 4096 mục

chuyển r2, #0 // điền tất cả bằng 0

1:

str r2, [r0], #4 // lưu r3 vào [r0]; tăng r0 lên 4

phụ r1, r1, #1 // r1–; đặt cờ điều kiện

bgt 1b // vòng lặp r1 = 4096 lần

//(m1): ptable[0] Bản đồ ID thấp 1 MB VA đến PA

// ptable[2048-2295] ánh xạ VA = [2 GB,2 GB + 258 MB] xuống mức thấp 258 MB PA

mov r0, #0x4000

chuyển động r1, r0

thêm r1, r1, #(2048*4) // mục nhập 2048 trong ptable[ ]

mov r2, #256 // r2 = 256

cộng r2, r2, #2 // r2 = 258 mục

mov r3, #0x100000 // r3 = 1 M gia số

mov r4, #0x400 // r4 = AP = 01 (KRW, số người dùng) AP = 11: cả KU r/w

hoặc r4, r4, #0x12 // r4 = 0x412 (HOẶC 0xC12 nếu AP = 11)

str r4, [r0] // ptable[0] = 0x412

// ptable[2048-2257] ánh xạ tới mức thấp 258 MB PA

2:

str r4, [r1], #4 // lưu r4 vào [r1]; tăng r1 lên 4

thêm r4, r4, r3 // tăng r4 thêm 1 M

phụ r2,r2, #1 // r2–

bgt 2b // vòng lặp r2 = 258 lần

//(m2): đặt thanh ghi TTB trỏ vào pgtable ở 0x4000

mov r0, #0x4000

mcr p15, 0, r0, c2, c0, 0 // đặt TTBR với PA = 0x4000

mcr p15, 0, r0, c8, c7, 0 // tuôn ra TLB

//(m3): đặt miền0: 01 = client(kiểm tra quyền)11 = người quản lý(không kiểm tra)

chuyển r0, #0x1 // b01 cho KHÁCH HÀNG

mcr p15, 0, r0, c3, c0, 0 // ghi vào miền REG c3

//(m4): bật MMU

chuyển r0, #0x1

mrc p15, 0, r0, c1, c0, 0 // đọc điều khiển REG tới r0

hoặc r0, r0, #0x00000001 // đặt bit0 của r0 thành 1

mcr p15, 0, r0, c1, c0, 0 // ghi vào điều khiển REG c1 ==> MMU bật

không

không

không

mrc p15, 0, r2, c2, c0, 0 // đọc reg cơ sở TLB c2 vào r2

chuyển động r2, r2 // thời gian cho phép MMU kết thúc

// đặt ngăn xếp SVC thành CAO CẤP của svc_stack[ ]

LDR r0, = svc_stack // r0 điểm svc_stack[]


Machine Translated by Google

186 6 Quản lý bộ nhớ trong ARM

THÊM r1, r0, #4096 // r1 -> cao cấp của svc_stack[]

MOV sp, r1

// thiết lập ngăn xếp IRQ và kích hoạt các ngắt IRQ

MSR cpsr, #0x92 // ghi vào cspr

ldr sp, = irq_stack // u32 irq_stack[1024] trong tc

thêm sp, sp, #4096 // đảm bảo đó là VA từ 2 GB

// đặt ngăn xếp ABT */

MSR cpsr, #0x97

LDR sp, = abt_stack_top

// quay lại chế độ SVC

MSR cpsr, #0x93 // Chế độ SVC tắt IRQ

BL copy_vector_table // sao chép bảng vectơ sang PA 0

MSR cpsr, #0x13 // Chế độ SVC có bật IRQ

BL chính // gọi hàm main() trong C


B .

data_handler:

sublr, lr, #4

stmfd sp!, {r0-r12, lr} // lưu regs vào abt_stack

bldata_chandler // gọi data_chandler trong C

ldmfd sp!, {r0-r12, pc}^ // bật abt_stack và quay lại

irq_handler: // IRQ ngắt điểm vào

sublr, lr, #

stmfd sp!, {r0-r12, lr} // lưu regs vào irq_stack

bl irq_chandler // gọi irq_chandler() trong C

ldmfd sp!, {r0-r12, pc}^ // bật regs và return

được: // trả về sp hiện tại

mov r0, sp

di chuyển máy tính, lr

svc_entry: // mục nhập SVC giả, chưa được sử dụng

vectơ_start: // bảng vectơ

PC LDR, reset_handler_addr

Máy tính LDR, undef_handler_addr

Máy tính LDR, svc_handler_addr

PC LDR, tìm nạp trước_abort_handler_addr

PC LDR, dữ liệu_abort_handler_addr

B .

Máy tính LDR, irq_handler_addr

PC LDR, fiq_handler_addr

đặt lại_handler_addr: .word reset_handler

undef_handler_addr: .word undef_handler

svc_handler_addr: .word svc_entry

prefetch_abort_handler_addr: .word prefetch_abort_handler

dữ liệu_abort_handler_addr: .word data_handler

irq_handler_addr: .word irq_handler

fiq_handler_addr: .word fiq_handler

vectơ_end:

// các chức năng tiện ích khác: không hiển thị

//——————————————— cuối tập tin ts.s ——————————————————

Như trước đây, chúng tôi tập trung vào mã thiết lập MMU, được gắn nhãn từ (m1) đến (m4).

(m1): Khi vào, trước tiên nó sẽ thiết lập bảng trang cấp 1 ở 0x4000 (16 KB) sử dụng các phần 1 MB. Mục 0 của bảng trang,
ptable [0], được sử dụng để ánh xạ ID 1 MB thấp nhất của VA tới PA, được yêu cầu bởi bảng vectơ. Sau đó nó điền vào bảng trang
các mục ptable [2048] đến ptable [2048 + 258] với các bộ mô tả phần, ánh xạ VA = (0x80000000, 0x80000000
Machine Translated by Google

6.8 Chương trình ví dụ về quản lý bộ nhớ 187

+ 258 MB) xuống mức thấp 258 MB PA. Trường thuộc tính của mỗi bộ mô tả phần được đặt thành AP = 01 (máy khách), miền =
0000, CB = 00 (tắt bộ đệm D và bộ đệm W) và loại = 10 (phần). (m2): Sau khi thiết
lập bảng trang cấp 1, nó đặt TTBR cho bảng trang ở mức 0x4000. (m3): Nó đặt các bit truy
cập của miền0 thành 01 cho chế độ máy khách. (m4): Sau đó, nó
kích hoạt MMU để dịch VA sang PA.

Khi bật MMU, giờ đây nó có thể gọi các hàm trong C, được biên dịch bằng các địa chỉ ảo bắt đầu từ 0x80010000. Nó
thiết lập ngăn xếp cho các chế độ khác nhau, sao chép bảng vectơ sang địa chỉ 0 và gọi main() ở chế độ SVC.

2. Tệp tc:

/*************** file tc của chương trình C6.3 *************/

#include "type.h"

#include "string.c"

#include "uart.c"

#include "kbd.c"

#include "timer.c"

#include "vid.c"

#include "Exceptions.c" int

irq_chandler() { // giống như trong Chương trình C6.2 } int copy_vector_table(){ //

giống như trong Chương trình C6.2 }

extern int reset_handler();

int svc_stack[1024], irq_stack[1024]; // Ngăn xếp SVC và IRQ trong VA

int g; // biến toàn cục để hiển thị VA

int chính()

int a, sp, *p;

dòng char[128];

fbuf_init();

kbd_init();

uart_init();

bộ đếm thời gian_init(); bộ đếm thời gian_start(0);

// kích hoạt ngắt IRQ của thiết bị: giống như trước

printf("Chào mừng đến với WANIX in Arm\n");

printf("Trình diễn các phần một cấp VA = 0x80000000(2G)\n");

printf("chính chạy ở VA = %x sử dụng các phần 1 MB cấp 1\n", main);

printf("reset_handler = %x\n", reset_handler); printf("data_chandler = %x\n",

data_chandler); printf("Con trỏ ngăn xếp SVC = %x\n", getsp()); printf("Biến

toàn cục g tại %x\n", &g); printf("biến cục bộ a tại %x\n", &a); printf("kiểm

tra khả năng bảo vệ MMU: thử truy cập VA = 0x80200000\n"); p =

(int *)(0x80200000); một = *p; printf("kiểm tra khả năng bảo vệ

của MMU: thử truy cập VA = 0x200000\n");

p = (int *)0x200000; một = *p;

printf("kiểm tra khả năng bảo vệ của MMU: thử truy cập VA = 0xA0000000\n");

p = (int *)0xA0000000; một = *p;

trong khi(1){

printf("nhập một dòng tại %x: ", line);

kgets(dòng);

printf("line = %s\n", line);

}
Machine Translated by Google

188 6 Quản lý bộ nhớ trong ARM

Giải thích về mã tc: Trong main(), chúng tôi hiển thị VA của một số hàm và biến để cho thấy rằng chúng nằm trong phạm
vi địa chỉ ảo trên 2 GB (0x80000000). Sau đó, chúng tôi xác minh cơ chế bảo vệ bộ nhớ của MMU bằng cách cố gắng truy
cập một số VA không hợp lệ, điều này sẽ tạo ra các ngoại lệ hủy bỏ dữ liệu. Trong data_abort_handler(), chúng tôi đọc và
hiển thị các thanh ghi error_status và error_addr của MMU để hiển thị lý do cũng như VA không hợp lệ gây ra ngoại lệ.

3. Bắt đầu địa chỉ ảo: Để sử dụng địa chỉ ảo bắt đầu từ 0x80000000, các lệnh liên kết biên dịch được sửa đổi thành

arm-none-eabi-as -mcpu = arm926ej-s ts.s -o ts.o


arm-none-eabi-gcc -c -mcpu = arm926ej-s tc -o tới
arm-none-eabi-ld -T t.ld vector.o ts.o to -Ttext = 0x80010000 -o t.elf

4. Sửa đổi khác: Với VA bắt đầu = 0x80000000, địa chỉ cơ sở của tất cả các thiết bị I/O phải được thay đổi thành địa
chỉ ảo. Những việc này được thực hiện bởi macro VA(x)
#define VA(x) (0x80000000 + (u32)x)

thêm 0x80000000 vào địa chỉ cơ sở của chúng trong bản đồ bộ nhớ.

5. Kết quả đầu ra mẫu của chương trình C6.3: Hình 6.9 thể hiện kết quả đầu ra mẫu khi chạy chương trình C6.3. Như
hình minh họa, phạm vi VA trên 0x80000000 và mọi nỗ lực truy cập VA không hợp lệ sẽ tạo ra ngoại lệ data_abort.

Hình 6.9 Trình diễn phân trang một cấp với không gian VA cao
Machine Translated by Google

6.8 Chương trình ví dụ về quản lý bộ nhớ 189

6.8.4 Phân trang hai cấp với không gian VA cao

Chương trình mẫu C6.4 sử dụng phân trang 2 cấp với không gian địa chỉ ảo bắt đầu từ 0x80000000 (2 GB). Vì chương trình sẽ được

tải tại địa chỉ vật lý 0x10000 và chạy từ đó nên nó được biên dịch liên kết với VA = 0x80010000 bắt đầu. Tương tự như Chương

trình C6.3, chúng ta phải thiết lập bảng trang và kích hoạt MMU trong mã hợp ngữ khi chương trình khởi động. Vì việc xây dựng các
bảng trang trong tập hợp rất tẻ nhạt nên chúng ta sẽ thực hiện việc đó theo hai bước riêng biệt. Ở bước đầu tiên, chúng tôi thiết

lập bảng trang một cấp ban đầu sử dụng các phần 1 MB để ánh xạ trong phạm vi VA = (2 GB, 2 GB + 258 MB) giống hệt như trong Chương

trình C6.3. Sau khi kích hoạt MMU để dịch địa chỉ, chúng tôi gọi một hàm trong C để xây dựng bảng trang cấp 1 mới (pgdir) có kích
thước 32 KB và các bảng trang cấp 2 liên quan của nó có kích thước 5 MB sử dụng các trang nhỏ 4 KB. Sau đó, chúng ta chuyển TTB

sang bảng trang cấp 1 mới và xóa TLB, từ đó chuyển MMU từ phân trang một cấp sang phân trang hai cấp. Sau đây là mã của chương

trình ví dụ C6.4.

(1). Tệp ts.s: Tệp ts.s giống với tệp của Chương trình C6.3, ngoại trừ chức năng switchPgdir được bổ sung, được liệt kê bên dưới.

switchPgdir: // chuyển pgdir sang pgdir mới được truyền trong r0

mcr p15, 0, r0, c2, c0, 0 // đặt TTBase thành C2

chuyển r1, #0

mcr p15, 0, r1, c8, c7, 0 // xóa TLB

mcr p15, 0, r1, c7, c10, 0 // xóa bộ đệm I và D

mrc p15, 0, r2, c2, c0, 0 // đọc TLB cơ sở reg C2

// đặt miền0: 01 = client (kiểm tra quyền) 11 = người quản lý (không kiểm tra)

chuyển r0, #0x1 // b01 cho KHÁCH HÀNG

mcr p15, 0, r0, c3, c0, 0 mov pc, lr // ghi vào miền reg C3

// trở lại

(2). Tệp tc: Tệp tc giống với tệp của Chương trình C6.3, ngoại trừ hàm mkPtable() được thêm vào, tạo ra bảng trang cấp 1 mới
(pgdir) ở 32 KB và các bảng trang cấp 2 liên quan của nó ở 5 MB. Sau đó, nó chuyển TTB sang pgdir mới để cho MMU sử dụng phân trang

2 cấp. Việc lựa chọn pgdir mới ở mức 32 KB và bảng trang cấp 2 ở mức 5 MB là khá tùy ý. Chúng có thể được xây dựng ở bất cứ đâu có

bộ nhớ vật lý.

int mkPtable()

int i, j, paddr, *pgdir, *pgtable

printf("1.xây dựng bảng trang hai cấp độ 32 KB\n");

pgdir = (int *)VA(0x8000); // 0x80000000

for (i = 0; i < 4096; i ++){ // loại bỏ 4096 mục

pgdir[i] = 0;

// xây dựng pgtable MỚI với dung lượng 5 MB

printf("2. điền vào 258 mục trong pgdir cấp 1 với 258 pgtables\n"); for (i = 0; i < 258; i ++){ // ĐÁNH

GIÁ 256 MB RAM; 2 phần I/O

pgdir[i + 2048] = (int)(0x500000 + i*1024) | 0x11; // thuộc tính mô tả = 0x11: DOMAIN

= 0,CB = 00,type = 01

printf("3. build 258 pgtable cấp 2 với dung lượng 5 MB\n");

vì (i = 0; i < 258; i ++){

pgtable = (int *)(VA(0x500000) + (int)i*1024); paddr = i*0x100000 | 0x55E; //

AP = 01|01|01|01|CB = 11|type = 10 for (j = 0; j < 256; j ++){ // 256 mục, mỗi mục trỏ tới 4 KB PA
Machine Translated by Google

190 6 Quản lý bộ nhớ trong ARM

Hình 6.10 Trình diễn phân trang 2 cấp với VA cao

pgtable[j] = paddr + j*4096; // tăng thêm 4 KB


}
}
pgdir[0] = pgdir[2048]; // pgdir[0] và pgdir[2048]- > thấp 1 MB PA printf("4. chuyển sang pgdir ở 0x8000

(32 KB) …. "); switchPgdir(0x8000);

printf("switchPgdir OK\n");
}

(3). Đầu ra mẫu của Chương trình C6.4: Hình 6.10 cho thấy đầu ra mẫu khi chạy chương trình C6.4.

6.9 Tóm tắt

Chương này đề cập đến đơn vị quản lý bộ nhớ ARM (MMU) và ánh xạ không gian địa chỉ ảo. Nó trình bày chi tiết về ARM MMU và hiển thị cách

định cấu hình MMU để ánh xạ địa chỉ ảo bằng cách sử dụng cả phân trang một cấp và hai cấp. Ngoài ra, nó cũng cho thấy sự khác biệt giữa ánh xạ

không gian VA thấp và ánh xạ không gian VA cao. Thay vì chỉ thảo luận về các nguyên tắc, nó trình bày các loại ánh xạ địa chỉ ảo khác nhau bằng

ví dụ hoạt động hoàn chỉnh.

các chương trình.

Danh sách các chương trình mẫu

C6.1: Phân trang một cấp sử dụng các phần 1 MB với VA được ánh xạ thấp C6.2:

Phân trang hai cấp sử dụng các trang 4 KB với VA được ánh xạ thấp
Machine Translated by Google

6.9 Tóm tắt 191

C6.3: Phân trang một cấp sử dụng các phần 1 MB với VA được ánh xạ cao C6.4:

Phân trang hai cấp sử dụng các trang 4 KB với VA được ánh xạ cao

Các vấn đề

1. Trong ví dụ C6.1, bảng trang cấp 1 được xây dựng bởi hàm mkptable() trong C.

(1). Tại sao có thể xây dựng bảng trang trong C?

(2). Triển khai hàm mkptable() trong mã hợp ngữ.

2. Chương trình ví dụ C6.2 thực hiện phân trang 2 cấp sử dụng các trang nhỏ 4 KB. Sửa đổi nó để triển khai phân trang 2 cấp bằng

cách sử dụng các trang lớn 64 KB.

3. Trong chương trình ví dụ C6.3, ánh xạ không gian VA tới 2 GB, bảng trang được xây dựng bằng mã hợp ngữ, thay vì bằng C, khi hệ

thống khởi động.

(1). Tại sao cần xây dựng bảng trang bằng mã hợp ngữ? (2).

4. Trong chương trình ví dụ C6.4, sử dụng phân trang 2 cấp độ để ánh xạ VA tới 2 GB, các bảng trang được xây dựng theo hai giai đoạn, tất cả đều

ở C. Ngoài ra, các bảng trang cũng có thể được xây dựng trong một bước duy nhất, tất cả đều có mã lắp ráp.

(1). Cố gắng xây dựng các bảng trang bằng mã hợp ngữ trong một bước. So sánh số lượng nỗ lực lập trình cần thiết trong cả hai phương

pháp.
(2). Hàm mkPtable() của C6.4 chứa các dòng mã

pgdir = (int *)VA(0x8000); // pgdir ở mức 32 KB

thiết lập bảng trang cấp 1 ở mức 32 KB bộ nhớ vật lý và

pgdir[i + 2048] = (int)(0x500000 + i*1024) | 0x11;

lấp đầy các bộ mô tả trang cấp 1 bằng các khung trang bắt đầu từ 5 MB bộ nhớ vật lý. Trong khi dòng mã đầu tiên sử dụng VA thì dòng
mã thứ hai sử dụng PA.

Tại sao lại có sự khác biệt?

Thẩm quyền giải quyết

ARM MMU: ARM926EJ-S, Hướng dẫn tham khảo kỹ thuật ARM946E-S, Trung tâm thông tin ARM 2008.
Machine Translated by Google

Chế độ người dùng Quy trình và cuộc gọi hệ thống


7

7.1 Quy trình chế độ người dùng

Trong chương. 5, chúng tôi đã phát triển một hạt nhân bộ xử lý đơn giản để quản lý quy trình. Hạt nhân đơn giản hỗ trợ tạo quy

trình động, đồng bộ hóa quy trình và giao tiếp quy trình. Nó có thể được sử dụng làm mô hình cho nhiều hệ thống nhúng đơn giản. Một

hệ thống nhúng đơn giản bao gồm một số tiến trình cố định, tất cả đều thực thi trong cùng một không gian địa chỉ của kernel. Hệ

thống có thể được triển khai theo hướng sự kiện, với các quy trình là các thực thể thực thi. Các sự kiện có thể bị gián đoạn từ các

thiết bị phần cứng, quá trình hợp tác thông qua các tín hiệu hoặc thông báo từ các quy trình khác. Nhược điểm của loại hệ thống này

là thiếu khả năng bảo vệ bộ nhớ. Một quy trình được thiết kế kém hoặc gặp trục trặc có thể làm hỏng không gian địa chỉ dùng chung,

khiến các quy trình khác không thành công. Vì lý do độ tin cậy và bảo mật, mỗi quy trình phải chạy trong không gian địa chỉ ảo riêng

tư được cách ly và bảo vệ khỏi các quy trình khác. Để hỗ trợ các quy trình có không gian địa chỉ ảo, cần sử dụng phần cứng quản lý

bộ nhớ để cung cấp cả ánh xạ địa chỉ ảo và bảo vệ bộ nhớ. Trong chương. Trong phần 6, chúng tôi đã thảo luận chi tiết về Đơn vị

quản lý bộ nhớ ARM (MMU) (ARM MMU 2008) và chỉ ra cách định cấu hình MMU để ánh xạ địa chỉ ảo. Trong chương này, chúng ta sẽ mở

rộng kernel đơn giản để hỗ trợ các tiến trình ở chế độ người dùng. Trong kernel mở rộng, mỗi tiến trình có thể thực thi ở hai chế

độ khác nhau, chế độ kernel và chế độ người dùng. Khi ở chế độ kernel, tất cả các tiến trình đều thực thi trong cùng một không gian

địa chỉ của kernel, điều này không có quyền ưu tiên. Khi ở chế độ người dùng, mỗi quy trình sẽ thực thi trong một không gian địa

chỉ ảo riêng tư và có thể giành được quyền ưu tiên. Các quy trình ở chế độ người dùng có thể vào kernel thông qua các ngoại lệ,

ngắt và lệnh gọi hệ thống. Cuộc gọi hệ thống là một cơ chế cho phép các tiến trình chế độ người dùng vào chế độ kernel để thực thi

các chức năng kernel. Sau khi thực hiện chức năng gọi hệ thống trong kernel, quy trình sẽ quay trở lại chế độ người dùng (ngoại trừ

lối thoát không bao giờ quay lại) với kết quả mong muốn. Để đơn giản, trước tiên chúng ta sẽ bỏ qua các ngoại lệ và tập trung vào

phát triển kernel để hỗ trợ các quy trình chế độ người dùng và lệnh gọi hệ thống.

7.2 Ánh xạ không gian địa chỉ ảo

Khi hệ thống nhúng khởi động, nhân hệ thống thường được tải xuống cấp thấp nhất của bộ nhớ vật lý, ví dụ: tới địa chỉ vật lý 0 hoặc

16 KB như trong trường hợp máy ảo ARM Versatilepb trong QEMU. Khi kernel khởi động, trước tiên nó sẽ cấu hình Đơn vị quản lý bộ

nhớ (MMU) để cho phép dịch địa chỉ ảo. Mỗi quy trình có thể chạy ở hai chế độ khác nhau, chế độ Kernel và chế độ Người dùng, mỗi

chế độ có một không gian địa chỉ ảo riêng biệt. Với địa chỉ 32 bit, tổng phạm vi không gian VA là 4 GB. Chúng tôi có thể chia đều

không gian VA 4 GB thành hai nửa bằng nhau và chỉ định cho mỗi chế độ một phạm vi không gian VA là 2 GB. Có hai cách để tạo không

gian VA ở chế độ Kernel và User. Trong sơ đồ Kernel Mapped Low (KML), không gian VA của chế độ Kernel được ánh xạ tới các địa chỉ

ảo thấp và không gian VA ở chế độ người dùng được ánh xạ tới các địa chỉ ảo cao. Trong trường hợp này, ánh xạ Kernel VA tới PA

thường là ánh xạ một-một hoặc ánh xạ nhận dạng, sao cho mọi VA đều giống với PA. Không gian VA ở chế độ người dùng được ánh xạ tới

dải địa chỉ ảo cao từ 0x80000000 (2 GB) trở lên. Trong sơ đồ Kernel Mapped High (KMH), ánh xạ địa chỉ VA bị đảo ngược. Trong trường

hợp này, không gian VA của chế độ kernel được ánh xạ tới các địa chỉ ảo cao và không gian VA của chế độ người dùng được ánh xạ tới

các địa chỉ ảo thấp. Từ quan điểm bảo vệ bộ nhớ, không có sự khác biệt giữa hai sơ đồ ánh xạ. Tuy nhiên, từ quan điểm lập trình, có

thể có một số khác biệt đáng kể. Ví dụ: trong sơ đồ KML, hạt nhân có thể được liên kết biên dịch với các địa chỉ thực. Khi kernel

khởi động, nó có thể thực thi trực tiếp ở chế độ địa chỉ thực mà không cần cấu hình MMU để dịch địa chỉ trước. Ngược lại, trong sơ

đồ KMH, hạt nhân phải được liên kết biên dịch với các địa chỉ ảo. Khi kernel khởi động, nó không thể

© Springer International Publishing AG 2017 193


KC Wang, Hệ điều hành nhúng và thời gian thực, DOI
10.1007/978-3-319-51517-5_7
Machine Translated by Google

194 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

thực thi bất kỳ mã nào sử dụng địa chỉ ảo trực tiếp. Trong trường hợp này, trước tiên nó phải cấu hình MMU để sử dụng địa chỉ ảo.
Kiến trúc ARM không hỗ trợ khái niệm bảng vectơ nổi, cho phép bảng vectơ được ánh xạ lại vào bất kỳ bộ nhớ vật lý nào. Trên một
số máy ARM, bảng vectơ chỉ có thể được ánh xạ lại thành 0xFFFF0000 trong khi khởi động.
Nếu không ánh xạ lại vectơ, bảng vectơ phải được đặt ở địa chỉ vật lý 0. Trong bảng vectơ, các lệnh Nhánh hoặc LDR có giới hạn
phạm vi địa chỉ là 4 KB. Điều này ngụ ý rằng cả bảng vectơ và các điểm vào trình xử lý ngoại lệ đều phải nằm trong bộ nhớ vật lý
4 KB thấp nhất. Vì những lý do này, chúng tôi chủ yếu sẽ sử dụng lược đồ KML vì nó tự nhiên và đơn giản hơn. Tuy nhiên, chúng
tôi cũng sẽ chỉ ra cách sử dụng lược đồ KMH và chứng minh sự khác biệt của nó với lược đồ KML bằng các hệ thống mẫu.

7.3 Quy trình chế độ người dùng

Từ giờ trở đi, chúng ta sẽ giả định rằng một tiến trình có thể thực thi ở hai chế độ khác nhau; Chế độ hạt nhân (chế độ SVC trong
ARM) và chế độ Người dùng. Để cho ngắn gọn, chúng ta sẽ gọi chúng lần lượt là Kmode và Umode. Mỗi chế độ có không gian VA riêng.
Khi định cấu hình MMU để ánh xạ địa chỉ ảo, chúng ta sẽ sử dụng sơ đồ KML sao cho không gian VA của Kmode từ 0 đến dung lượng bộ
nhớ vật lý và không gian VA của Umode từ 0x80000000 (2 GB) đến kích thước của Umode hình ảnh.

7.3.1 Hình ảnh chế độ người dùng

Đầu tiên, chúng tôi trình bày cách phát triển hình ảnh quy trình ở chế độ Người dùng. Một chương trình ở chế độ người dùng bao gồm một tệp hợp ngữ, us.s

và một tập hợp các tệp C, được hiển thị và giải thích bên dưới.

(1). /*tệp us.s trong tập hợp ARM */

.global entryPoint, main, syscall, getcsr, getAddr

.chữ

.code 32

. toàn cầu _exit // syscall(99, 0, 0 ,0) để chấm dứt

// khi vào, r0 trỏ tới chuỗi cmdline trong ngăn xếp Umode

điểm vào:

bl chính

bl _exit

cuộc gọi chung: // syscall(a,b,c,d): a,b,c,d được truyền vào r0-r3

bơi #0

di chuyển máy tính, lr

get_cpsr:

bà r0, cpsr

di chuyển máy tính, lr

Giải thích về tập tin us.s: us.s là điểm vào của tất cả các chương trình Umode. Như sẽ được hiển thị ngay sau đây, trước
khi vào chế độ Người dùng, kernel đã thiết lập môi trường thực thi của chương trình, bao gồm cả ngăn xếp Umode. Vì vậy, khi vào
nó chỉ cần gọi main(). Nếu main() trả về, nó gọi _exit(), đưa ra một syscall(99, 0, 0, 0) để kết thúc. Các tiến trình Umode có
thể vào kernel để thực thi các chức năng của kernel thông qua các lệnh gọi hệ thống.

int r = syscall(int a, int b, int c, int d)

Khi thực hiện lệnh gọi hệ thống, tham số đầu tiên a là số lệnh gọi hệ thống, b, c, d là tham số cho hàm kernel và r là giá trị
trả về. Trong các hệ thống dựa trên ARM, lệnh gọi hệ thống hay gọi tắt là syscall được thực hiện bằng lệnh SWI, lệnh này khiến
CPU chuyển sang chế độ Giám sát đặc quyền (SVC). Do đó, các tiến trình trong kernel chạy ở chế độ SVC. Hàm get_cpsr() trả về thanh
ghi trạng thái hiện tại của CPU. Nó được sử dụng để xác minh rằng quy trình thực sự đang thực thi ở chế độ Người dùng (mode =
0x10).
Machine Translated by Google

7.3 Quy trình chế độ người dùng 195

(2). /************ tập tin ucode.c *************/

#include "string.c" // các hàm chuỗi // uprintf(), v.v.

#include "uio.c"

int umenu() // hiển thị menu lệnh

uprintf("--------------------------\n"); uprintf("getpid

getppid ps chname \n"); uprintf("--------------------------

\n");

// syscalls tới kernel

int getpid() { trả về syscall(0,0,0,0); }

int getppid() { trả về syscall(1,0,0,0); }

int ps() { trả về syscall(2,0,0,0); }

int chname(char *s){ return syscall(3,s,0,0); }

// Các hàm lệnh của chế độ người dùng, mỗi hàm sẽ đưa ra một syscall int ugetpid()

int pid = getpid();

uprintf("pid = %d\n", pid);

int ugetppid()

int ppid = getppid();

uprintf("ppid = %d\n", ppid);

int uchname()

char s[32];

uprintf("nhập chuỗi tên: ");

(các) đường dây; uprintf("\n");

(các) tên;

int up(){ ps(); }

/** I/O Umode BASIC là các cuộc gọi hệ thống tới kernel **/

int ugetc() int { trả về syscall(90,0,0,0); }

uputc(char c){ return syscall(91,c,0,0); }

Giải thích về tệp ucode.c: ucode.c chứa các chức năng giao diện cuộc gọi hệ thống. Khi một chương trình ở chế độ người dùng chạy, trước tiên

nó sẽ hiển thị một số thông tin khởi động, chẳng hạn như chế độ CPU và địa chỉ ảo khởi đầu. Sau đó, nó hiển thị một menu và yêu cầu lệnh của người

dùng. Với mục đích trình diễn, mỗi lệnh của người dùng sẽ đưa ra một lệnh gọi hệ thống tới kernel. Mỗi syscall được gán một số để nhận dạng,

tương ứng với một hàm trong kernel. Số syscall hoàn toàn tùy thuộc vào sự lựa chọn của người thiết kế hệ thống. Vì các chương trình ở chế độ

Người dùng chạy trong không gian địa chỉ Umode nên chúng không thể truy cập trực tiếp vào không gian I/O trong kernel. Do đó, I/O cơ bản trong

Umode, chẳng hạn như ugetc() và uputc(), cũng là các lệnh gọi hệ thống. Vì tất cả các chương trình ở chế độ người dùng đều dựa vào lệnh gọi hệ

thống nên tất cả các chương trình ở chế độ người dùng đều có thể chia sẻ cùng một tệp ucode.c. Trong hệ thống thực, giao diện cuộc gọi hệ thống

thường được biên dịch trước như một phần của thư viện liên kết, được trình liên kết sử dụng để phát triển tất cả các chương trình chế độ người dùng.

(3). Tệp u1.c: Đây là phần chính của chương trình Umode. Nó có thể được sử dụng làm mẫu để phát triển các chương trình Umode khác.

/********* tập tin u1.c *************/

#include "ucode.c" // tất cả chương trình chế độ người dùng đều có chung ucode.c

int chính()

int i, pid, ppid, chế độ;


Machine Translated by Google

196 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

dòng char[64];

chế độ = get_cpsr() & 0x1F); // lấy CPSR để xác định chế độ CPU

printf("Chế độ CPU=%x\n", chế độ); pid = // hiển thị chế độ CPU

getpid(); ppid = getppid();

trong khi(1){

printf("Đây là quá trình %d trong Umode tại %x: parent=%d\n", pid, &entryPoint, ppid);

umenu();

uprintf("nhập lệnh: "); ugetline(dòng);

uprintf("\n"); if (!strcmp(line, "getpid"))

ugetpid();

if (!strcmp(line, "getppid"))

ugetppid();

if (!strcmp(line, "ps"))

up();

if (!strcmp(line, "chname"))

uchname();

}
}

Giải thích về tệp u1.c: u1.c là phần chính của chương trình Umode. Sau khi hiển thị chế độ CPU để xác minh rằng nó thực sự
đang thực thi trong Umode, nó sẽ thực hiện các lệnh gọi hệ thống để lấy pid và ppid của tiến trình gốc. Sau đó nó thực hiện một
vòng lặp vô hạn. Đầu tiên, nó hiển thị ID tiến trình và địa chỉ ảo bắt đầu của hình ảnh Umode. Sau đó, nó sẽ hiển thị một menu. Để
bắt đầu, menu chỉ bao gồm bốn lệnh: getpid, getppid, ps và chname. Khi chúng tôi tiếp tục mở rộng kernel, chúng tôi sẽ thêm nhiều
lệnh người dùng hơn sau. Mỗi lệnh người dùng gọi một hàm giao diện trong ucode.c, hàm này đưa ra lệnh gọi hệ thống để thực thi
hàm syscall trong kernel. Ví dụ: lệnh ps khiến tiến trình thực thi kps() trong kernel, lệnh này sẽ in tất cả thông tin trạng thái
PROC. Mỗi tiến trình được khởi tạo bằng một chuỗi tên trong trường PROC.name. Lệnh chname thay đổi chuỗi tên của tiến trình đang
chạy. Sau khi đổi tên, người dùng có thể sử dụng lệnh ps để xác minh kết quả.

(4). tập lệnh mku: Tập lệnh mku sh được sử dụng để tạo hình ảnh nhị phân u1.o

arm-none-eabi-as -mcpu=arm926ej-s us.s -o us.o arm-none-eabi-gcc -c

-mcpu=arm926ej-s -o $1.o $1.c arm-none-eabi- ld -T u.ld us.o $1.o -Ttext=0x80000000

-o $1.elf arm-none-eabi-objcopy -O nhị phân $1.elf $1 arm-none-eabi-objcopy -O elf32-littlearm -B arm

$1 $1.o cp -av $1.o ../

Giải thích về tệp tập lệnh mku: Tập lệnh mku tạo ra tệp hình ảnh thực thi nhị phân. Đầu tiên, nó (chéo) biên dịch liên kết us.s và
u1.c thành một tệp ELF có địa chỉ ảo bắt đầu 0x800000000 (2 GB). Sau đó, nó sử dụng objcopy để chuyển đổi tệp ELF thành tệp hình
ảnh nhị phân thô. Trước khi phát triển trình tải để tải hình ảnh chương trình, chúng ta sẽ sử dụng hình ảnh nhị phân làm phần dữ
liệu thô trong hình ảnh hạt nhân. Điều này được thực hiện trong tập lệnh liên kết t.ld.

#--------- script liên kết t.ld --------------

NHẬP(reset_handler)

PHẦN

. = 0x10000;

.text : { ts.o to }

.dữ liệu : { ts.o(.data) đến (.data) }


Machine Translated by Google

7.3 Quy trình chế độ người dùng 197

.bss : { *(.bss) }

. = CÁNH(8);

. = . + 0x1000; /* 4kB không gian ngăn xếp irq */

irq_stack_top = .;

. = . + 0x1000; /* 4kB bộ nhớ ngăn xếp abt */

abt_stack_top = .;

. = . + 0x1000; /* 4kB bộ nhớ ngăn xếp */

und_stack_top = .;

. = CÁNH(1024);

.data : { /* bao gồm u1.o làm phần dữ liệu RAW */

u1.o

7.4 Nhân hệ thống hỗ trợ các quy trình ở chế độ người dùng

Nhân hệ thống bao gồm các thành phần sau: bộ xử lý ngắt, trình điều khiển thiết bị, thao tác I/O và hàng đợi.
chức năng và chức năng quản lý quy trình. Hầu hết các thành phần kernel, ví dụ như bộ xử lý ngắt, trình điều khiển thiết bị và
các chức năng quản lý quy trình cơ bản, v.v. đã được đề cập trong các Chương trước. Sau đây, chúng ta sẽ tập trung vào
các tính năng mới của hạt nhân. Để rõ ràng, ngoài tiêu đề của phần chúng tôi cũng sẽ sử dụng số thứ tự (trong
dấu ngoặc đơn) để hiển thị các đoạn mã hạt nhân.

7.4.1 Cấu trúc PROC

(1). Cấu trúc PROC (trong tệp type.h)

#xác định NPROC 9

#định nghĩa MIỄN PHÍ 0

#xác định SẴN SÀNG 1

#định nghĩa GIẤC NGỦ 2

#xác định KHỐI 3

#định nghĩa ZOMBIE 4

#xác định SSIZE 1024

typedef struct proc{ struct // độ lệch byte:

proc *next; *ksp; // 0

int // 4 : đã lưu Kmode sp khi không chạy

int *đồng ý; // 8 : Umode sp tại thời điểm syscall

int *upc; // 12: Umode pc tại thời điểm syscall

int *ucpsr; // 16: Umode cpsr

int *pgdir; // con trỏ bảng trang cấp 1

int trạng thái; // MIỄN PHÍ|SẴN SÀNG|NGỦ|BLOCK|ZOMBIE, v.v.

int sự ưu tiên;
int pid;

int ppid; // pid cha

struct proc *parent; // con trỏ PROC cha

int sự kiện; // sự kiện để ngủ tiếp

int mã thoát; // mã thoát


Machine Translated by Google

198 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

tên char[64]; // trường tên

int kstack[SSIZE]; // ngăn xếp Kmode

}THỦ TỤC;

Mỗi quá trình được đại diện bởi một cấu trúc PROC. Các trường mới trong cấu trúc PROC hỗ trợ các hoạt động của Umode
là usp, upc, ucpsr: Khi một tiến trình đi vào kernel thông qua syscall, nó sẽ lưu Umode sp, lr và cpsr trong cấu trúc
PROC để quay lại

Umode sau. pgdir: Mỗi tiến trình có một bảng trang cấp 1 (pgdir) được trỏ bởi PROC.pgdir. pgdir và các bảng trang liên
quan của nó xác định không gian địa chỉ ảo của quy trình ở cả chế độ
Kernel và User. ppid và con trỏ PROC cha: pid tiến trình cha và con trỏ tới PROC
cha Mã thoát: để quá trình kết thúc bằng tên giá trị
exitCode: chuỗi tên tiến trình được sử dụng để thể hiện
lệnh gọi hệ thống. (2) tệp ts.s: Mã hợp ngữ của hạt nhân bao gồm năm phần, được đánh dấu trong danh sách mã hiển thị bên dưới.

// ********** tập tin ts.s *************

.chữ

.code 32

.global reset_handler, vectơ_start, vectơ_end

.global proc, procsize .global

tswitch, bộ lập lịch, đang chạy, goUmode, switchPgdir .global int_off, int_on, khóa, mở khóa,

get_cpsr

// Phần 1: reset_handler

reset_handler:

// đặt ngăn xếp SVC thành CAO CẤP của proc[0].kstack[]

LDR r0, =proc // r0 trỏ tới Proc's

LDR r1, =procsize // r1 -> procsize

LDR r2, [r1, #0] // r2 = xử lý

CỘNG r0, r0, r2 // r0 -> cao cấp của Proc[0]

MOV sp, r0

// đi tới chế độ IRQ để thiết lập ngăn xếp IRQ

MSR cpsr, #0x92

LDR sp, =irq_stack_top // đặt ngăn xếp IRQ

// chuyển sang chế độ ABT để đặt ngăn xếp ABT

MSR cpsr, #0x97

LDR sp, =abt_stack_top // đặt ngăn xếp abt

// chuyển sang chế độ UND để đặt ngăn xếp UND

MSR cpsr, #0x9B

LDR sp, =und_stack_top // đặt ngăn xếp UND

// quay lại chế độ SVC, đặt SPSR thành chế độ Người dùng khi bật IRQ

MSR cpsr, #0x93 // Chế độ SVC

MSR spsr, #0x10 // đặt SPSR thành Umode khi bật IRQ // sao chép bảng vectơ

Bản sao BL_vector sang địa chỉ 0 // tạo bảng trang trong C

BL mkPtable

// khởi tạo thanh ghi điều khiển MMU C1

// bit12=1: EnIcache; bit9-8=RS=11; bit2=1: EnDcache;

// mặc định tất cả các bit khác của C1=0

LDR r0, regC1 // tải r0 với 0x1304

MCR p15, 0, r0, c1, c0, 0 // ghi vào reg điều khiển MMU C1

// thiết lập thanh ghi cơ sở bảng trang, xóa TLB

LDR r0, MTABLE // pgdir ở 0x4000 (16KB)

MCR p15, 0, r0, c2, c0, 0 // đặt Bảng trang Thanh ghi cơ sở C2

MCR p15, 0, r0, c8, c7, 0 // tuôn ra TLB

// đặt miền0: 01=client(kiểm tra quyền),11=manager(không kiểm tra)


Machine Translated by Google

7.4 Nhân hệ thống hỗ trợ các quy trình ở chế độ người dùng 199

MOV r0, #0x1 // b01 cho KHÁCH HÀNG

MCR p15, 0, r0, c3, c0, 0

// kích hoạt MMU: bật bit0 của reg điều khiển C1

MRC p15, 0, r0, c1, c0, 0

ORR r0, r0, #0x00000001 // đặt bit0 thành 1

MCR p15, 0, r0, c1, c0, 0 // ghi vào c1 để kích hoạt MMU

không // thời gian cho phép MMU kết thúc

không

không

BL chính // gọi main() trong tc

B . // nếu main() trả về, lặp lại ở đây

MTABLE: .word 0x4000 // MTABLE ở mức 16KB

regC1: .word 0x1304 // Cài đặt c1 thanh ghi điều khiển P15

// phần 2: IRQ và các điểm vào xử lý ngoại lệ

irq_handler: // IRQ ngắt điểm vào

SUB lr, lr, #4 // điều chỉnh thanh ghi liên kết lr

STMFD sp!,{r0-r12, lr} // lưu ngữ cảnh vào ngăn xếp IRQ

BL irq_chandler // gọi irq_handler() trong C

LDMFD sp!,{r0-r12, pc}^ // bật ngăn xếp IRQ, khôi phục SPSR

Trình xử lý dữ liệu: // trình xử lý hủy dữ liệu

SUBlr, lr, #4

STMFD sp!, {r0-r12, lr}

Dữ liệu BL_chandler

LDMFD sp!, {r0-r12, pc}^

// Phần 3: chuyển ngữ cảnh task, switch pgdir

tswitch://tswitch() trong Kmode

bà r0, cpsr // TẮT IRQ

hoặc r0, #0x80

msr cpsr, r0

STMFD sp!, {r0-r12, lr} // lưu bối cảnh

LDR r0, = đang chạy // r0=&đang chạy

LDR r1, [r0, #0] // r1->runningPROC

STR sp, [r1, #4] // đang chạy->ksp = sp

Bộ lập lịch BL // gọi bộ lập lịch() trong C

LDR r0, = đang chạy

LDR r1, [r0, #0] // r1->runningPROC

LDR sp, [r1, #4] // sp = đang chạy->ksp

bà r0, cpsr // IRQ BẬT

bi r0, #0x80

msr cpsr, r0

LDMFD sp!, {r0-r12, pc} // tất cả đều ở Kmode

switchPgdir: // chuyển sang pgdir của PROC mới trong khi chuyển đổi tác vụ

// r0 chứa PA của pgdir của PROC mới

MCR p15, 0, r0, c2, c0, 0 // đặt TTB trong C2

MOV r1, #0

MCR p15, 0, r1, c8, c7, 0 // tuôn ra TLB

MCR p15, 0, r1, c7, c10, 0 // xóa bộ đệm

MRC p15, 0, r2, c2, c0, 0

// đặt các bit AP của miền thành chế độ CLIENT: kiểm tra các bit AP

MOV r0, #0x5 // 0101: |domain1|domain0=CLIENT

MCR p15, 0, r0, c3, c0, 0

Máy tính MOV, LR // trở lại


Machine Translated by Google

200 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

// Phần 4: Nhập SVC, định tuyến cuộc gọi hệ thống và quay về chế độ Người dùng

svc_entry:// r0-r3 chứa thông số syscall, không làm phiền stmfd sp!, {r0-r12, lr}

ldr r4, =running // r4=&running

ldr r5, [r4, #0] // r5 -> PROC của tiến trình đang chạy

Mov r6, spsr

str r6, [r5, #16] // lưu Umode SR trong PROC.ucpsr ở offset 16

// chuyển sang chế độ SYS;

bà r6, cpsr // r6 = Chế độ SVC cpsr

di chuyển r7, r6 // lưu một bản sao vào r7

orr r6, r6, #0x1F // r6 = chế độ SYS

msr cpsr, r6 // thay đổi cpsr sang chế độ SYS

// bây giờ ở chế độ SYS, lưu Umode sp, pc vào chạy PROC

str sp, [r5, #8] // lưu usp vào proc.usp ở offset 8

str lr, [r5, #12] // lưu upc vào proc.upc ở offset 12

// quay lại chế độ SVC

msr cpsr, r7

// thay thế lr đã lưu trong kstack bằng Umode PC tại syscall

mov r6, sp

thêm r6, r6, #52 // mục 13 => offset=13*4 = 52

ldr r7, [r5, #12] // Umode LR tại syscall, KHÔNG tại swi

str r7, [r6] // thay thế LR đã lưu trong kstack

// kích hoạt ngắt

bà r6, cpsr

bic r6, r6, #0xC0 // I và F bit=0 bật IRQ,FIQ

msr cpsr, r6

bl svc_handler // gọi svc_handler() trong C

goUmode:

ldr r4, =running // r4=&running

ldr r5, [r4, #0] // r5 -> PROC của quá trình chạy

ldr r6, [r5, #16] // đã lưu spsr

msr spsr, r6 // // khôi phục spsr của tiến trình NÀY

chuyển sang chế độ SYS

bà r6, cpsr // r6 = Chế độ SVC cpsr

di chuyển r7, r6 // lưu một bản sao vào r7

orr r6, r6, #0x1F // r6 = chế độ SYS

msr cpsr, r6 // chuyển sang chế độ SYS

// hiện đang ở chế độ SYS

ldr sp, [r5, #8] // khôi phục Umode sp từ PROC.usp

// quay lại chế độ SVC

msr cpsr, r3

// bật reg từ kstack VÀ khôi phục Umode cpsr

ldmfd sp!, {r0-r12, pc}^ // pop kstack AND spsr: quay lại Umode

// Phần 5: Các hàm tiện ích

// IRQ ngắt kích hoạt/vô hiệu hóa chức năng

int_off: // SR = int_off()

MRS r0, cpsr

MOV r1, r0

ORR r1, r1, #0x80

MSR cpsr, r1 // giá trị trả về trong r0 = cpsr gốc

Máy tính MOV, LR

int_on: // int_on(SR); SR ở r0

MSR cpsr, r0

Máy tính MOV, LR

kho a:
Machine Translated by Google

7.4 Nhân hệ thống hỗ trợ các quy trình ở chế độ người dùng 201

MRS r0, cpsr


ORR r0, r0, #0x80

MSR cpsr, r0

Máy tính MOV, LR

mở khóa:

MRS r0, cpsr


BIC r0, r0, #0x80

MSR cpsr, r0

Máy tính MOV, LR

get_cpsr: // trả về cpsr để xác minh MODE

MRS r0, cpsr

MOV pc, lr //

bảng vectơ: được sao chép vào PA 0 trong reset_handler


vectơ_start:

PC LDR, reset_handler_addr

Máy tính LDR, undef_handler_addr

Máy tính LDR, svc_handler_addr

PC LDR, tìm nạp trước_abort_handler_addr


PC LDR, dữ liệu_abort_handler_addr
B .

Máy tính LDR, irq_handler_addr

PC LDR, fiq_handler_addr
đặt lại_handler_addr: .word reset_handler

undef_handler_addr: .word undef_handler

svc_handler_addr: .word svc_entry

prefetch_abort_handler_addr: .word prefetch_abort_handler


dữ liệu_abort_handler_addr: .word data_handler

irq_handler_addr: .word irq_handler .word

fiq_handler_addr: fiq_handler
vectơ_end:

7.4.2 Trình xử lý đặt lại

reset_handler bao gồm ba bước.

7.4.2.1 Ngăn xếp ngoại lệ và IRQ Bước 1:


Thiết lập ngăn xếp: Giả sử NPROC=9 PROC, mỗi PROC có một ngăn xếp Kmode trong cấu trúc PROC. Hệ thống khởi động ở chế độ SVC.
Reset_handler khởi tạo con trỏ ngăn xếp chế độ SVC ở đầu cao của proc[0], do đó đặt proc[0].kstack làm ngăn xếp ban đầu. Nó cũng
đặt spsr ở chế độ Người dùng, giúp CPU sẵn sàng quay lại chế độ Người dùng khi nó thoát khỏi chế độ SVC.
Sau đó, nó khởi tạo con trỏ ngăn xếp của các chế độ đặc quyền khác, ví dụ IRQ, data_abort, undef_abort, v.v. Mỗi chế độ đặc quyền
(ngoại trừ chế độ FIQ, không được sử dụng) có một vùng ngăn xếp 4 KB riêng biệt (được xác định trong tập lệnh liên kết t.ld) cho
xử lý ngắt và ngoại lệ.

7.4.2.2 Sao chép bảng vectơ Bước


2: Copy_vector_table: Trong khi đặt lại, MMU quản lý bộ nhớ bị tắt và bit cho phép ánh xạ lại bảng vectơ (bit V trong thanh ghi
điều khiển MMU c0) là 0, nghĩa là bảng vectơ không được ánh xạ lại thành 0xFFFF0000 . Tại thời điểm này, mọi địa chỉ đều là địa
chỉ vật lý. Reset_handler sao chép bảng vectơ vào địa chỉ vật lý 0 theo yêu cầu của phần cứng vectơ của CPU ARM.
Machine Translated by Google

202 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

7.4.2.3 Tạo Bảng trang chế độ hạt nhân Bước 3: Tạo Bảng

trang Kmode: Sau khi khởi tạo con trỏ ngăn xếp của các chế độ đặc quyền khác nhau, reset_handler gọi mkPtable() để thiết lập bảng trang chế độ hạt

nhân. Để bắt đầu, chúng ta sẽ sử dụng phân trang một cấp đơn giản với các phần 1 MB để tạo ánh xạ nhận dạng từ VA đến PA. Giả sử bộ nhớ vật lý 256

MB cộng với 2 MB dung lượng I/O (của ARM Versatilepb VM) ở mức 256 MB, hàm mkPtable() trong C là

#define PTABLE 0x4000

int mkPtable() // tạo Ptable được ánh xạ ID với các phần 1MB

int tôi;

int *ut = (int *)PTABLE; // PTABLE ở mức 16KB của PA

mục nhập int = 0 | 0x41E; // |AP=01|domain=0000|1|CB=11|type=10|

for (i=0; i<258; i++){ // giả sử 256 MB RAM + 2 MB dung lượng I/O

ut[i] = mục nhập;

mục += 0x100000; // kích thước phần = 1 MB byte

Các thuộc tính của các mục trong bảng trang được đặt thành 0x41E cho AP = 01, miền = 0000, CB = 11 và loại = 10 (Phần).
Ngoài ra, các bit CB có thể được đặt thành 00 để vô hiệu hóa lệnh và bộ nhớ đệm dữ liệu cũng như bộ đệm ghi. Toàn bộ không
gian Kmode được coi là miền 0 với các bit cấp phép = 01 cho R|W ở chế độ đặc quyền nhưng không có quyền truy cập ở chế độ Người dùng.
Sau đó, nó cho phép MMU dịch địa chỉ VA sang PA. Sau đó, mọi địa chỉ ảo VA được phần cứng MMU ánh xạ tới địa chỉ vật lý PA.
Trong trường hợp này, địa chỉ VA và PA giống nhau do ánh xạ nhận dạng.
Các sơ đồ ánh xạ địa chỉ ảo thay thế sẽ được thảo luận sau. Sau đó nó gọi main() trong C để tiếp tục khởi tạo kernel.

7.4.2.4 Chức năng chuyển đổi bối cảnh quy trình Phần 2: Chuyển

đổi bối cảnh quy trình: tswitch() dành cho quá trình chuyển đổi trong Kernel. Khi một tiến trình từ bỏ CPU, nó gọi tswitch(), trong đó nó lưu các

thanh ghi CPU trong kstack tiến trình, lưu con trỏ ngăn xếp vào PROC.ksp và gọi bộ lập lịch () trong C. Trong bộ lập lịch (), tiến trình đi vào

hàng đợi sẵn sàng theo mức độ ưu tiên nếu nó vẫn SẴN SÀNG để chạy. Sau đó, nó chọn quy trình có mức độ ưu tiên cao nhất từ ReadyQueue làm quy

trình chạy tiếp theo. Nếu tiến trình đang chạy tiếp theo khác với tiến trình hiện tại, nó sẽ gọi switchPgdir() để chuyển bảng trang sang bảng trang

của tiến trình đang chạy tiếp theo. SwitchPgdir() cũng xóa TLB, vô hiệu hóa lệnh và bộ đệm dữ liệu cũng như xóa bộ đệm ghi để ngăn CPU sử dụng các

mục TLB thuộc bối cảnh quy trình cũ. Để tăng tốc quá trình dịch địa chỉ, ARM MMU hỗ trợ nhiều tùy chọn nâng cao, chẳng hạn như hướng dẫn khóa và

bộ đệm dữ liệu, vô hiệu hóa TLB đã chọn và các mục trong bộ đệm, v.v. Để giữ cho hệ thống đơn giản, chúng ta sẽ không sử dụng các tùy chọn này

tính năng MMU nâng cao.

7.4.2.5 Nhập và thoát cuộc gọi hệ thống Phần 3:

Vào và thoát cuộc gọi hệ thống: Các quy trình ở chế độ người dùng sử dụng syscall(a, b, c, d) để thực thi các chức năng gọi hệ thống trong kernel.

Trong syscall(), nó phát hành SWI để vào chế độ SVC, chế độ này được định tuyến đến trình xử lý SVC thông qua vectơ SWI.

7.4.2.6 Trình xử lý SVC Trình

xử lý SVC: svc_entry là điểm vào cuộc gọi hệ thống. Các tham số lệnh gọi hệ thống a, b, c, d được truyền vào các thanh ghi r0–r3.

Khi vào, trước tiên quy trình sẽ lưu tất cả các thanh ghi CPU trong ngăn xếp Kmode của quy trình (PROC.kstack). Ngoài ra, nó
còn lưu Umode sp, lr và cpsr vào các trường usp, upc và ucpsr của PROC tương ứng. Để truy cập vào các thanh ghi Umode, nó
tạm thời chuyển CPU sang chế độ Hệ thống, chia sẻ cùng một bộ thanh ghi với chế độ Người dùng. Sau đó, nó thay thế lr đã lưu
trong kstack bằng upc Umode. Điều này là do lr đã lưu trỏ đến lệnh SWI trong Umode chứ không phải upc tại thời điểm gọi hệ
thống. Sau đó, nó cho phép ngắt IRQ và gọi svc_handler() trong C, thực tế là xử lý lệnh gọi hệ thống. Khi tiến trình thoát khỏi
kernel, nó thực thi goUmode() để quay lại Umode. Trong mã goUmode, nó lần đầu tiên khôi phục Umode sp và cpsr từ usp và cpsr đã
lưu trong PROC. Sau đó nó trở về Umode bằng cách

ldmfd sp!, {r0-r12, pc}^


Machine Translated by Google

7.4 Nhân hệ thống hỗ trợ các quy trình ở chế độ người dùng 203

Người đọc có thể thắc mắc tại sao cần lưu và khôi phục Umode sp và cpsr trong các cuộc gọi hệ thống. Vấn đề là như sau. Khi
một tiến trình vào Kernel, nó có thể không quay lại chế độ Người dùng ngay lập tức. Ví dụ: quy trình có thể bị tạm dừng trong
Kernel và chuyển sang quy trình khác. Khi quy trình mới chuyển từ chế độ Kernel sang chế độ Người dùng, usp và spsr của CPU là
của quy trình bị treo, không phải của quy trình hiện tại.
Cần lưu ý rằng trong hầu hết kiến trúc hướng ngăn xếp, việc lưu và khôi phục con trỏ ngăn xếp chế độ Người dùng và các
thanh ghi trạng thái là tự động trong các cuộc gọi hệ thống. Ví dụ, trong CPU Intel x86 (Intel 1990, 1992), lệnh INT tương tự
như lệnh ARM SWI, cả hai đều khiến tiến trình chuyển sang chế độ Kernel. Sự khác biệt chính là khi CPU Intel x86 thực thi lệnh
INT, nó sẽ tự động xếp chồng chế độ Người dùng [uss, usp], uflags, [ucs, upc], tương đương với chế độ Người dùng SP, CPSR,
LR của CPU ARM. Khi CPU Intel x86 thoát khỏi chế độ Kernel bằng IRET (tương tự như thao tác ^ liên quan đến PC trong ARM), nó
sẽ khôi phục tất cả các thanh ghi Umode đã lưu từ ngăn xếp chế độ Kernel. Ngược lại, bộ xử lý ARM không tự động xếp chồng bất
kỳ thanh ghi Umode nào khi nó chuyển sang chế độ đặc quyền. Lập trình viên hệ thống phải thực hiện các thao tác lưu và khôi phục
theo cách thủ công.

7.4.2.7 Trình xử lý ngoại lệ

(4) Trình xử lý ngoại lệ: Hiện tại, chúng tôi chỉ xử lý các ngoại lệ data_abort, được sử dụng để chứng minh khả năng bảo vệ bộ
nhớ của MMU. Tất cả các trình xử lý ngoại lệ khác đều là vòng lặp while(1). Phần sau đây trình bày thuật toán của trình xử
lý ngoại lệ data_abort.

/*** Tệp ngoại lệ.c: chỉ hiển thị data_abort_handler ***/

int data_chandler()
{
// đọc các thanh ghi MMU C5=fault_status, C6=fault_address;
// in error_address và error_status;
}

7.4.3 Mã hạt nhân

(5) Tệp Kernel.c: Tệp này xác định cấu trúc dữ liệu kernel và thực hiện các chức năng kernel. Khi hệ thống khởi động,
reset_handler gọi main(), gọi kernel_init() để khởi tạo kernel. Đầu tiên, nó khởi tạo danh sách PROC miễn phí và hàng đợi

sẵn sàng. Sau đó, nó tạo ra quy trình ban đầu P0, chỉ chạy trong Kmode với mức ưu tiên thấp nhất là 0. Sau đó, nó thiết lập
các bảng trang cho PROC. Khi thiết lập các bảng trang quy trình, chúng ta sẽ giả định rằng không gian VA của kernel được
ánh xạ ở mức thấp, từ 0 đến lượng bộ nhớ vật lý khả dụng. Không gian VA của chế độ Người dùng được ánh xạ ở mức cao, từ
0x80000000 (2 GB) đến 2 GB + kích thước hình ảnh Umode. Mỗi PROC có một pid duy nhất (1 đến NPROC-1) và một con trỏ bảng
trang cấp 1 pgdir. Các bảng trang quy trình được xây dựng trong vùng bộ nhớ vật lý 6 MB. Mỗi bảng trang yêu cầu không gian
4096 * 4 = 16 K byte. Vùng 1 MB từ 6 đến 7 MB có đủ không gian cho 64 bảng trang PROC.
Mỗi tiến trình (trừ P0) có một bảng trang có kích thước 6 MB + (pid 1) * 16 KB. Trong mỗi bảng trang, 2048 mục nhập thấp
xác định không gian địa chỉ chế độ nhân tiến trình, giống hệt nhau cho tất cả các tiến trình vì chúng chia sẻ cùng một không
gian địa chỉ trong Kmode. 2048 mục cao của bảng trang xác định không gian địa chỉ Umode của quy trình, chỉ được điền khi
quy trình được tạo.

Sau đây, chúng ta sẽ giả sử rằng mỗi quy trình (ngoại trừ P0) có hình ảnh Umode 1 MB ở mức 8 MB + (pid 1) * 1 MB, ví dụ P1
ở mức 8 MB, P2 ở mức 9 MB, v.v. Giả định này không phải phê bình. Nếu muốn, người đọc có thể giả sử các kích thước hình ảnh
Umode khác nhau. Với kích thước hình ảnh Umode 1 MB, mỗi bảng trang chỉ cần một mục nhập cho không gian VA chế độ người dùng,
tức là mục nhập 2048 trỏ đến vùng Umode 1 MB của quy trình. Hãy nhớ lại rằng chúng ta đã chỉ định vùng bộ nhớ chế độ Kernel là miền 0.
Chúng tôi sẽ gán tất cả các vùng bộ nhớ của Chế độ người dùng cho miền 1. Theo đó, chúng tôi đặt thuộc tính mục nhập trang Umode
thành 0xC3E (AP = 11, miền = 0001, CB = 11 và loại = 10). Các bit quyền truy cập (AP) của miền0 được đặt thành 01 để cho phép
truy cập từ Kmode nhưng không cho phép truy cập từ Umode. Tuy nhiên, các bit AP của bộ mô tả bảng trang Umode hoặc miền1 (của
Machine Translated by Google

204 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

thanh ghi kiểm soát truy cập miền) phải được đặt thành 11 để cho phép truy cập từ Umode. Vì các bit AP của miền1 được đặt thành
01 trong switchPgdir(), nên các bit AP của bộ mô tả trang Umode phải được đặt thành 11. Phần sau đây liệt kê mã tệp kernel.c.

/********* tập tin kernel.c *************/

PROC proc[NPROC], *freeList, *readyQueue, *sleepList, *running;

int procsize = sizeof(PROC);

char *pname[NPROC]={"mặt trời", "thủy ngân", "sao kim", "trái đất", "sao hỏa",

"Sao Mộc", "Sao Thổ", "Sao Thiên Vương", "Sao Hải Vương"};

int kernel_init()

int tôi, j;

PROC *p; char *cp;

int *MTABLE, *mtable, paddr;

printf("kernel_init()\n"); cho (i=0;

i<NPROC; i++){

p = &proc[i]; p->pid

= tôi;

p->trạng thái = MIỄN PHÍ;

p->ưu tiên = 0; p->ppid

= 0; p->cha mẹ =

0; strcpy(p->name,

pname[i]); p->tiếp theo = p + 1; p->pgdir

= (int *)(0x600000 + p-

>pid*0x4000);

proc[NPROC-1].next = 0;

freeList = &proc[0]; // tất cả PROC đều có trong freeList

hàng đợi sẵn sàng = 0;

Danh sách ngủ = 0;

// tạo và chạy P0

đang chạy = get_proc(&freeList);

đang chạy->trạng thái = SẴN SÀNG;

printList(freeList); printQ(readyQueue);

printf("xây dựng pgdir ở mức 6MB\n");

// tạo pgdir cho TẤT CẢ PROC ở mức 6MB; MTABLE ở mức 16KB tính bằng ts.s

MTABLE = (int *)0x4000; mtable = // Mtable ở 0x4000

(int *)0x600000; // mtable bắt đầu ở mức 6MB

// Mỗi pgdir PHẢI ở giới hạn 16K ==>

// 1 MB ở mức 6 MB có dung lượng cho 64 pgdir của 64 PROC

cho (i=0; i<64; i++){ // cho 64 bảng PROC

cho (j=0; j<2048; j++){

mtable[j] = MTABLE[j]; // sao chép 2048 mục thấp của MTABLE

mtable += 4096; // nâng mtable lên 16KB tiếp theo

mtable = (int *)0x600000; // mtable PROC bắt đầu ở mức 6MB

cho (i=0; i<64; i++){

for (j=2048; j<4096; j++){ // loại bỏ 2048 mục cao

mtable[j] = 0;

if (i) // loại trừ P0, thuộc tính trang=0xC3E:AP=11,domain=1

mtable[2048]=(0x800000 + (i-1)*0x100000) | 0xC3E;

mtable += 4096;

}
Machine Translated by Google

7.4 Nhân hệ thống hỗ trợ các quy trình ở chế độ người dùng 205

bộ lập lịch int()

PROC *cũ = đang chạy; if (đang

chạy->trạng thái==SẴN SÀNG){

enqueue(&readyQueue, đang chạy);

đang chạy = dequeue(&readyQueue); if (đang chạy !

= cũ){

// chuyển sang pgdir của hoạt động mới; xóa bộ nhớ đệm TLB và I&D switchPgdir((u32)running-

>pgdir);

(6) Trình xử lý cuộc gọi hệ thống và các hàm kernel

/*** các hàm gọi hệ thống trong Kernel ***/

int kgetpid() { chạy trở lại->pid; }

int kgetppid(){ return Running->ppid; }

int kchname(char *s){ lấy *s từ Umode; strcpy(đang chạy->tên, s); }

char *pstatus[]={"MIỄN PHÍ","SẴN SÀNG","SLEEP","BLOCK","ZOMBIE"};

int kps()

int tôi; PROC *p;

cho (i=0; i<NPROC; i++){

p = &proc[i];

printf("proc[%d]: pid=%d ppid=%d", i, p->pid, p->ppid); nếu (p==đang chạy)

printf("%s", "ĐANG CHẠY");

khác

printf("%s", pstatus[p->status]);

printf(" name=%s\n", p->name);

/********* tập tin svc.c xử lý cuộc gọi hệ thống ***********/

int svc_handler(dễ bay hơi int a, int b, int c, int d)

int r = -1; // giá trị trả về BAD mặc định

chuyển đổi (a) {

trường hợp 0: r = kgetpid(); phá vỡ;

trường hợp 1: r = kgetppid(); phá vỡ;

trường hợp 2: r = kps(); phá vỡ;

trường hợp 3: r = kchname((char *)b); phá vỡ;

trường hợp 90: r = kgetc() & 0x7F; phá vỡ;

trường hợp 91: r = kputc(b); phá vỡ;

mặc định: printf("tòa nhà không hợp lệ %d\n", a);

đang chạy->kstack[SSIZE-14] = r; // đã lưu r0 trong kstack = r

Hàm svc_handler() về cơ bản là một bộ định tuyến cuộc gọi hệ thống. Các tham số lệnh gọi hệ thống (a, b, c, d) được chuyển vào

các thanh ghi r0– r3, giống nhau trong tất cả các chế độ CPU. Dựa trên số lệnh gọi hệ thống a, cuộc gọi được định tuyến đến hàm

kernel tương ứng. Tệp kernel.c thực hiện tất cả các hàm gọi hệ thống trong kernel. Tại thời điểm này, mục đích của chúng tôi là để chứng minh
Machine Translated by Google

206 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

cơ chế và luồng điều khiển của các cuộc gọi hệ thống. Chính xác những gì các chức năng gọi hệ thống làm là không quan trọng. Vì vậy chúng tôi chỉ

thực hiện bốn lệnh gọi hệ thống rất đơn giản: getpid, getppid, ps và chname. Mỗi hàm trả về một giá trị r, được tải vào
r0 đã lưu trong kstack làm giá trị trả về cho chế độ Người dùng.

(7) Tệp tc: Đây là phần chính của hạt nhân hệ thống. Vì hầu hết các thành phần hệ thống, chẳng hạn như các ngắt, thiết bị

trình điều khiển, v.v. đã được giải thích trong các chương trước, chúng ta sẽ chỉ tập trung vào các tính năng mới được nêu bật

trong những đường nét đậm. Trước khi vào main(), bảng trang chế độ kernel đã được thiết lập trong ts.s bởi mkPtable()

chức năng và MMU được kích hoạt để dịch địa chỉ. Do ánh xạ nhận dạng của địa chỉ ảo tới địa chỉ vật lý

địa chỉ ở chế độ kernel, không cần thay đổi mã kernel. Khi main() khởi động, nó sao chép hình ảnh chương trình u1 sang

8 MB. Điều này là do chúng ta giả định rằng ảnh chế độ Người dùng của tiến trình P1 có địa chỉ vật lý là 8 MB nhưng nó

chạy trong không gian địa chỉ ảo từ 0x80000000 đến 0x80100000 (2G đến 2G + 1 MB) ở chế độ Người dùng. Sau đó nó gọi kfork()

để tạo tiến trình P1 và chuyển tiến trình sang chạy P1.

/************ tập tin tc của C7.1 *************/

#include "type.h"

#include "string.c"

#include "uart.c"

#include "kbd.c"

#include "timer.c"

#include "vid.c"

#include "ngoại lệ.c"

#include "queue.c"

#include "kernel.c"

#include "svc.c"

int copy_vectors(){ // sao chép vectơ về 0; giống như trước }

int mkPtable() int { // tạo bảng trang Kmode ở 0x4000; }

irq_chandler(){ // IRQ ngắt điểm vào của trình xử lý; }

int chính() // được nhập với các ngắt TẮT

int i, sử dụng;

dòng char[128], *cp, *cq;

/* kích hoạt ngắt thiết bị VIC và SIC */

VIC_INTENABLE |= (1<<4); // hẹn giờ0 lúc 4 giờ

VIC_INTENABLE |= (1<<12); // UART0 lúc 12 giờ

VIC_INTENABLE |= (1<<13); // UART1 lúc 13

VIC_INTENABLE = 1<<31; // SIC tới IRQ31 của VIC

fbuf_init(); // màn hình LCD

kbd_init(); // trình điều khiển KBD

uart_init(); // trình điều khiển UART

bộ đếm thời gian_init(); bộ đếm thời gian_start(0); // hẹn giờ

kernel_init(); // khởi tạo kernel, tạo và chạy P0

mở khóa(); // kích hoạt ngắt IRQ

printf("Chào mừng đến với Wanix trong ARM\n");

kfork(); // tạo P1 với Umode image=u1 với dung lượng 8MB

// code demo Bảo vệ MMU: thử truy cập VA không hợp lệ=>data_abort

printf("P0 chuyển sang P1 : nhập một dòng : \n");

kgets(dòng); // nhập một dòng để tiếp tục

trong khi(1){ // mã P0

while(readyQueue==0); // vòng lặp nếu hàng đợi trống

tswitch(); // chuyển sang chạy bất kỳ tiến trình sẵn sàng nào

}
Machine Translated by Google

7.4 Nhân hệ thống hỗ trợ các quy trình ở chế độ người dùng 207

7.4.3.1 Tạo quy trình với hình ảnh chế độ người dùng

(8) tệp fork.c: Tệp này triển khai hàm kfork(), tạo ra một quy trình con với hình ảnh chế độ Người dùng u1 trong khu vực Chế độ
người dùng của quy trình mới. Ngoài ra, nó cũng đảm bảo rằng tiến trình mới có thể thực thi hình ảnh Umode của nó ở chế độ
Người dùng khi nó chạy.

#xác định UIMAGE_SIZE 0x100000

PROC *kfork()

char bên ngoài _binary_u1_start, _binary_u1_end;

int sử dụng;

ký tự *cp, *cq;

PROC *p = get_proc(&freeList); if (p==0)

{ printf("kfork

thất bại\n");

trả về (PROC *)0;

p->ppid = đang chạy->pid; p->cha mẹ

= đang chạy;

p->trạng thái = SẴN SÀNG;

p->ưu tiên = 1; // tất cả ưu tiên quy trình MỚI = 1

// xóa tất cả các reg "đã lưu" trong kstack về 0

for (i=1; i<29; i++) // tất cả 28 ô = 0

p->kstack[SSIZE-i] = 0;

// đặt kstack tiếp tục thành goUmode trong ts.s

p->kstack[SSIZE-15] = (int)goUmode;

// để ksp đã lưu trỏ tới kstack[SSIZE-28]

p->ksp = &(p->kstack[SSIZE-28]);

// tải ảnh Umode vào bộ nhớ Umode

cp = (char *)&_binary_u1_start;

sử dụng = &_binary_u1_end - &_binary_u1_start;

cq = (char *)(0x800000 + (p->pid-1)*UIMAGE_SZIE);

memcpy(cq, cp, use);

// trở về VA 0 trong ảnh Umode;

p->kstack[SSIZE-1] = (int)VA(0); p->usp =

VA(UIMAGE_SIZE); // con trỏ ngăn xếp Umode trống

p->ucpsr = 0x10; // CPU.spsr=Umode

enqueue(&readyQueue, p);

printf("Proc %d đã phân nhánh một con %d\n", Running->pid, p->pid); printQ(readyQueue);

trả lại p;

Giải thích về kfork(): kfork() tạo một quy trình mới với hình ảnh chế độ Người dùng và nhập quy trình đó vào hàng đợi sẵn sàng.
Khi tiến trình mới bắt đầu chạy, đầu tiên nó sẽ tiếp tục trong kernel. Sau đó nó quay trở lại chế độ Người dùng để thực thi hình
ảnh Umode. Hàm kfork() hiện tại giống như trong Chap. 5 ngoại trừ phần hình ảnh chế độ Người dùng. Vì phần này rất quan trọng nên
chúng tôi sẽ giải thích chi tiết hơn. Để một tiến trình thực thi hình ảnh Umode của nó, chúng ta có thể đặt câu hỏi: làm thế nào
mà tiến trình đó kết thúc trong ReadyQueue? Trình tự các sự kiện phải như sau.

(1).

Nó thực hiện lệnh gọi hệ thống từ Umode bằng SWI #0, khiến nó vào kernel để thực thi trình xử lý SVC (trong ts.s), trong đó nó sử
dụng STMFD sp!, {r0–r12, lr} để lưu các thanh ghi Umode vào (trống) kstack, trở thành
Machine Translated by Google

208 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

-------------------------------------------------- ----------

|uLR|ur12 ur11 ur10 ur9 ur8 ur7 ur6 ur5 ur4 ur3 ur2 ur1|ur0|
-------------------------------------------------- ----------

-1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14

trong đó tiền tố u biểu thị các thanh ghi Umode và i có nghĩa là SSIZE-i. Nó cũng lưu Umode sp và cpsr vào PROC.usp và PROC.ucpsr.

Sau đó, nó gọi tswitch() để loại bỏ CPU, trong đó nó lại sử dụng STMFD sp!, {r0–r12, lr} để lưu các thanh ghi Kmode vào kstack. Điều

này thêm một khung nữa vào kstack, khung này sẽ trở thành

ksp

-------goUmode------------------------------------------ -|---

|KLR kr12 kr11 kr10 kr9 kr8 kr7 kr6 kr5 kr4 kr3 kr2 kr1 kr0|
-------------------------------------------------- -----------

-15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28

trong đó tiền tố k biểu thị các thanh ghi Kmode. Trong kstack PROC, kLR

= trong đó quá trình được gọi là tswitch() và đó là nơi nó sẽ tiếp tục, uLR = nơi quá

trình thực hiện cuộc gọi hệ thống và đó là nơi nó sẽ quay trở lại khi quay trở lại Umode.

Vì quá trình này chưa bao giờ thực sự chạy trước đó nên tất cả các thanh ghi CPU "đã lưu" khác không thành vấn đề, vì vậy chúng đều có thể được đặt thành 0.

Theo đó, chúng ta khởi tạo tiến trình mới kstack như sau.

1. Xóa tất cả các thanh ghi "đã lưu" trong kstack về 0

for (i=1; i<29; i++){ p->kstack[SSIZE-i] = 0; }

2. Đặt ksp đã lưu thành kstack[SSIZE-28]

p->ksp = &(p->kstack[SSIZE-28]);

3. Đặt kLR = goUmode, để p sẽ tiếp tục chuyển sang goUmode (tính bằng ts.s)

p->kstack[SSIZE-15] = (int)goUmode;

4. Đặt uLR thành VA(0), để p sẽ thực thi từ VA=0 trong Umode

p->kstack[SSIZE-1] = VA(0); // phần đầu của ảnh Umode

5. Đặt quy trình mới usp trỏ tới ustack TOP và ucpsr thành Umode

p->usp = (int *)VA(UIAMGE_SIZE); // cao cấp của ảnh Umode p->ucpsr = (int *)0x10;

// Thanh ghi trạng thái Umode

7.4.3.2 Thực thi hình ảnh chế độ người dùng Với

thiết lập này, khi quy trình mới bắt đầu chạy, trước tiên nó sẽ tiếp tục về goUmode (tính bằng ts.s), trong đó nó đặt Umode sp=PROC.

usp, cpsr=PROC.ucpsr. Sau đó nó thực thi

ldmfd sp!, {r0-r12, pc}^

điều này khiến nó thực thi từ uLR = VA(0) trong Umode, tức là từ đầu hình ảnh Umode với con trỏ ngăn xếp trỏ vào đầu cao của hình

ảnh Umode. Khi truy cập vào chúng tôi, nó gọi main() trong C. Để xác minh rằng quy trình thực sự đang thực thi trong Umode, chúng tôi

lấy thanh ghi cpsr của CPU và hiển thị chế độ hiện tại, phải là 0x10. Để kiểm tra khả năng bảo vệ bộ nhớ của MMU, chúng tôi cố gắng

truy cập các VA nằm ngoài phạm vi VA 1 MB của quy trình, ví dụ: 0x80200000, cũng như
Machine Translated by Google

7.4 Nhân hệ thống hỗ trợ các quy trình ở chế độ người dùng 209

VA trong không gian kernel, ví dụ 0x4000. Trong cả hai trường hợp, MMU sẽ phát hiện lỗi và tạo ra ngoại lệ hủy bỏ dữ liệu. Trong trình xử

lý data_abort, chúng tôi đọc các thanh ghi error_status và error_address của MMU để hiển thị nguyên nhân của ngoại lệ cũng như địa chỉ VA

gây ra ngoại lệ. Khi trình xử lý data_abort kết thúc, chúng tôi để nó quay trở lại PC-4, tức là bỏ qua hướng dẫn sai gây ra ngoại lệ

data_abort, cho phép quá trình tiếp tục. Trong một hệ thống thực, khi một tiến trình Umode phạm phải các ngoại lệ truy cập bộ nhớ, đó là một

vấn đề rất nghiêm trọng và thường khiến quá trình này bị chấm dứt. Về cách xử lý các trường hợp ngoại lệ do quá trình Umode gây ra nói

chung, người đọc có thể tham khảo Chap. 9 của Wang (2015) về xử lý tín hiệu.

7.4.4 Tập lệnh liên kết biên dịch hạt nhân

(2). tập lệnh mk: tạo hình ảnh hạt nhân và chạy nó trong QEMU

(cd USER; mku u1) # tạo tệp thực thi nhị phân u1.o
arm-none-eabi-as -mcpu=arm926ej-s ts.s -o ts.o
arm-none-eabi-gcc -c -mcpu=arm926ej-s tc -o tới
arm-none-eabi-ld -T t.ld ts.o tới u1.o -Ttext=0x10000 -o t.elf

arm-none-eabi-objcopy -O nhị phân t.elf t.bin qemu-


system-arm -M linh hoạtpb -m 256M -kernel t.bin

Trong tập lệnh liên kết, u1.o được sử dụng làm phần dữ liệu nhị phân thô trong ảnh hạt nhân. Đối với mỗi phần dữ liệu thô, trình liên kết

xuất các địa chỉ tượng trưng của nó, chẳng hạn như

_binary_u1_start, _binary_u1_end, _binary_u1_size

có thể được sử dụng để truy cập phần dữ liệu thô trong ảnh hạt nhân đã tải.

7.4.5 Trình diễn hạt nhân với quy trình chế độ người dùng

Hình 7.1 hiển thị màn hình đầu ra khi chạy chương trình C7.1. Khi một quá trình bắt đầu thực thi hình ảnh Umode, trước tiên nó sẽ nhận

được cpsr của CPU để xác minh rằng nó thực sự đang thực thi ở chế độ Người dùng (mode = 0x10). Sau đó nó cố gắng truy cập vào một số VA bên

ngoài không gian VA của nó. Như hình minh họa, mỗi VA không hợp lệ sẽ tạo ra một ngoại lệ hủy bỏ dữ liệu. Sau khi kiểm tra MMU để bảo vệ bộ

nhớ, nó sẽ phát hành các cuộc gọi tổng hợp để lấy pid và ppid. Sau đó, nó hiển thị một menu và yêu cầu lệnh thực thi. Mỗi lệnh đưa ra một

syscall, khiến tiến trình vào kernel thực thi chức năng syscall tương ứng trong kernel.

Sau đó, nó quay trở lại Umode và nhắc lệnh thực thi lại. Người đọc có thể chạy chương trình và nhập lệnh để kiểm tra các lệnh gọi hệ thống

trong hệ thống mẫu.

7.5 Hệ thống nhúng với các quy trình ở chế độ người dùng

Dựa trên chương trình ví dụ C7.1, chúng tôi đề xuất hai mô hình khác nhau cho các hệ thống nhúng để hỗ trợ nhiều chế độ Người dùng

quá trình.

7.5.1 Các quy trình trong cùng một miền

Thay vì tạo một ảnh Umode duy nhất, chúng ta có thể tạo nhiều ảnh Umode, ký hiệu là u1, u2, …, un. Sửa đổi tập lệnh liên kết để bao gồm tất

cả các hình ảnh Umode dưới dạng các phần dữ liệu thô riêng biệt trong hình ảnh hạt nhân. Khi hệ thống khởi động, tạo n tiến trình,
Machine Translated by Google

210 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

Hình 7.1 Trình diễn quy trình chế độ người dùng và lệnh gọi hệ thống

P1, P2,…, Pn, mỗi cái thực thi một image tương ứng ở chế độ User. Sửa đổi kfork() thành kfork(int i), tạo tiến trình Pi và tải
hình ảnh ui vào vùng bộ nhớ của tiến trình Pi. Trên các hệ thống dựa trên ARM, hãy sử dụng sơ đồ quản lý bộ nhớ đơn giản nhất
bằng cách phân bổ cho mỗi quy trình một vùng hình ảnh Umode 1 MB theo quy trình PID, ví dụ: P1 ở 8 MB, P2 ở 9 MB, v.v. Một số quy
trình có thể là định kỳ trong khi các quy trình khác có thể là sự kiện -điều khiển. Tất cả các quy trình đều chạy trong cùng một
không gian địa chỉ ảo [2 GB đến 2 GB + 1 MB] nhưng mỗi quy trình có một vùng bộ nhớ vật lý riêng biệt, tách biệt với các quy trình
khác và được bảo vệ bởi phần cứng MMU. Chúng tôi chứng minh một hệ thống như vậy bằng một ví dụ.

7.5.2 Trình diễn các quy trình trong cùng một miền

Trong hệ thống ví dụ này, chúng tôi tạo 4 ảnh Umode, ký hiệu là u1 đến u4. Tất cả các hình ảnh Umode được liên kết biên dịch với
cùng một địa chỉ ảo bắt đầu 0x80000000. Chúng thực thi cùng một hàm ubody(int i). Mỗi tiến trình gọi ubody(pid) với một số ID tiến
trình duy nhất để nhận dạng. Khi thiết lập các bảng trang quy trình, không gian hạt nhân được gán số miền 0. Tất cả các không gian
Umode được gán số miền 1. Khi một quá trình bắt đầu thực thi trong Umode, nó cho phép người dùng kiểm tra khả năng bảo vệ bộ nhớ
bằng cách thử truy cập ảo không hợp lệ. địa chỉ, điều này sẽ tạo ra lỗi bảo vệ bộ nhớ. Trong trình xử lý ngoại lệ hủy bỏ dữ liệu,
nó hiển thị các thanh ghi error_status và error_addr của MMU để hiển thị nguyên nhân ngoại lệ cũng như địa chỉ ảo bị lỗi. Sau đó,
mỗi tiến trình sẽ thực hiện một vòng lặp vô hạn, trong đó nó sẽ nhắc một lệnh và thực thi lệnh đó. Mỗi lệnh gọi một giao diện cuộc
gọi hệ thống, giao diện này đưa ra một cuộc gọi hệ thống, khiến quá trình thực thi chức năng gọi hệ thống trong kernel. Để thể
hiện các khả năng bổ sung của hệ thống, chúng tôi thêm các lệnh sau:

switch: nhập kernel để chuyển tiến trình; : nhập kernel để ngủ trên

ngủ tiến trình pid;

thức dậy: nhập kernel để đánh thức quá trình ngủ bằng pid;
Machine Translated by Google

7.5 Hệ thống nhúng với các quy trình ở chế độ người dùng 211

Nếu muốn, chúng tôi cũng có thể sử dụng chế độ ngủ/thức để triển khai các quy trình theo hướng sự kiện cũng như để hợp tác trong quy trình. ĐẾN

hỗ trợ và kiểm tra các lệnh Chế độ người dùng đã thêm, chỉ cần thêm chúng vào lệnh và giao diện tòa nhà.

#include "string.c"

#include "uio.c"

int ubody(int id)

dòng char[64]; int *va;

int pid = getpid();

int ppid = getppid();

int PA = getPA();

printf("kiểm tra khả năng bảo vệ bộ nhớ\n");

printf("thử VA=0x80200000 : ");

va = (int *)0x80200000; *va = 123; printf("thử

VA=0x1000 : "); va = (int *)0x1000;

*va = 456;

trong khi(1){

printf("Xử lý #%d trong Umode tại %x parent=%d\n", pid, PA, ppid);

umenu();

printf("nhập lệnh: "); ugetline(dòng);

uprintf("\n");

if (!strcmp(line, "switch")) // chỉ hiển thị các lệnh được thêm vào

uswitch();

if (!strcmp(dòng, "ngủ"))

bạn ngủ();

if (!strcmp(line, "wakeup"))

uwakeup();

/******* file u1.c ******/ // u2.c, u3.c u4.c tương tự nhau

#include "ucode.c"

main(){ ubody(1); }

Phần sau đây hiển thị các tệp hạt nhân (cô đọng). Đầu tiên, bảng định tuyến cuộc gọi tòa nhà, svc_handler(), được sửa đổi để hỗ trợ các cuộc gọi tòa

nhà được thêm vào.

int svc_handler(dễ bay hơi int a, int b, int c, int d)

int r;

chuyển đổi (a) {

trường hợp 0: r = kgetpid(); phá vỡ;

trường hợp 1: r = kgetppid(); phá vỡ;

trường hợp 2: r = kps(); phá vỡ;

trường hợp 3: r = kchname((char *)b); phá vỡ;

trường hợp 4: r = ktswitch(); phá vỡ;

trường hợp 5: r = ksleep(running->pid); phá vỡ;

trường hợp 6: r = kwakeup((int)b); trường phá vỡ;

hợp 92:r = kgetPA(); // return runnig->pgdir[2048]&0xFFF00000;

đang chạy->kstack[SSIZE-14] = r;

Trong tệp kernel tc, sau khi khởi tạo hệ thống, nó tạo ra 4 tiến trình, mỗi tiến trình có một hình ảnh Umode khác nhau. Trong kfork(pid), nó sử dụng

pid tiến trình mới để tải hình ảnh tương ứng vào vùng bộ nhớ tiến trình. Địa chỉ tải là P1 tại
Machine Translated by Google

212 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

8 MB, P2 ở mức 9 MB, P3 ở mức 10 MB và P4 ở mức 11 MB. Các bảng trang quy trình được thiết lập giống như trong Chương trình C7.1.

Mỗi tiến trình có một bảng trang trong vùng 6 MB. Trong bảng trang quy trình, mục nhập 2048 (VA = 0x80000000) trỏ đến vùng hình ảnh

Umode quy trình trong bộ nhớ vật lý.

/************ tập tin kernel tc *************/

#define VA(x) (0x80000000 + (u32)x)

int chính()

{ // khởi tạo kernel như trước

for (int i=1; i<=4, i++){ // tạo P1 thành P4

nĩa(i);

printf("P0 chuyển sang P1\n"); tswitch(); //

chuyển sang chạy P1 ==> không bao giờ quay lại nữa

PROC *kfork(int i) // i = 1 đến 4

// tạo PROC *p mới với pid=i như trước

u32 *addr = (char *)(0x800000 + (p->pid-1)*0x100000);

// copy image Umode ui của p sang addr

p->usp = (int *)VA(0x100000); // cao cấp 1MB VA

p->kstack[SSIZE-1] = VA(0); // bắt đầu VA=0

enqueue(&readyQueue, p);

trả lại p;

Hình 7.2 cho thấy kết quả đầu ra khi chạy chương trình C7.2. Nó cho thấy lệnh chuyển đổi đang chạy

quá trình từ P1 đến P2. Đầu đọc có thể chạy hệ thống và nhập các lệnh Chế độ người dùng khác để kiểm tra hệ thống.

7.5.3 Quy trình với các tên miền riêng lẻ

Trong mô hình hệ thống đầu tiên của C7.2, mỗi tiến trình có bảng trang riêng. Quá trình chuyển đổi yêu cầu chuyển đổi bảng trang, do

đó yêu cầu xóa bộ đệm TLB và I&D. Trong mô hình hệ thống tiếp theo, hệ thống hỗ trợ n < 16 quy trình chế độ người dùng chỉ với một

bảng trang. Trong bảng trang, 2048 mục đầu tiên xác định không gian địa chỉ ảo của chế độ lõi, được ID ánh xạ tới bộ nhớ vật lý khả

dụng (258 MB). Như trước đây, không gian kernel được chỉ định là miền 0.

Giả sử rằng tất cả hình ảnh chế độ người dùng đều có kích thước 1 MB. Mục 2048 + pid của bảng trang ánh xạ tới bộ nhớ vật lý 1 MB của

tiến trình Pi. Thuộc tính mục nhập pgdir được đặt thành 0xC1E | (pid 5), sao cho mỗi vùng hình ảnh của người dùng nằm trong một
miền duy nhất được đánh số từ 1 đến pid. Khi chuyển tiến trình sang Pi, thay vì chuyển bảng trang, nó gọi

set_domain( (1 << 2*pid) | 0x01);

đặt các bit truy cập của domain0 và domainPid thành b01 và xóa các bit truy cập của tất cả các miền khác thành 0, khiến chúng không

thể truy cập được. Bằng cách này, mỗi tiến trình chỉ chạy trong không gian địa chỉ ảo của riêng nó, được bảo vệ khỏi các tiến trình

khác. Đương nhiên, các tiến trình ở chế độ kernel vẫn có thể truy cập vào tất cả bộ nhớ vì nó đang chạy ở chế độ SVC đặc quyền. Hạn

chế của mô hình này là hệ thống chỉ có thể hỗ trợ 15 quy trình ở chế độ người dùng. Một nhược điểm khác là mỗi hình ảnh chế độ người

dùng phải được liên kết biên dịch với một địa chỉ ảo bắt đầu khác phù hợp với chỉ mục bảng trang của nó. Hệ thống mẫu C7.3 thực hiện

các quy trình với các miền riêng lẻ.

7.5.4 Trình diễn các quy trình với các miền riêng lẻ

Hình 7.3 cho thấy kết quả đầu ra khi chạy chương trình C7.3. Như hình minh họa, tất cả các quy trình chia sẻ cùng một bảng trang ở

0x4000 nhưng mỗi quy trình có một mục nhập khác nhau trong bảng trang. Hình này cũng cho thấy rằng mỗi tiến trình chỉ có thể truy cập vào
Machine Translated by Google

7.5 Hệ thống nhúng với các quy trình ở chế độ người dùng 213

Hình 7.2 Đầu ra của hệ thống mẫu C7.2

không gian VA riêng. Mọi nỗ lực truy cập VA bên ngoài không gian VA của nó sẽ tạo ra ngoại lệ data_abort do tên miền không hợp lệ.

Đĩa RAM 7.6

Trong các ví dụ lập trình trước, hình ảnh chế độ Người dùng được đưa vào dưới dạng phần dữ liệu thô trong hình ảnh hạt nhân. Khi hình

ảnh hạt nhân khởi động, nó dựa vào các địa chỉ tượng trưng do trình liên kết tạo ra để tải (sao chép) các hình ảnh chế độ Người dùng

khác nhau vào vị trí bộ nhớ của chúng. Lược đồ này hoạt động tốt nếu số lượng hình ảnh chế độ Người dùng ít. Có thể rất tẻ nhạt khi số

lượng ảnh chế độ Người dùng trở nên lớn, điều này cũng làm tăng kích thước ảnh kernel. Nếu chúng tôi dự định chạy một số lượng lớn quy

trình ở Chế độ người dùng với các hình ảnh khác nhau thì cần có cách tốt hơn để quản lý hình ảnh ở chế độ người dùng. Trong phần này,

chúng tôi sẽ trình bày cách sử dụng hệ thống tệp ramdisk để quản lý hình ảnh chế độ người dùng. Đầu tiên, chúng tôi tạo một đĩa RAM ảo và

định dạng nó dưới dạng hệ thống tệp. Sau đó, chúng tôi tạo hình ảnh ở chế độ Người dùng dưới dạng tệp ELF có thể thực thi được trong hệ

thống tệp ramdisk. Khi khởi động hệ thống, chúng tôi cũng tải hình ảnh ramdisk để kernel có thể truy cập được. Khi kernel khởi động, chúng

tôi di chuyển hình ảnh đĩa RAM đến một vùng bộ nhớ đã biết, ví dụ như ở mức 4 MB và sử dụng nó làm đĩa RAM trong bộ nhớ. Có hai cách để

làm cho hình ảnh đĩa RAM có thể truy cập được vào kernel.

(1).

Bao gồm hình ảnh đĩa ram dưới dạng phần dữ liệu thô: Chuyển đổi hình ảnh đĩa ram thành nhị phân và đưa nó làm phần dữ liệu thô trong

ảnh hạt nhân, tương tự như các hình ảnh Umode riêng lẻ trước đây. (2).

Là hình ảnh đĩa RAM ban đầu: Chạy QEMU với tùy chọn –initrd ramdisk, như trong

qemu-system-arm –M Verstilpb –m 256M –kernel t.bin –initrd ramdisk


Machine Translated by Google

214 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

Hình 7.3 Đầu ra của hệ thống mẫu C7.3

QEMU sẽ tải hình ảnh hạt nhân về 0x10000 (64 KB) và hình ảnh đĩa ram ban đầu về 0x4000000 (64 MB). Mặc dù

tài liệu QEMU nêu rõ rằng hình ảnh ramdisk ban đầu sẽ được tải tới 0x800000 (8 MB), nó thực sự phụ thuộc vào

kích thước bộ nhớ của máy ảo. Theo nguyên tắc chung, kích thước bộ nhớ của VM phải là lũy thừa của 2. Hình ảnh đĩa RAM

địa chỉ tải là kích thước bộ nhớ chia cho 2, với giới hạn trên là 128 MB. Sau đây liệt kê một số điều thường gặp

đã sử dụng kích thước bộ nhớ VM và địa chỉ tải của hình ảnh đĩa ram ban đầu.

Kích thước bộ nhớ VM (MB) Địa chỉ tải Ramdisk (MB)

16 số 8

32 16

64 32

128 64

512 128 (giới hạn trên)

Một cách đơn giản để tìm ra địa chỉ tải của hình ảnh đĩa RAM là kết xuất một chuỗi, ví dụ: "ramdisk started" vào

đầu của hình ảnh đĩa RAM. Khi kernel khởi động, hãy quét từng vùng bộ nhớ 1 MB để phát hiện chuỗi. Sau khi tải

đã biết địa chỉ, chúng ta có thể di chuyển hình ảnh đĩa RAM đến một vị trí bộ nhớ và sử dụng nó làm đĩa RAM. Để truy cập Umode

hình ảnh trong hệ thống tệp RAMdisk, chúng tôi thêm trình điều khiển RAMdisk để đọc-ghi các khối RAMdisk. Sau đó, chúng tôi phát triển tệp ELF

bộ nạp để tải các tập tin hình ảnh vào vùng bộ nhớ xử lý. Có một số hệ thống tập tin phổ biến được sử dụng trong các hệ thống nhúng.

Hầu hết các hệ thống nhúng đời đầu đều sử dụng hệ thống tệp Microsoft FAT. Vì chúng tôi sử dụng Linux làm nền tảng phát triển nên chúng tôi sẽ

sử dụng hệ thống tệp tương thích với Linux để tránh mọi chuyển đổi tệp không cần thiết. Vì lý do này, chúng ta sẽ sử dụng EXT2

hệ thống tập tin hoàn toàn tương thích với Linux. Người đọc có thể tham khảo (EXT2 2001; Card và cộng sự 1995; Cao và cộng sự 2007) để biết

Thông số kỹ thuật của hệ thống tệp EXT2. Trong phần sau đây, chúng tôi trình bày cách tạo hình ảnh hệ thống tệp EXT2 và sử dụng nó làm đĩa RAM.
Machine Translated by Google

Đĩa RAM 7.6 215

7.6.1 Tạo ảnh đĩa RAM

(1). Trong Linux, hãy chạy các lệnh sau (hoặc dưới dạng tập lệnh sh). Đối với hệ thống tệp EXT2 nhỏ, tốt hơn nên sử dụng tệp 1 KB
kích thước khối.

dd if=/dev/zero of=ramdisk bs=1024 count=1024 # tạo tập tin ramdisk

mke2fs –b 1024 ramdisk 1024 # định dạng nó dưới dạng 1 MB EXT2 FS

mount –o vòng lặp ramdisk /mnt # gắn nó như một thiết bị lặp # tạo thư

mkdir /mnt/bin mục /bin

số lượng /mnt # vượt qua nó

(2). Biên dịch liên kết các tệp nguồn chế độ Người dùng trong thư mục USER dưới dạng tệp thực thi ELF và sao chép chúng vào thư mục /bin của

ramdisk, như được hiển thị bằng tập lệnh mku sau.

arm-none-eabi-as -mcpu=arm926ej-s us.s -o us.o arm-none-eabi-gcc -c

-mcpu=arm926ej-s -o $1.o $1.c arm-none-eabi- ld -T u.ld us.o $1.o

-Ttext=0x80000000 -o $1 mount -o loop ../ramdisk /mnt cp $1 /mnt/bin

số lượng /mnt

(3). Chạy tập lệnh sh sau để tạo hình ảnh chế độ Người dùng và chuyển đổi ramdisk thành tệp đối tượng.

(cd USER; mku u1; mku u2) # giả sử các tập tin nằm trong thư mục USER

arm-none-eabi-objcopy –I nhị phân -O elf32-littlearm -B arm \

đĩa ram ramdisk.o

(4). Biên dịch liên kết các tệp kernel để bao gồm ramdisk.o dưới dạng phần dữ liệu thô trong ảnh kernel.

(5). Trong tệp tc, sao chép ramdisk.o vào một vị trí bộ nhớ và sử dụng nó làm đĩa RAM.

ký tự bên ngoài _binary_ramdisk_start, _binary_ramdisk_end; char *RAMdisk = (char

*)0x400000; // toàn cầu ở mức 4 MB

int chính()

char *cp, *cq = Đĩa RAM;

kích thước int;

cp = (char *)&_binary_ramdisk_start;

kích thước = &_binary_ramdisk_end - &_binary_ramdisk_start;

// sao chép hình ảnh ramdisk vào RAMdisk với dung lượng 4MB

memcpy(cq, cp, size);

fbuf_init();

kbd_init();

uart_init();

// kích hoạt SIC VIC khi ngắt thiết bị như trước

bộ đếm thời gian_init(); bộ đếm thời gian_start(0);

kernel_init(); // khởi tạo hạt nhân

printf("Đĩa RAM bắt đầu=%x size=%x\n", Đĩa RAM, kích thước); kfork("/bin/

u1"); // Tạo P1 bằng Umode image="/bin/u1" printf("P0 switch to P1\n");

tswitch(); // chuyển sang chạy P1; không bao giờ trở lại

}
Machine Translated by Google

216 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

Ngoài ra, chúng tôi cũng có thể dựa vào QEMU để tải hình ảnh ramdisk bằng tùy chọn –initrd trực tiếp, như trong

qemu-system-arm –M Verstilpb –m 128M –kernel t.bin –initrd ramdisk

Trong trường hợp đó, chúng tôi có thể di chuyển hình ảnh đĩa ram từ địa chỉ tải của nó (64 MB) sang 4 MB hoặc sử dụng trực tiếp

địa chỉ tải đĩa ram. Phần sau đây trình bày các chức năng I/O khối RAMdisk, về cơ bản là các chức năng sao chép bộ nhớ.

/********* Trình điều khiển I/O đĩa RAM **********/

#xác định BLKSZIE 1024

int get_block(u32 blk, char *buf) // đọc từ khối 1KB

char *cp = RAMdisk + blk*BLKSIZE;

memcpy(buf, cp, BLKSIZE);

int put_block(u32 blk, char *buf) // ghi vào khối 1KB

char *cp = RAMdisk + blk*BLKSIZE;

memcpy(cp, buf, BLKSIZE);

7.6.2 Trình tải tệp hình ảnh quy trình

Trình tải hình ảnh bao gồm hai phần. Phần đầu tiên là xác định vị trí tệp hình ảnh và kiểm tra xem nó có thể thực thi được hay không.

Phần thứ hai thực sự là tải nội dung thực thi của tệp hình ảnh vào bộ nhớ. Chúng tôi giải thích từng phần chi tiết hơn.

Phần 1 của trình tải hình ảnh: Trong hệ thống tệp EXT2, mỗi tệp được biểu thị bằng cấu trúc dữ liệu INODE duy nhất, chứa tất cả

thông tin của tệp. Mỗi INODE có một số inode (ino), là vị trí (tính từ 1) trong bảng INODE. Để tìm một tập tin cũng giống như tìm

INODE của nó. Thuật toán như sau.

(1). Đọc trong Superblock (khối 1) Kiểm tra số ma thuật (0xEF53) để xác minh đó thực sự là EXT2 FS.

(2). Đọc trong khối mô tả nhóm (khối 2) để truy cập vào bộ mô tả nhóm 0. Từ mục nhập bg_in-ode_table của bộ mô tả nhóm, hãy tìm số

khối bắt đầu của INODE, gọi nó là InodesBeginBlock.

(3). Đọc trong InodeBeginBlock để lấy INODE của thư mục gốc /, là inode thứ hai (ino=2) trong Bảng INODE (4). Mã hóa tên đường

dẫn thành các chuỗi thành phần và đặt số lượng thành phần là n. Ví dụ: nếu tên đường dẫn = /a/b/c thì các chuỗi thành phần là "a",

"b", "c", với n = 3. Biểu thị các thành phần theo tên[0], tên[1], …, tên[n 1].

(5). Bắt đầu từ INODE gốc trong (3), tìm kiếm name[0] trong (các) khối dữ liệu của nó. Mỗi khối dữ liệu của DIR INODE chứa

cấu trúc dir_entry của biểu mẫu

[ino rlen nlen TÊN] [ino rlen nlen TÊN] ……

trong đó NAME là một chuỗi các ký tự nlen (không có NULL ở cuối). Đối với mỗi khối dữ liệu, hãy đọc khối đó vào bộ nhớ và sử dụng

dir_entry *dp để trỏ đến khối dữ liệu được tải. Sau đó sử dụng nlen để trích xuất NAME dưới dạng chuỗi và so sánh nó với tên[0].

Nếu chúng không khớp, hãy chuyển sang dir_entry tiếp theo bằng cách

dp = (dir_entry *)((char *)dp + dp->rlen);

và tiếp tục cuộc tìm kiếm. Nếu tên[0] tồn tại, chúng ta có thể tìm thấy dir_entry và số inode của nó.
Machine Translated by Google

Đĩa RAM 7.6 217

(6). Sử dụng số inode (ino) để tính toán khối đĩa chứa INODE và offset của nó trong khối đó bằng

Thuật toán của Mailman (Wang 2015).

blk ¼ ð Þ ino 1 =ð BLKSIZE=kích thước của INODE ð ÞÞþInodesBeginBlock;

bù đắp ¼ ð %ð
Þ ino 1 BLKSIZE=kích thước của INODE ð Þ Þ ;

Sau đó đọc INODE của /a, từ đó chúng ta có thể xác định xem đó có phải là DIR hay không. Nếu /a không phải là DIR thì không thể có /a/b, vì vậy

việc tìm kiếm thất bại. Nếu /a là TRỰC TIẾP và có nhiều thành phần hơn để tìm kiếm, hãy tiếp tục tìm tên thành phần tiếp theo[1]. Các

vấn đề bây giờ trở thành: tìm kiếm name[1] trong INODE của /a, hoàn toàn giống với Bước (5).

(7). Vì Bước 5–6 sẽ được lặp lại n lần nên tốt hơn hết bạn nên viết hàm tìm kiếm

tìm kiếm u32(INODE *inodePtr, char *name)

// tìm kiếm tên trong khối dữ liệu của INODE này

// nếu tìm thấy, trả về ino của nó; ngược lại trả về 0

Sau đó, tất cả những gì chúng ta phải làm là gọi search() n lần, như được phác họa bên dưới.

Giả sử: n, tên[0], …., tên [n-1] là toàn cầu

INODE *ip trỏ vào INODE của /

cho (i=0; i<n; i++){

ino = tìm kiếm(ip, tên[i])

if (!ino){ // không tìm thấy tên[i], return 0;}

sử dụng ino để đọc trong INODE và để ip trỏ đến INODE

Nếu vòng tìm kiếm kết thúc thành công, ip phải trỏ vào INODE của tên đường dẫn. Sau đó chúng ta có thể kiểm tra loại tệp và tệp của nó

tiêu đề (nếu cần) để đảm bảo nó có thể thực thi được.

Phần 2 của trình tải hình ảnh: Từ INODE của tệp, chúng tôi biết kích thước và khối tệp của nó. Phần thứ hai của bộ nạp sẽ tải

nội dung thực thi của tập tin vào bộ nhớ. Bước này phụ thuộc vào loại tệp thực thi.

(1). Tệp thực thi nhị phân phẳng là một đoạn mã nhị phân duy nhất, được tải toàn bộ để thực thi trực tiếp. Cái này

có thể được thực hiện bằng cách chuyển đổi tất cả các tệp ELF thành tệp nhị phân trước. Trong trường hợp này, việc tải nội dung file cũng giống như việc tải file

khối.

(2). Định dạng tệp thực thi ELF: Tệp thực thi ELF bắt đầu bằng tiêu đề elf, theo sau là một hoặc nhiều chương trình

tiêu đề phần, được xác định là tiêu đề ELF và cấu trúc tiêu đề phần chương trình ELF.

struct elfhdr { // Tiêu đề tệp ELF

u32 phép thuật; // ELF_MAGIC 0x464C457F

yêu tinh u8[12];

loại u16;

máy u16;

phiên bản u32;

mục nhập u32;

phoffset u32; // offset byte của tiêu đề chương trình

bộ xáo trộn u32; // độ lệch byte của các phần

cờ u32;

u16 ehsize; // kích thước tiêu đề elf


Machine Translated by Google

218 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

u16 phentsize; // kích thước tiêu đề chương trình

u16phnum; // số tiêu đề phần chương trình

u16 shentsize;

u16 shnum;

u16 shstrndx;

};

struct proghdr { // ELF Tiêu đề phần chương trình // 1 = hình ảnh có thể

loại u32; tải // độ lệch byte của phần

bù u32; chương trình

u32 vaddr; // địa chỉ ảo

u32 paddr; kích // địa chỉ vật lý

thước tập tin u32; // số byte của phần chương trình

u32 memsize; // tải kích thước bộ nhớ

cờ u32; // Cờ R|W|Ex

căn chỉnh u32; // căn chỉnh

};

Người đọc có thể tham khảo (ELF 1995) để biết định dạng tệp ELF. Để có thông tin trợ giúp, người đọc cũng có thể sử dụng (Linux)

lệnh readelf để xem nội dung tệp ELF. Ví dụ,

tập tin readelfeSt:elf

hiển thị các tiêu đề (e), tiêu đề phần (S) và chi tiết phần (t) của tệp ELF. Đối với các tệp thực thi ELF, trình tải phải tải các phần

khác nhau của tệp ELF vào các địa chỉ ảo được chỉ định của chúng. Ngoài ra, mỗi phần được tải phải được đánh dấu bằng các thuộc tính R|W|

Ex thích hợp để bảo vệ. Ví dụ: các trang phần mã phải được đánh dấu cho RO (chỉ đọc), các trang phần dữ liệu phải được đánh dấu cho RW,

v.v. Nói chung, trình tải hình ảnh của chúng tôi có thể tải các tệp thực thi nhị phân hoặc ELF. Thuật toán của trình tải như sau. Người

đọc có thể tham khảo mã trình tải ELF trong tập tin Loadelf.c để biết chi tiết triển khai.

/************ Thuật toán trình tải ***********/

tìm INODE của tập tin; trả về 0 nếu thất bại;

đọc elf-header để kiểm tra xem đó có phải là tệp ELF hay không;

if (!ELF){ // giả sử tệp BINARY phẳng

xác định kích thước tập tin;

tải các khối tệp để xử lý vùng hình ảnh theo kích thước tệp;

trả về 1; // cho sự thành công

/*************** Tệp ELF ****************/

xác định vị trí (các) tiêu đề chương trình;

đối với mỗi tiêu đề chương trình, hãy làm{

lấy phần bù, địa chỉ tải và kích thước của phần;

tải phần vào địa chỉ ảo cho đến khi kích thước;

đặt thuộc tính R|W|Ex của phần trong các trang được tải;

trả về 1; // cho sự thành công

7.7 Quản lý quy trình

7.7.1 Tạo quy trình

Hầu hết các hệ thống nhúng được thiết kế cho các môi trường ứng dụng cụ thể. Một hệ thống nhúng điển hình bao gồm một số quy trình cố

định. Mỗi tiến trình là một đơn vị thực thi độc lập, không có bất kỳ mối quan hệ hay tương tác nào với các tiến trình khác. Đối với các

hệ thống như vậy, hàm kfork() hiện tại tạo ra một quy trình để thực thi một hàm cụ thể là
Machine Translated by Google

7.7 Quản lý quy trình 219

đủ. Để hỗ trợ các quy trình ở chế độ người dùng động, chúng tôi sẽ mở rộng kernel để áp đặt mối quan hệ cha-con giữa các quy trình.

Khi kernel khởi động, nó chạy tiến trình ban đầu P0, tiến trình này được tạo thủ công hoặc tạo ra bằng bạo lực.

Sau đó, mọi tiến trình khác được tạo ra bởi

int newpid = kfork(char *tên tệp, ưu tiên int);

tạo ra một quy trình mới với mức độ ưu tiên được chỉ định để thực thi tên tệp hình ảnh Umode. Khi tạo một tiến trình mới, người

tạo là tiến trình cha và tiến trình mới được tạo là tiến trình con. Trong cấu trúc PROC, trường ppid ghi pid tiến trình cha và con

trỏ cha trỏ tới PROC cha. Do đó, các tiến trình tạo thành một cây phả hệ với P0 là gốc.

7.7.2 Chấm dứt quy trình

Trong một hệ thống đa nhiệm với các quy trình động, một quy trình có thể chấm dứt hoặc chết, đây là thuật ngữ phổ biến để chỉ việc chấm

dứt quy trình. Một tiến trình có thể kết thúc theo hai cách:

Chấm dứt thông thường: Quá trình đã hoàn thành nhiệm vụ của nó, có thể không cần thiết nữa trong một thời gian dài. Để bảo tồn tài

nguyên hệ thống, chẳng hạn như cấu trúc và bộ nhớ PROC, quy trình gọi kexit(int exitValue) trong kernel để tự kết thúc, đó là trường

hợp chúng ta đang thảo luận ở đây.

Chấm dứt bất thường: Quá trình chấm dứt do một ngoại lệ, khiến quá trình không thể tiếp tục.

Trong trường hợp này, nó gọi kexit(value) với một giá trị duy nhất xác định ngoại lệ.

Trong cả hai trường hợp, khi một tiến trình kết thúc, cuối cùng nó sẽ gọi kexit() trong kernel. Thuật toán chung của kexit() là như
theo sau.

/*************** Thuật toán kexit *******************/

kexit(int exitValue)

1. xóa bối cảnh chế độ người dùng của quá trình, ví dụ: đóng bộ mô tả tệp, giải phóng tài nguyên,

giải phóng bộ nhớ hình ảnh ở chế độ người dùng, v.v.

2. loại bỏ các tiến trình con, nếu có. 3. ghi lại exitValue

trong PROC.exitCode để cha mẹ lấy.

4. trở thành ZOMBIE (nhưng không giải phóng PROC)

5. đánh thức cha mẹ và, nếu cần, cả quy trình INIT P1. 6. chuyển tiến trình bỏ CPU

Cho đến nay, mô hình hệ thống của chúng tôi chưa hỗ trợ hệ thống tệp. Do sơ đồ phân bổ bộ nhớ đơn giản, mỗi quy trình chạy trong

vùng bộ nhớ 1 MB chuyên dụng, việc phân bổ hình ảnh chế độ người dùng cũng không đáng kể. Khi một quá trình kết thúc, vùng bộ nhớ chế

độ người dùng của nó sẽ không được sử dụng cho đến khi một quá trình có cùng pid được tạo lại. Vì vậy, chúng ta bắt đầu thảo luận về

Bước 2 của kexit(). Vì mỗi tiến trình là một thực thể thực thi độc lập nên nó có thể kết thúc bất cứ lúc nào. Nếu một tiến trình có

tiến trình con kết thúc trước thì tất cả tiến trình con của tiến trình đó sẽ không còn cha mẹ nữa, tức là chúng trở thành trẻ mồ côi.

Câu hỏi đặt ra là: phải làm gì với những đứa trẻ mồ côi như vậy? Trong xã hội loài người, họ sẽ được gửi đến nhà bà ngoại. Nhưng nếu

bà đã chết rồi thì sao? Theo lý do này, ngay lập tức trở nên rõ ràng rằng phải có một quy trình không nên kết thúc nếu vẫn còn tồn tại

các quy trình khác. Nếu không, mối quan hệ tiến trình cha-con sẽ sớm bị phá vỡ. Trong tất cả các hệ thống giống Unix, quy trình P1, còn

được gọi là quy trình INIT, được chọn để đóng vai trò này. Khi một tiến trình chết, nó sẽ gửi tất cả những đứa trẻ mồ côi, dù còn

sống hay đã chết, đến P1, tức là trở thành con của P1. Theo đó, chúng ta cũng sẽ chỉ định P1 là một quá trình như vậy. Vì vậy, P1 sẽ

không chết nếu vẫn còn tồn tại các tiến trình khác. Vấn đề còn lại là làm thế nào để thực hiện Bước 2 một cách hiệu quả. Để một quá

trình sắp chết có thể loại bỏ các con, quá trình này phải có khả năng xác định liệu nó có con hay không và nếu nó có con thì phải nhanh

chóng tìm thấy tất cả các con. Nếu số lượng quy trình nhỏ, cả hai câu hỏi đều có thể được trả lời một cách hiệu quả bằng cách tìm

kiếm tất cả các cấu trúc PROC. Ví dụ: để xác định xem một quy trình có bất kỳ quy trình con nào hay không, chỉ cần tìm kiếm PROC để

tìm bất kỳ quy trình nào không MIỄN PHÍ và ppid của nó khớp với pid quy trình. Nếu số lượng quy trình lớn, ví dụ theo thứ tự hàng

trăm, sơ đồ tìm kiếm đơn giản này sẽ quá chậm. Vì lý do này, hầu hết các nhân hệ điều hành lớn đều theo dõi các mối quan hệ của tiến

trình bằng cách duy trì một cây tiến trình.


Machine Translated by Google

220 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

7.7.3 Cây phả hệ quy trình

Thông thường, cây tiến trình được triển khai dưới dạng cây nhị phân bởi một cặp con trỏ con và con trỏ anh chị em trong mỗi PROC, như trong

struct proc *con, *anh chị em, *cha mẹ;

trong đó đứa trẻ chỉ vào đứa con đầu tiên của một quá trình và anh chị em chỉ vào danh sách những đứa trẻ khác có cùng cha mẹ. Để thuận

tiện, mỗi PROC cũng sử dụng một con trỏ cha trỏ tới cha của nó. Với cây quy trình, việc tìm thấy các tiến trình con của một tiến trình sẽ dễ

dàng hơn nhiều. Đầu tiên, đi theo con trỏ con tới PROC con đầu tiên. Sau đó đi theo các con trỏ anh em để duyệt qua các PROC anh em. Để gửi

tất cả con đến P1, chỉ cần tách danh sách con và nối nó vào danh sách con của P1 (đồng thời thay đổi con trỏ ppid và con trỏ cha của chúng).

Do số lượng PROC nhỏ trong tất cả các hệ thống mẫu của cuốn sách này nên chúng tôi không triển khai cây quy trình. Điều này được để lại như

một bài tập lập trình. Trong cả hai trường hợp, việc thực hiện Bước 2 của kexit() đều khá dễ dàng.

Mỗi PROC có trường Mã thoát 2 byte, ghi lại trạng thái thoát của quá trình. Trong Linux, byte cao của exitCode là exitValue và byte thấp

là số ngoại lệ khiến quá trình kết thúc. Vì một tiến trình chỉ có thể chết một lần nên chỉ một trong các byte có ý nghĩa. Sau khi ghi

exitValue trong PROC.exitCode, quy trình sẽ thay đổi trạng thái của nó thành ZOMBIE nhưng không giải phóng PROC. Sau đó, tiến trình gọi

kwakeup() để đánh thức tiến trình cha của nó và cả P1 nếu nó đã gửi bất kỳ tiến trình mồ côi nào đến P1. Hành động cuối cùng của một quá

trình sắp chết là gọi tswitch() lần cuối. Sau đó, về cơ bản, quy trình đã chết nhưng vẫn còn một xác chết ở dạng ZOMBIE PROC, quy trình này

sẽ được chôn cất (đặt MIỄN PHÍ) bởi quy trình gốc thông qua thao tác chờ.

7.7.4 Chờ kết thúc tiến trình con

Bất cứ lúc nào, một tiến trình có thể gọi hàm kernel

int pid = kwait(int *trạng thái)

để chờ đợi một đứa trẻ ZOMBIE. Nếu thành công, pid được trả về là pid con ZOMBIE và trạng thái chứa Mã thoát của pid con ZOMBIE. Ngoài

ra, kwait() cũng giải phóng ZOMBIE PROC trở lại freeList, cho phép nó được sử dụng lại cho một quy trình khác. Thuật toán kwait là

int kwait(int *trạng thái)

if (người gọi không có con)

trả về -1 nếu có lỗi;

trong khi(1){ // người gọi có con

tìm kiếm một (bất kỳ) đứa trẻ ZOMBIE nào;

if (tìm thấy một con ZOMBIE){ lấy pid con

ZOMBIE; sao chép Mã thoát con

ZOMBIE sang trạng thái *; phát hành PROC con vào freeList dưới

dạng MIỄN PHÍ; trả về pid con ZOMBIE;

ksleep(đang chạy); // ngủ trên địa chỉ PROC của nó

Trong thuật toán kwait, quy trình trả về 1 nếu có lỗi nếu nó không có con. Ngược lại, nó sẽ tìm kiếm (bất kỳ) con ZOMBIE nào.

Nếu nó tìm thấy một ZOMBIE con, nó sẽ thu thập pid con ZOMBIE và mã thoát, giải phóng PROC ZOMBIE tới freeList và trả về pid con ZOMBIE. Nếu

không, nó sẽ chuyển sang chế độ ngủ trên địa chỉ PROC của chính nó, chờ đợi một địa chỉ con kết thúc.

Tương ứng, khi một tiến trình kết thúc, nó phải đưa ra
Machine Translated by Google

7.7 Quản lý quy trình 221

kwakeup(cha mẹ); // cha mẹ là con trỏ tới PROC cha

để đánh thức cha mẹ. Khi tiến trình cha thức dậy trong kwait(), nó sẽ tìm thấy một tiến trình con đã chết khi thực hiện lại vòng lặp

while. Lưu ý rằng mỗi lệnh gọi kwait() chỉ xử lý một con ZOMBIE, nếu có. Nếu một tiến trình có nhiều tiến trình con, nó có thể phải gọi

kwait() nhiều lần để loại bỏ tất cả các tiến trình con đã chết. Ngoài ra, một tiến trình có thể kết thúc trước tiên mà không cần đợi bất kỳ

tiến trình con nào đã chết. Khi một tiến trình chết, tất cả các tiến trình con của nó đều trở thành con của P1. Như chúng ta sẽ thấy sau,

trong một hệ thống thực, P1 thực hiện một vòng lặp vô hạn, trong đó nó liên tục chờ đợi những đứa trẻ đã chết, bao gồm cả những đứa trẻ mồ

côi được nhận làm con nuôi. Thay vì ngủ/thức, chúng ta cũng có thể sử dụng semaphore để triển khai các hàm kwait()/kexit(). Các biến thể của

thao tác kwait() bao gồm waitpid và waitid của Linux (Linux Man Page 2016), cho phép một tiến trình chờ một đứa trẻ cụ thể bằng pid với nhiều tùy chọn.

7.7.5 Fork-Exec trong Unix/Linux

Khi một tiến trình thực thi, nó có thể cần lưu thông tin do quá trình thực thi tạo ra. Một ví dụ điển hình là nhật ký ghi lại các sự kiện

quan trọng xảy ra trong quá trình thực thi. Nhật ký có thể được sử dụng để theo dõi quá trình thực thi quy trình nhằm gỡ lỗi trong trường

hợp có sự cố. Một quy trình cũng có thể cần đầu vào để kiểm soát đường dẫn thực thi của nó. Việc lưu và truy xuất thông tin cần có sự hỗ

trợ của hệ thống tệp. Nhân hệ điều hành thường cung cấp hỗ trợ hệ thống tệp cơ bản để cho phép các tiến trình thực hiện các thao tác với

tệp. Trong các hệ thống như vậy, môi trường thực thi của mỗi quy trình bao gồm cả bối cảnh thực thi và khả năng truy cập tệp của nó. Cơ

chế kfork() hiện tại chỉ có thể tạo các tiến trình để thực thi các hình ảnh khác nhau chứ không cung cấp bất kỳ phương tiện nào cho các

thao tác với tệp. Để hỗ trợ cái sau, chúng ta cần một cách khác để tạo và chạy các tiến trình.

Trong Unix/Linux, lệnh gọi hệ thống

int pid = nĩa();

tạo ra một tiến trình con với hình ảnh Umode giống hệt với tiến trình cha. Ngoài ra, nó cũng chuyển tất cả các bộ mô tả tệp đã mở cho tệp

con, cho phép tệp con kế thừa cùng môi trường hoạt động tệp của tệp cha. Nếu thành công, fork() trả về pid của tiến trình con. Ngược lại,

nó trả về 1. Khi tiến trình con chạy, nó sẽ trở về hình ảnh Umode của chính nó và pid trả về là 0. Điều này cho phép chúng ta viết các

chương trình ở chế độ Người dùng dưới dạng

int pid = nĩa(); nếu (pid) // rẽ nhánh một tiến trình con

// cha mẹ thực thi phần này;

else{ // con thực hiện phần này;

Đoạn mã sử dụng pid trả về để phân biệt giữa tiến trình cha và tiến trình con. Khi trở về từ fork(), tiến trình con thường sử dụng lệnh

gọi hệ thống

int r = exec(char *tên tệp, char *para-list);

để thay đổi hình ảnh thực thi của nó thành một tệp khác, chuyển dưới dạng tham số para-list sang hình ảnh mới khi bắt đầu thực thi. Nếu

thành công, exec() chỉ thay thế hình ảnh Umode gốc bằng hình ảnh mới. Nó vẫn là quá trình tương tự nhưng với hình ảnh Umode khác. Điều này

cho phép một quá trình thực hiện các chương trình khác nhau. Fork và exec có thể được gọi là cốt lõi của Unix/Linux vì hầu hết mọi hoạt

động đều phụ thuộc vào fork-exec. Ví dụ: khi người dùng nhập dòng lệnh có dạng

cmdLine = "cmd arg1 arg2 …. argn"

quá trình sh phân nhánh một đứa trẻ và đợi đứa trẻ kết thúc. Tiến trình con sử dụng exec để thay đổi hình ảnh của nó thành tệp cmd,

chuyển arg1 sang argn làm tham số cho hình ảnh mới. Khi tiến trình con kết thúc, nó sẽ đánh thức sh cha, lệnh này sẽ nhắc lệnh khác, v.v.

Lưu ý rằng fork-exec tạo một tiến trình để thực thi một hình ảnh mới theo hai bước. Ưu điểm chính của mô hình fork-exec là gấp đôi. Đầu

tiên, fork tạo ra một đứa trẻ có hình ảnh giống hệt nhau. Điều này giúp loại bỏ
Machine Translated by Google

222 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

nhu cầu truyền thông tin qua các không gian địa chỉ khác nhau giữa cha mẹ và con. Thứ hai, trước khi thực thi hình ảnh mới, trẻ có thể

kiểm tra các tham số dòng lệnh để thay đổi môi trường thực thi của nó cho phù hợp với nhu cầu riêng của mình.

Ví dụ: tiến trình con có thể chuyển hướng đầu vào tiêu chuẩn (stdin) và đầu ra (stdout) của nó sang các tệp khác nhau.

7.7.6 Triển khai Fork

Việc thực hiện fork-exec khá đơn giản. Thuật toán của fork() là

/************************ Thuật toán fork() ********************** ****/

1. lấy PROC cho con và khởi tạo nó, ví dụ ppid = pid cha, mức độ ưu tiên=1, v.v. 2. sao chép

hình ảnh Umode gốc sang con, sao cho hình ảnh Umode của chúng giống hệt nhau; 3. sao chép

(một phần) kstack mẹ sang kstack con; Đảm bảo rằng đứa trẻ trở lại như cũ

địa chỉ ảo là địa chỉ gốc nhưng ở hình ảnh Umode của chính nó;

4. sao chép usp cha và spsr sang con; 5.

đánh dấu PROC con là SẴN SÀNG và nhập nó vào hàng đợi sẵn sàng; 6.

trả về pid con;

Phần sau đây hiển thị mã fork() triển khai thuật toán fork

ngã ba int()

int tôi;

ký tự *PA, *CA;

PROC *p = get_proc(&freeList); if (p==0)

{ printf("fork thất bại\n"); trả về -1; } p->ppid = đang chạy->pid; p->cha mẹ

= đang chạy;

p->trạng thái = SẴN SÀNG;

p->ưu tiên = 1;

PA = (char *)running->pgdir[2048] & 0xFFFF0000; // cha mẹ Umode PA

CA = (char *)p->pgdir[2048] & 0xFFFF0000; memcpy(CA, PA, // con Umode PA

0x100000); // sao chép hình ảnh Umode 1 MB

for (i=1; i <= 14; i++){ // sao chép 14 mục dưới cùng của kstack

p->kstack[SSIZE-i] = đang chạy->kstack[SSIZE-i];

p->kstack[SSIZE - 14] = 0; // trả về con pid = 0

p->kstack[SSIZE-15] = (int)goUmode; // con tiếp tục goUmode

p->ksp = &(p->kstack[SSIZE-28]); // con đã lưu ksp

p->usp = đang chạy->usp; // giống usp với cha mẹ

p->ucpsr = đang chạy->ucpsr; // giống spsr với cha mẹ

enqueue(&readyQueue, p);

trả về p->pid;

Chúng tôi giải thích chi tiết mã fork() trong chế độ. Khi cha thực thi fork() trong kernel, nó đã lưu các thanh ghi Umode trong

kstack bằng stmfd sp!, {r0–r12, LR} và thay thế LR đã lưu bằng địa chỉ trả về thích hợp cho Umode. Do đó, đáy kstack của nó chứa
Machine Translated by Google

7.7 Quản lý quy trình 223

-1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14


-------------------------------------------------- -------------

|uLR ur12 ur11 ur10 ur9 ur8 ur7 ur6 ur5 ur4 ur3 ur2 ur1 ur0=0|
-------------------------------------------------- -------------

được sao chép vào cuối kstack của trẻ. 14 mục này sẽ được trẻ sử dụng để quay lại Umode khi

thực thi ldmfs sp!, {r0–12, pc}^ trong goUmode. LR được sao chép ở mục 1 cho phép trẻ quay trở lại cùng VA như

cha mẹ, tức là với cùng một tòa nhà pid = fork(). Để đứa trẻ trả về pid = 0, r0 đã lưu ở mục 14 phải được đặt thành

0. Để phần tử con tiếp tục hoạt động trong kernel, chúng tôi gắn khung ngăn xếp TIẾP TỤC vào kstack của phần tử con để nó tiếp tục khi nó

được lên kế hoạch để chạy. Khung ngăn xếp được thêm vào phải nhất quán với phần TIẾP TỤC của tswitch(). Khung kstack được thêm vào là

được hiển thị bên dưới và trẻ đã lưu điểm ksp vào mục 28.

-15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28
-------------------------------------------------- ---------------

|goUmode kr12 kr11 kr10 kr9 kr8 kr7 kr6 kr5 kr4 kr3 kr2 kr1 kr0 |
----|---------------------------------------------------------- -------------|---
klr ksp

Vì đứa trẻ tiếp tục chạy trong kernel nên tất cả các thanh ghi Kmode "đã lưu" không thành vấn đề, ngoại trừ sơ yếu lý lịch klr khi vào

15, được đặt thành goUmode. Khi đứa trẻ chạy, nó sử dụng khung kstack TIẾP TỤC để thực thi trực tiếp goUmode. Sau đó nó

thực thi goUmode với khung ngăn xếp tòa nhà được sao chép, khiến nó trở về cùng VA với cha mẹ nhưng ở chính nó

vùng nhớ có giá trị trả về là 0.

7.7.7 Triển khai Exec

Exec cho phép một tiến trình thay thế hình ảnh Umode của nó bằng một tệp thực thi khác. Chúng tôi giả định rằng tham số exec(char

*cmdlie) là một dòng lệnh có dạng

cmdline = "cmd arg1 arg2 … argn";

trong đó cmd là tên tệp của chương trình thực thi. Nếu cmd bắt đầu bằng / thì đó là tên đường dẫn tuyệt đối. Nếu không nó là một

tập tin trong thư mục mặc định /bin. Nếu thực thi thành công, quá trình sẽ quay trở lại Umode để thực thi tệp mới, với từng tệp

chuỗi mã thông báo làm tham số dòng lệnh. Tương ứng với dòng lệnh, chương trình cmd có thể được viết dưới dạng

main(int argc, char *argv[]){ }

trong đó argc = n + 1 và argv là một mảng con trỏ chuỗi kết thúc null, mỗi con trỏ tới một chuỗi mã thông báo có dạng

argv = [ * * *
| | | … … | * |0]
"cmd" "arg1" "arg2" …… "arg"

Phần sau đây trình bày thuật toán và cách thực hiện thao tác exec.

/****************** Thuật toán thực thi *******************/

1. tìm nạp cmdline từ không gian Umode;

2. mã hóa cmdline để lấy tên tệp cmd;


3. kiểm tra xem tệp cmd có tồn tại và có thể thực thi được không; trả về 1 nếu thất bại;

4. tải tập tin cmd vào vùng hình ảnh Umode;


Machine Translated by Google

224 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

5. sao chép cmdline vào phần cao nhất của usatck, ví dụ: vào x = high

end 128; 6. khởi tạo lại khung kstack tòa nhà để trở về VA = 0 trong
Umode; 7. trả về x;

Mã exec() sau đây triển khai thuật toán exec ở trên.

int exec(char *cmdline) // cmdline=VA trong Uspace

int i, upa, usp; char

*cp, kline[128], file[32], tên file[32];

PROC *p = đang chạy;

strcpy(kline, cmdline); // tìm nạp cmdline vào không gian kernel // lấy mã thông báo đầu tiên

của kline làm tên tệp

cp = kline; tôi = 0;

while(*cp != ' '){

tên tệp [i] = *cp;

tôi++; cp++;

tên tệp[i] = 0;

tập tin[0] = 0;

if (tên tệp [0] != '/') // nếu tên file tương đối

strcpy(file, "/bin/"); // tiền tố có /bin/

strcat(tập tin, tên tập tin);

upa = p->pgdir[2048] & 0xFFFF0000; // PA của ảnh Umode

// trình tải trả về 0 nếu tệp không tồn tại hoặc không thể thực thi được

if (!loadelf(file, p))

trả về -1;

// sao chép cmdline lên cấp cao nhất của Ustack trong ảnh Umode

usp = upa + 0x100000 - 128; // giả sử cmdline len < 128 strcpy((char *)usp, kline); p->usp

= (int *)VA(0x100000 - 128); // sửa khung

syscall trong kstack để trở về VA=0 của hình ảnh mới //

xóa Umode reg r1-r12

cho (i=2, i<14; i++)

p->kstack[SSIZE – i] = 0;

p->kstack[SSIZE-1] = (int)VA(0); // trả về uLR = VA(0)

trả về (int)p->usp; // sẽ thay thế r0 đã lưu trong kstack

Khi quá trình thực thi bắt đầu từ us.s trong Umode, r0 chứa p->usp, trỏ đến dòng lệnh gốc trong ngăn xếp Umode.

Thay vì gọi trực tiếp main(), nó gọi hàm khởi động C main0(char *cmdline), hàm này phân tích cmdline thành argc và argv, đồng thời

gọi main(argc, argv). Do đó, chúng ta có thể viết mọi chương trình Umode ở dạng chuẩn sau như thường lệ.

#include "ucode.c"

main(int argc, char *argv[ ]){………}

Sau đây liệt kê các đoạn mã của main0(), đóng vai trò tương tự như tệp khởi động C tiêu chuẩn crt0.c.

int argc; char *argv[32]; // giả sử tối đa 32 token trong cmdline int parg(char *line)

char *cp = dòng; argc = 0; while (*cp !=

0){ while (*cp == if (*cp !

= 0) ' ') *cp++ = 0; // bỏ qua chỗ trống

// bắt đầu mã thông báo


Machine Translated by Google

7.7 Quản lý quy trình 225

argv[argc++] = cp; // được trỏ bởi argv[ ]


'
trong khi (*cp != ' && *cp != 0) // quét ký tự mã thông báo

cp++;

nếu (*cp != 0)

*cp = 0; // kết thúc mã thông báo

khác

phá vỡ; // kết thúc dòng

cp++; // tiếp tục quét

argv[argc] = 0; // argv[argc]=0

main0(char *cmdline)

uprintf("main0: cmdline = %s\n", cmdline);

ParseArg(cmdline);

chính(argc, argv);

7.7.8 Trình diễn Fork-Exec

Chúng tôi trình diễn một hệ thống hỗ trợ các quy trình động với các chức năng fork, exec, wait và exit bằng hệ thống mẫu
C7.4. Vì tất cả các thành phần hệ thống đã được đề cập và giải thích trước đó nên chúng tôi chỉ hiển thị tệp tc chứa
hàm main(). Để chứng minh exec, chúng tôi cần một tệp hình ảnh Umode khác. Tệp u2.c giống hệt với u1.c, ngoại trừ việc
nó hiển thị bằng tiếng Đức (chỉ để giải trí).

/****************** file tc của chương trình C7.4 *******************/

#include "uart.c"

#include "kbd.c"

#include "timer.c"

#include "vid.c"

#include "ngoại lệ.c" #include

"queue.c"

#include "svc.c"

#include "kernel.c"

#include "chờ.c"

#include "fork.c" // kfork() và fork()

#include "exec.c" // exec()#include "disk.c" // Trình điều khiển đĩa RAM

#include "loadelf.c" // trình tải tệp ELF

ký tự bên ngoài _binary_ramdisk_start, _binary_ramdisk_end;

int chính()

ký tự *cp, *cq;

int kích

thước; cp = đĩa = &_binary_ramdisk_start; dsize =

&_binary_ramdisk_end - &_binary_ramdisk_start;

cq = (char *)0x400000;

memcpy(cq, cp, dsize);

fbuf_init();

printf("Chào mừng đến với WANIX in Arm\n");

kbd_init();

uart_init();
Machine Translated by Google

226 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

bộ đếm thời gian_init();

bộ đếm thời

gian_start(0); kernel_init();

mở khóa(); // kích hoạt các ngắt IRQ

printf("RAMdisk start=%x size=%x\n", disk, dsize); kfork("/bin/u1"); // cteate

P1 để thực thi image u1

trong khi(1){ // P0 là tiến trình nhàn rỗi

nếu (sẵn sàng)

tswitch();

Hình 7.4a hiển thị kết quả đầu ra của việc chạy lệnh fork và switch. Như hình minh họa, P1 chạy ở địa chỉ vật lý PA = 0x800000 (8

MB). Khi nó phân nhánh một con P2 ở PA = 0x900000 (9 MB), nó sẽ sao chép cả hình ảnh Umode và kstack cho con đó. Sau đó nó quay về

Umode với pid con = 2. Lệnh switch khiến P1 vào kernel để chuyển tiến trình sang P2, P2 quay về Umode với pid con = 0, chỉ ra rằng

đó là con rẽ nhánh. Bạn đọc có thể kiểm tra các thao tác thoát và chờ như sau.

(1) Trong khi P2 chạy, nhập lệnh chờ. P2 sẽ đưa ra một lệnh chờ để thực thi kwait() trong kernel. Vì P2 không có

bất kỳ con nào, nó trả về -1 nếu không có lỗi con.

(2) Trong khi P2 chạy, nhập lệnh thoát và nhập giá trị thoát, ví dụ 1234. P2 sẽ đưa ra lệnh gọi tòa nhà exit(1234) để kết thúc trong

kernel. Trong kexit(), nó ghi lại exitValue trong PROC.exitCode của nó, trở thành ZOMBIE và cố gắng đánh thức cha mẹ của nó. Vì

P1 chưa ở trạng thái chờ nên lệnh đánh thức của P2 sẽ không có hiệu lực. Sau khi trở thành ZOMBIE, P2 không thể chạy được nữa

nên nó chuyển quy trình, khiến P1 tiếp tục chạy. Trong khi P1 chạy, nhập lệnh chờ. P1 sẽ nhập kernel để thực thi kwait(). Vì P2

đã kết thúc nên P1 sẽ tìm P2 con ZOMBIE mà không cần đi ngủ. Nó giải phóng ZOMBIE PROC và trả về pid con đã chết = 2, cũng như

giá trị thoát của nó.

(3) Ngoài ra, P1 cha có thể đợi trước và P2 con thoát ra sau. Trong trường hợp đó, P1 sẽ đi ngủ trong kwait() cho đến khi được P2

đánh thức khi P2 (hoặc bất kỳ đứa trẻ nào) kết thúc. Vì vậy, các lệnh chờ đợi của cha mẹ và thoát ra của con cái không quan trọng.

Hình 7.4b cho thấy kết quả đầu ra của việc thực thi lệnh exec với các tham số dòng lệnh. Như hình vẽ cho thấy, đối với
dòng lệnh

u2 một hai ba

quá trình thay đổi hình ảnh Umode của nó thành tệp u2. Khi quá trình thực thi hình ảnh mới bắt đầu, các tham số dòng lệnh được

truyền vào dưới dạng chuỗi argv[] với argc = 4.

7.7.9 Sh đơn giản để thực thi lệnh

Với fork-exec, chúng ta có thể chuẩn hóa việc thực thi các lệnh của người dùng bằng một lệnh sh đơn giản. Đầu tiên, chúng tôi biên dịch trước main0.c dưới

dạng crt0.o và đặt nó vào thư viện liên kết dưới dạng mã khởi động C của tất cả các chương trình Umode. Sau đó chúng ta viết chương trình Umode bằng C như

/********** filename.c file ****************/

#include "ucode.c" // lệnh người dùng và giao diện syscall

main(int argc, char *argv[ ])

{ // Code C của chương trình Umode }

Sau đó, chúng tôi triển khai sh thô sơ để thực thi lệnh như sau.

/********************* tập tin sh.c *************************/

#include "ucode.c" // lệnh người dùng và giao diện syscall

main(int argc, char *argv[ ])

{
Machine Translated by Google

7.7 Quản lý quy trình 227

Hình 7.4 a Trình diễn fork và b trình diễn exec


Machine Translated by Google

228 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

int pid, trạng thái;

trong khi(1){

hiển thị các lệnh thực thi trong dấu nhắc thư mục /bin cho dòng lệnh

cmdline = "cmd a1 a2 …. an" if (!strcmp(cmd,"exit"))

thoát (0);

// phân nhánh một tiến trình con để thực thi dòng cmd pid = fork(); if

(pid) // cha mẹ sh

đợi con chết pid = wait(&status);

khác // cmdline thực thi con

thực thi (cmdline); // exec("cmd a1 a2 … an");

Sau đó biên dịch tất cả các chương trình Umode dưới dạng tệp thực thi nhị phân trong thư mục /bin và chạy sh khi hệ thống khởi

động. Điều này có thể được cải thiện hơn nữa bằng cách thay đổi hình ảnh Umode của P1 thành tệp init.c. Những điều này sẽ làm cho hệ

thống có khả năng tương tự như Unix/Linux về mặt quản lý quy trình và thực thi lệnh.

/********* init.c file : ảnh Umode ban đầu của P1 ********/

chủ yếu( )

int sh, pid, trạng thái;

sh = nĩa();

nếu (sh){ // P1 chạy trong vòng lặp while(1)

trong khi(1){

pid = chờ(&trạng thái); // đợi BẤT KỲ đứa trẻ nào chết nếu (pid==sh){

// nếu sh chết thì fork cái khác

sh = nĩa();

Tiếp tục;

printf("P1: Tôi vừa chôn một đứa trẻ mồ côi %d\n", pid);

khác

exec("sh"); // con của P1 chạy sh

7.7.10 vfork

Trong tất cả các hệ thống giống Unix, cách tiêu chuẩn để tạo các quy trình để chạy các chương trình khác nhau là bằng mô hình fork-

exec. Hạn chế chính của mô hình là nó phải sao chép hình ảnh tiến trình gốc, việc này tốn nhiều thời gian. Trong hầu hết các hệ thống

giống Unix, hành vi thông thường của các tiến trình cha và con như sau.

nếu (ngã ba()) // fork cha() một tiến trình con // tiến trình cha

chờ đợi(&trạng thái); đợi tiến trình con kết thúc

khác

exec(tên tệp); // con thực thi một file ảnh mới

Sau khi tạo con, cha mẹ đợi con kết thúc. Khi trẻ chạy, nó sẽ thay đổi hình ảnh Umode thành một tệp mới. Trong trường hợp này, việc

sao chép hình ảnh trong fork() sẽ lãng phí vì tiến trình con sẽ từ bỏ hình ảnh được sao chép ngay lập tức. Vì lý do này, hầu hết các

hệ thống giống Unix đều hỗ trợ thao tác vfork, tạo ra một tiến trình con mà không cần
Machine Translated by Google

7.7 Quản lý quy trình 229

sao chép hình ảnh cha mẹ. Thay vào đó, tiến trình con được tạo để chia sẻ cùng một hình ảnh với tiến trình cha. Khi đứa trẻ thực hiện thay

đổi hình ảnh, nó chỉ tự tách ra khỏi hình ảnh được chia sẻ mà không phá hủy nó. Nếu mọi tiến trình con hoạt động theo cách này thì lược đồ

sẽ hoạt động tốt. Nhưng điều gì sẽ xảy ra nếu người dùng không tuân theo quy tắc này và cho phép trẻ sửa đổi hình ảnh được chia sẻ? Nó sẽ

thay đổi hình ảnh được chia sẻ, gây ra sự cố cho cả hai quá trình. Để ngăn chặn điều này, hệ thống phải dựa vào tính năng bảo vệ bộ nhớ.

Trong các hệ thống có phần cứng bảo vệ bộ nhớ, hình ảnh được chia sẻ có thể được đánh dấu là chỉ đọc để các quá trình chia sẻ cùng một

hình ảnh chỉ có thể thực thi chứ không thể sửa đổi nó. Nếu một trong hai quá trình cố gắng sửa đổi hình ảnh được chia sẻ thì hình ảnh đó

phải được chia thành các hình ảnh riêng biệt. Trong các hệ thống dựa trên ARM, chúng ta cũng có thể triển khai vfork bằng thuật toán sau.

/********************* Thuật toán của vfork **********************/

1. tạo một tiến trình con sẵn sàng chạy trong Kmode, trả về 1 nếu thất bại;

2. sao chép một phần ustack của cha mẹ từ parent.usp hoàn toàn trở lại nơi có tên pid = vfork(), ví

dụ: 1024 mục dưới cùng; đặt usp con = usp cha - 1204;

3. Đặt pgdir con = pgdir cha, để chúng chia sẻ cùng một bảng trang; 4. đánh dấu trẻ

là vforked; trả về pid con;

Để đơn giản, trong thuật toán vfork, chúng tôi không đánh dấu các mục trong bảng trang chia sẻ CHỈ ĐỌC. Tương ứng với vfork, chức năng

exec phải được sửa đổi để tính đến các hình ảnh có thể được chia sẻ. Sau đây cho thấy thuật toán exec đã sửa đổi

/********************* Thuật toán thực thi đã sửa đổi **********************/

1. tìm nạp cmdline từ hình ảnh Umode (có thể được chia sẻ); 2.

nếu người gọi được vforked: chuyển sang bảng trang của người gọi và switchPgdir;

3. tải tập tin vào hình ảnh Umode;

4. sao chép cmdline vào ustack top và đặt usp; 5.

sửa đổi khung ngăn xếp tòa nhà để trở về VA = 0 trong Umode 6. tắt cờ

vforked; trả lại usp;

Trong thuật toán exec đã sửa đổi, tất cả các bước đều giống như trước, ngoại trừ bước 2, chuyển sang bảng trang của lệnh gọi, tách nó

khỏi hình ảnh gốc. Sau đây liệt kê các đoạn mã của hàm kvfork() và (đã sửa đổi) kexec() hỗ trợ vfork.

int kvfork()

int i, đỉnh, mủ;

PROC *p = get_proc(&freeList); if (p==0)

{ printf("vfork thất bại\n"); trả về -1; } p->ppid = đang chạy->pid; p->cha

mẹ = đang chạy;

p->trạng thái = SẴN SÀNG;

p->ưu tiên = 1;

p->vforked = 1; // thêm mục vforked vào PROC

p->pgdir = đang chạy->pgdir; // chia sẻ pgdir của cha mẹ

cho (i=1; tôi <= 14; i++){

p->kstack[SSIZE-i] = đang chạy->kstack[SSIZE-i];

cho (i=15; i<=28; i++){ // loại bỏ các thanh ghi Umode


Machine Translated by Google

230 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

p->kstack[SSIZE-i] = 0;

p->kstack[SSIZE - 14] = 0; // trả về con pid = 0 p->kstack[SSIZE-15] =

(int)goUmode; // tiếp tục quay lại goUmode

p->ksp = &(p->kstack[SSIZE-28]);

p->ucpsr = đang chạy->ucpsr; pusp =

(int)running->usp;

đỉnh = mủ - 1024; // ustack con: giảm 1024 byte

p->usp = (int *)cusp;

memcpy((char *)cusp, (char *)pusp, 128); // 128 mục đủ enqueue(&readyQueue, p); printf("Proc

%d đã phân nhánh một đứa trẻ

%d\n",running->pid, p->pid); trả về p->pid;

int kexec(char *cmdline) // cmdline=VA trong không gian Umode


{

// lấy cmdline và lấy tên file cmd: CÙNG như trước if (p->vforked){

p->pgdir = (int *)(0x600000 + p->pid*0x4000);

printf("%d bị VFORKED: switchPgdir sang %x",p->pid, p->pgdir);

switchPgdir(p->pgdir);

p->vforked = 0;

// nạp file cmd, thiết lập kstack; trở về VA=0: CÙNG như trước

7.7.11 Trình diễn vfork

Hệ thống mẫu C7.5 triển khai vfork. Để chứng minh vfork, chúng tôi thêm lệnh vfork vào các chương trình Umode. Lệnh vfork gọi hàm uvfork(),

hàm này đưa ra một lệnh gọi hệ thống để thực thi kvfork() trong kernel.

int uvfork()

int ppid, pid, trạng thái; ppid

= getpid(); pid =

syscall(11, 0,0,0); // vfork() syscall# = 11 if (pid){ printf("vfork parent

%d return

child pid=%d ", ppid, pid); printf("đợi con kết thúc\n"); pid = chờ(&trạng thái);

printf("vfork parent: dead child=%d, status=%x\n", pid,

status);

khác{

printf("vfork con: mypid=%d ", getpid());

printf("vfork child: exec(\"u2 test vfork\")\n");

syscall(10, "u2 test vfork",0,0);

Trong uvfork(), quy trình đưa ra một lệnh gọi hệ thống để tạo thành phần con bằng vfork(). Sau đó nó chờ cho vfork child kết thúc. Đứa trẻ

được vforked đưa ra một lệnh gọi tòa nhà thực thi để thay đổi hình ảnh sang một chương trình khác. Khi đứa trẻ ra ngoài, nó đánh thức cha mẹ,
Machine Translated by Google

7.7 Quản lý quy trình 231

Hình 7.5 Trình diễn vfork

sẽ không bao giờ biết rằng đứa trẻ đang thực thi trong hình ảnh Umode của nó trước đó. Hình 7.5 thể hiện kết quả đầu ra khi chạy hệ

thống mẫu C7.5.

7.8 Chủ đề

Trong mô hình quy trình, mỗi quy trình là một đơn vị thực thi độc lập trong không gian địa chỉ Umode duy nhất. Nói chung, không gian

địa chỉ Umode của các tiến trình đều khác biệt và riêng biệt. Cơ chế của vfork() cho phép một tiến trình tạo ra một tiến trình con tạm

thời chia sẻ cùng một không gian địa chỉ với tiến trình mẹ, nhưng cuối cùng chúng sẽ phân kỳ. Kỹ thuật tương tự có thể được sử dụng

để tạo các thực thể thực thi riêng biệt trong cùng một không gian địa chỉ của một quy trình. Các thực thể thực thi như vậy trong cùng

một không gian địa chỉ của một tiến trình được gọi là các tiến trình có trọng số nhẹ, thường được gọi là các luồng (Silberschatz et al.

2009). Người đọc có thể tham khảo (Posix 1C 1995; Buttlar et al. 1996; Pthreads 2015), để biết thêm thông tin về luồng và lập trình

luồng. Chủ đề có nhiều lợi thế hơn các quy trình. Để phân tích chi tiết về ưu điểm của luồng và ứng dụng của chúng, người đọc có thể

tham khảo Chap. 5 của Vương (2015). Trong phần này, chúng ta sẽ trình bày kỹ thuật mở rộng vfork() để tạo các luồng.

7.8.1 Tạo chủ đề

(1) Cấu trúc PROC của luồng:

Là một thực thể thực thi độc lập, mỗi luồng cần có cấu trúc PROC. Vì các luồng của cùng một tiến trình thực thi trong cùng một không

gian địa chỉ nên chúng chia sẻ nhiều điểm chung, chẳng hạn như pgdir, bộ mô tả tệp đã mở, v.v. Chỉ cần duy trì
Machine Translated by Google

232 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

chỉ một bản sao của thông tin được chia sẻ đó cho tất cả các luồng trong cùng một quy trình. Thay vì thay đổi mạnh mẽ cấu trúc PROC, chúng

ta sẽ thêm một số trường vào cấu trúc PROC và sử dụng nó cho cả tiến trình và luồng. Cấu trúc PROC được sửa đổi là

#define NPROC 10

#xác định NTHREAD 16

quy trình cấu trúc typedef{

// giống như trước, nhưng thêm

kiểu int; // PROCESS|loại THREAD // số lượng

int tcount; thread trong một tiến trình

int kstack[SSIZE]

}THỦ TỤC;

PROC proc[NPROC+NTHREAD],*freeList,*tfreeList,*readyQueue;

Trong quá trình khởi tạo hệ thống, chúng tôi đặt các PROC NPROC đầu tiên vào freeList như trước, nhưng đặt các PROC NTHREAD còn lại

vào một tfreeList riêng. Khi tạo một quy trình bằng fork() hoặc vfork(), chúng tôi phân bổ PROC miễn phí từ freeList và loại là PROCESS.

Khi tạo một luồng, chúng tôi phân bổ PROC từ tfreeList và loại là THREAD. Ưu điểm của thiết kế này là nó giữ những sửa đổi cần thiết ở mức

tối thiểu. Ví dụ: hệ thống quay trở lại mô hình quy trình thuần túy nếu NTHREAD = 0. Với các luồng, mỗi quy trình có thể được coi là một

vùng chứa các luồng. Khi tạo một tiến trình, nó được tạo làm luồng chính của tiến trình. Chỉ với luồng chính, hầu như không có sự khác

biệt giữa tiến trình và luồng. Tuy nhiên, luồng chính có thể tạo các luồng khác trong cùng tiến trình. Để đơn giản, chúng ta giả sử rằng

chỉ luồng chính mới có thể tạo các luồng khác và tổng số luồng trong một tiến trình được giới hạn ở mức TMAX.

(2) Tạo luồng: Lời gọi hệ thống

int thread(void *fn, int *ustack, int *ptr);

tạo một luồng trong không gian địa chỉ của một tiến trình để thực thi một hàm, fn(ptr), sử dụng vùng ustack làm ngăn xếp thực thi của nó.

Thuật toán của thread() tương tự như vfork(). Thay vì chia sẻ tạm thời ngăn xếp Umode với luồng gốc, mỗi luồng có một ngăn xếp Umode

chuyên dụng và hàm được thực thi với một tham số được chỉ định (có thể là một con trỏ tới cấu trúc dữ liệu phức tạp). Phần sau đây hiển

thị đoạn mã của kthread() trong kernel.

// tạo một thread để thực thi fn(ptr); trả về pid của chủ đề

int kthread(int fn, int *stack, int *ptr)

int i, uaddr, tcount; tcount =

đang chạy->tcount; if (running->type!

=PROCESS || tcount >= TMAX){ printf("không xử lý HOẶC số tcount tối đa %d đã

đạt\n", tcount);

trả về -1;

p = (PROC *)get_proc(&tfreeList); // lấy một thread PROC if (p == 0){ printf("\nno more

THREAD PROC "); trả về -1; }

p->trạng thái = SẴN SÀNG;

p->ppid = đang chạy->pid; p->cha mẹ =

đang chạy;

p->ưu tiên = 1;

p->sự kiện = 0;

p->exitCode = 0;

p->pgdir = đang chạy->pgdir; // pgdir giống như cha mẹ

p->loại = CHỦ ĐỀ;

p->tcount = 0; // tcount không được sử dụng bởi thread

p->ucpsr = đang chạy->ucpsr;


Machine Translated by Google

7.8 Chủ đề 233

p->usp = ngăn xếp + 1024; // đầu cao của ngăn xếp

for (i=1; i<29; i++) p- // loại bỏ các thanh ghi "đã lưu"

>kstack[SSIZE-i] = 0;

p->kstack[SSIZE-1] = fn; // uLR = fn

p->kstack[SSIZE-14] = (int)ptr; p->kstack[SSIZE-15] // đã lưu r0 cho fn(ptr)

= (int)goUmode; // tiếp tục quay lại goUmode

p->ksp = &p->kstack[SSIZE-28]; // aved ksp

enqueue(&readyQueue, p);

đang chạy->tcount++; return(p- // inc số lượng người gọi

>pid);

Khi một luồng bắt đầu chạy, trước tiên nó sẽ tiếp tục chuyển sang goUmode. Sau đó nó đi theo khung ngăn xếp syscall để trở về Umode để

thực thi fn(ptr) như thể nó được gọi bằng một lệnh gọi hàm. Khi điều khiển vào fn(), nó sử dụng

stmfd sp!; fg fp; lr

để lưu thanh ghi liên kết trả về trong ngăn xếp Umode. Khi hàm kết thúc, nó trả về bằng

ldmfs sp!; f fp; máy tính g

Để hàm fn() trả về một cách duyên dáng khi nó kết thúc, thanh ghi Umode lr ban đầu phải chứa một kết quả trả về thích hợp

Địa chỉ. Trong hàm Scheduler(), khi PROC chạy tiếp theo là một luồng, chúng ta tải thanh ghi Umode lr với giá trị

của VA(4). Tại địa chỉ ảo 4 của mỗi hình ảnh Umode là một tòa nhà thoát (0) (tính bằng ts.s), cho phép luồng kết thúc

thông thường. Mỗi luồng có thể chạy tĩnh, tức là trong một vòng lặp vô hạn và không bao giờ kết thúc. Nếu cần, nó sử dụng chế độ ngủ hoặc

semaphore để tự đình chỉ. Một luồng động chấm dứt khi nó hoàn thành nhiệm vụ được chỉ định. Như thường lệ, phụ huynh

Luồng (chính) có thể sử dụng lệnh gọi hệ thống chờ để loại bỏ các luồng con đã kết thúc. Đối với mỗi đứa trẻ bị chấm dứt, nó

giảm giá trị tcount của nó đi 1. Chúng tôi minh họa một hệ thống hỗ trợ các luồng bằng một ví dụ.

7.8.2 Trình diễn chủ đề

Hệ thống mẫu C7.6 triển khai hỗ trợ cho các luồng. Trong tệp ucode.c, chúng tôi thêm lệnh luồng và luồng liên quan

syscalls vào kernel. Lệnh thread gọi uthread(), trong đó nó phát ra thread syscall để tạo N ≤ 4 thread. Tất cả

các luồng thực thi cùng một hàm fn(ptr), nhưng mỗi luồng có ngăn xếp riêng và một tham số ptr khác nhau. Sau đó quá trình

chờ cho tất cả các chủ đề kết thúc. Mỗi luồng in pid, giá trị tham số và địa chỉ vật lý của tiến trình

hình ảnh. Khi tất cả các thread đã kết thúc, tiến trình chính vẫn tiếp tục.

int a[4] = {1,2,3,4}; // tham số cho thread

ngăn xếp int[4][1024]; int // ngăn xếp luồng

fn(int *ptr) // hàm luồng

int pid = getpid();

int pa = getPA(); // lấy PA của bộ nhớ tiến trình

printf("luồng %d trong %x ptr=%x *ptr=%d\n", pid, pa, ptr, *ptr);

int uthread()

int i, trạng thái, pid, cid, N = 2;

pid = getpid();

cho (i=0; i<N; i++){

printf("Proc %d tạo thread %d\n", pid, i);

syscall(12, fn, stack[i], &a[i]);

printf("proc %d đợi %d thread kết thúc\n", pid, N);


Machine Translated by Google

234 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

Hình 7.6 Trình diễn các luồng

cho (i=0; i<N; i++){

cid = chờ(&trạng thái);

printf("Proc %d: đứa trẻ chết=%d status=%x\n",pid, cid, status);

Hình 7.6 cho thấy kết quả đầu ra khi chạy chương trình C7.6. Như hình minh họa, tất cả các luồng thực thi trong cùng một không gian địa chỉ (PA =

0x800000) của tiến trình cha P1. Nó cũng chỉ ra rằng tiến trình cha P1 chờ tất cả các luồng con kết thúc bằng thao tác chờ kết thúc con thông thường.

7.8.3 Đồng bộ hóa luồng

Bất cứ khi nào nhiều thực thể thực thi chia sẻ cùng một không gian địa chỉ, chúng có thể truy cập và sửa đổi các đối tượng dữ liệu được chia sẻ (toàn cầu).

Nếu không đồng bộ hóa thích hợp, chúng có thể làm hỏng các đối tượng dữ liệu được chia sẻ, gây ra sự cố cho tất cả các thực thể thực thi. Mặc dù rất dễ

dàng để tạo nhiều tiến trình hoặc luồng, nhưng việc đồng bộ hóa quá trình thực thi của chúng nhằm đảm bảo tính toàn vẹn của các đối tượng dữ liệu được

chia sẻ không phải là một nhiệm vụ dễ dàng. Các công cụ tiêu chuẩn để đồng bộ hóa luồng trong Pthreads là các biến điều kiện và mutex (Pthreads 2015). Chúng

tôi sẽ đề cập đến việc đồng bộ hóa các luồng trong Chương. 9 khi chúng ta thảo luận về hệ thống đa bộ xử lý.

7.9 Hệ thống nhúng với phân trang hai cấp

Phần này trình bày cách định cấu hình ARM MMU cho bộ nhớ ảo phân trang hai cấp để hỗ trợ các quy trình ở Chế độ người dùng.

Đầu tiên, chúng tôi hiển thị bản đồ bộ nhớ hệ thống, trong đó chỉ định mục đích sử dụng không gian bộ nhớ hệ thống theo kế hoạch.
Machine Translated by Google

7.9 Hệ thống nhúng với phân trang hai cấp 235

--Phạm vi-- ---------- Cách sử dụng -------------

0 -64K Các vectơ, pgidr/pgtable ban đầu

64K-2MB Nhân hệ thống

2MB-4MB Bộ đệm khung hiển thị LCD

4MB-5MB Hệ thống tập tin đĩa RAM

5MB-6MB Bảng trang cấp 2 hạt nhân

6MB-7MB Quy trình pgdir cấp 1

7MB-8MB Xử lý pgtable cấp 2 của Umode

RAM trống 8MB-256MB cho hình ảnh Proc Umode

Không gian I/O được ánh xạ bộ nhớ 256MB-257MB


------------------------------------------

Khi hệ thống khởi động, trước tiên chúng tôi thiết lập phân trang một cấp sử dụng các phần 1 MB như trước. Trong môi trường
phân trang đơn giản này, chúng tôi khởi tạo cấu trúc dữ liệu kernel, tạo và chạy quy trình ban đầu P0 trong Kmode. Trong
kernel_init(), chúng tôi thiết lập bảng trang cấp 1 mới (pgdir), được ký hiệu là ktable, ở mức 32 KB và các bảng trang cấp 2 liên
quan (pgtables) ở mức 5 MB để tạo ánh xạ nhận dạng VA đến PA. Sau đó, chúng tôi tạo 64 pgdir ở mức 6 MB cho các quy trình khác,
mỗi quy trình có một pgdir ở mức 6 MB + (pid 1) * 16 KB. Vì không gian địa chỉ Kmode của tất cả các tiến trình đều giống hệt
nhau nên pgdir của chúng chỉ được sao chép từ ktable. Sau đó, chúng tôi thay đổi pgdir thành ktable ở mức 32 KB, chuyển MMU sang
phân trang 2 cấp. Phần sau đây hiển thị mã hàm makePageTable() trong C.

int *mtable = (int *)0x4000; // pgdir ban đầu ở mức 16KB

int *ktable = (int *)0x8000; // pgdir 2 cấp mới có dung lượng 32KB

int makePageTable()

int tôi;

for (i=0; i<4096; i++){ // loại bỏ 4096 mục của ktable[]

ktable[i] = 0;

for (i=0; i<258; i++){ // ĐÁNH GIÁ 256MB PA + 2MB dung lượng I/O

ktable[i] = (0x500000 + i*1024) | 0x11; // TÊN MIỀN 0,type=01

// xây dựng pgtables cấp 2 Kmode ở mức 5 MB

cho (i=0; i<258; i++){

pgtable = (int *)(0x500000 + i*1024); paddr = i*0x100000 |

0x55E; // APs=01|01|01|01|CB=11|type=10

for (j=0; j<256; j++){ // 256 mục, mỗi mục trỏ tới 4KB PA

pgtable[j] = paddr + j*4096;

// xây dựng 64 pgdir cấp 1 cho các PROC khác với dung lượng 6 MB

ktable = (int *)0x600000; // xây dựng pgdir của 64 proc ở mức 6 triệu

cho (i=0; i<64; i++){ // vùng 512KB trong 6MB

ktable = (int *)(0x600000 + i*0x4000); // mỗi pgdir 16KB

for (j=0; j<4096; j++){ // sao chép ktable[ ]

ktable[j] = mtable[j];

đang chạy->pgdir = ktable; // thay đổi pgdir của P0 thành ktable switchPgdir((int)ktable); //

chuyển sang pgdir 2 cấp

Trong bảng trang cấp 1 (pgdir) của mỗi tiến trình, 2048 mục cao nhất ban đầu là 0. Các mục này xác định không gian Umode VA của
mỗi quy trình, không gian này sẽ được điền vào khi quy trình được tạo trong kfork() hoặc fork(). Phần này phụ thuộc vào sơ đồ cấp
phát bộ nhớ cho các ảnh Umode xử lý, có thể là tĩnh hoặc động.
Machine Translated by Google

236 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

7.9.1 Phân trang 2 cấp tĩnh

Trong phân trang tĩnh, mỗi tiến trình được phân bổ một vùng bộ nhớ có kích thước cố định PSIZE cho hình ảnh Umode của nó. Để đơn
giản, chúng ta có thể giả sử rằng PSIZE là bội số của 1 MB. Chúng tôi phân bổ mỗi hình ảnh Umode của quy trình ở mức 8 MB + (pid
1) * PSIZE. Ví dụ: nếu PSZIE = 4 MB thì P1 là 8 MB, P2 là 12 MB, v.v. Sau đó, chúng tôi thiết lập các bảng trang quy trình để truy
cập hình ảnh Umode dưới dạng trang. Phần sau đây hiển thị các đoạn mã trong kfork(), fork() và kexec().

#xác định PSIZE 0x400000

PROC *fork1() // mã chung để tạo một Proc mới *p

int tôi, j;

int *pgtable, npgdir;

PROC *p = get_proc(&freeList);

nếu (p==0){

kprintf("fork1() không thành công\n");

trả về (PROC *)0;

p->ppid = đang chạy->pid; p->cha mẹ

= đang chạy;

p->trạng thái = SẴN SÀNG;

p->ưu tiên = 1; p->tcount

= 1;

p->paddr = (int *)(0x800000 + (p->pid-1)*PSIZE);

p->psize = PSIZE;

p->pgdir = (int *)(0x600000 + (p->pid-1)*0x4000);

// điền vào các mục pgdir cao và xây dựng pgtables Umode

npgdir = p->size/0x100000; // KHÔNG. của các mục nhập pgdir và pgtables của Umode

cho (i=0; i<npgdir; i++){

pgtable = (int *)(0x700000 + (pid-1)*1024 + i*1024);

p->pgdir[2048+i] = (int)pgtable | 0x31; // TÊN MIỀN=1,type=01

cho (j=0; j<256; j++){

pgtable[j] = (int)(p->paddr + j*1024) | 0xFFE; // AP=11

trả lại p;

PROC *kfork(char *tên tệp)

int tôi; char *cp;

PROC *p = fork1();

if (p==0){ printf("kfork thất bại\n"); trả về 0; }

for (i=1; i<29; i++){ // tất cả 28 ô trong kstack[] = 0

p->kstack[SSIZE-i] = 0;

p->kstack[SSIZE-15] = (int)goUmode; // vào tháng 12 reg=address ORDER !!! cp = (char *)p->paddr + PSIZE -

128; strcpy(cp, istring);

p->usp = (int *)VA(0x200000 – 128); // usp tại VA(2MB-128)

p->ucpsr = (int *)0x10;

p->upc = (int *)VA(0);

p->kstack[SSIZE-14] = (int)p->usp; // đã lưu r0

p->kstack[SSIZE-1] = VA(0);

enqueue(&readyQueue, p);

trả lại p;
Machine Translated by Google

7.9 Hệ thống nhúng với phân trang hai cấp 237

int fork() // fork một proc con có hình ảnh Umode giống hệt nhau

int i, PA, CA; int

*pgtable;

PROC *p = fork1();

if (p==0){ printf("fork thất bại\n"); trả về -1; }

PA = (int)running->paddr;

CA = (int)p->paddr;

memcpy((char *)CA, (char *)PA, Running->psize); // sao chép ảnh p->usp = Running->usp; // cả hai

phải là VA trong phần của chúng p->ucpsr = Running->ucpsr;

cho (i=1; tôi <= 14; i++){

p->kstack[SSIZE-i] = đang chạy->kstack[SSIZE-i];

cho (i=15; i<=28; i++){

p->kstack[SSIZE-i] = 0;

p->kstack[SSIZE-14] = 0; // trả về con pid=0

p->kstack[SSIZE-15] = (int)goUmode;

p->ksp = &(p->kstack[SSIZE-28]);

enqueue(&readyQueue, p);

trả về p->pid;

int kexec(char *cmdline) // cmdline=VA trong Uspace

int i, j, upa, usp; char

*cp, kline[128], file[32], tên file[32];

PROC *p = đang chạy; int

*pgtable; // lấy

cmdline, lấy tên file cmd: SAME AS TRƯỚC if (p->vforked){ // tạo pgtables

Umode của riêng nó

p->paddr = (int *)(0x800000 + (p->pid-1)*PSIZE); p->psize = PSIZE; p->pgdir

= (int *)(0x600000 + (p-

>pid-1)*0x4000);

npgdir = p->psize/0x100000;

cho (i=0; i<npgdir; i++){

pgtable = (int *)(0x700000 + (p->pid-1)*1024 + i*1024);

p->pgdir[2048+i] = (int)((int)pgtable | 0x31);

cho (j=0; j<256; j++){

pgtable[j] = (int)(p->paddr + j*1024) | 0x55E;

p->vforked = 0; // tắt cờ vfored

switchPgdir(p->pgdir); // chuyển sang pgdir của chính nó

Loadelf(file, p); // sao

chép kline vào Ustack và trở về VA=0: CÙNG NHƯ TRƯỚC

7.9.2 Trình diễn phân trang 2 cấp tĩnh

Hình 7.7 cho thấy các đầu ra của hệ thống mẫu C7.7, sử dụng phân trang tĩnh 2 cấp độ.
Machine Translated by Google

238 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

Hình 7.7 Trình diễn phân trang 2 cấp tĩnh

7.9.3 Phân trang 2 cấp động

Trong phân trang động, vùng bộ nhớ Umode của mỗi tiến trình bao gồm các khung trang được phân bổ động. Để hỗ trợ phân trang động, hệ

thống phải quản lý bộ nhớ khả dụng dưới dạng khung trang trống. Điều này có thể được thực hiện bằng bitmap hoặc danh sách liên kết.

Nếu số lượng khung trang lớn thì việc quản lý chúng bằng danh sách liên kết sẽ hiệu quả hơn. Khi hệ thống khởi động, chúng tôi xây

dựng một danh sách liên kết trang miễn phí, pfreeList, xâu chuỗi tất cả các khung trang miễn phí vào một danh sách liên kết. Khi cần

khung trang, chúng tôi phân bổ khung trang từ pfreeList. Khi khung trang không còn cần thiết nữa, chúng tôi sẽ phát hành lại khung

trang đó cho pfreeList để sử dụng lại. Các đoạn mã sau đây hiển thị các chức năng quản lý danh sách trang miễn phí.

int *pfreeList; int // danh sách khung trang trống //

*palloc() cấp phát khung trang trống

int *p = pfreeList; if (p)

{ pfreeList

= (int *)(*p); *p = 0;

trả lại p;

void pdealloc(int p) // giải phóng một khung trang

u32 *a = (u32 *)((int)p & 0xFFFFFF000); // PA của khung

*a = (int)pfreeList;

pfreeList = a;

}
Machine Translated by Google

7.9 Hệ thống nhúng với phân trang hai cấp 239

int *free_page_list(int *startva, int *endva) // xây dựng pfreeList

int *p = startva; while(p

< (int *)(endva-1024)){ *p = (int)(p + 1024);

p += 1024;

*p = 0;

trở lại bắt đầu;

pfreeList = free_page_list((int *)8MB, (int *)256MB);

7.9.3.1 Sửa đổi hạt nhân cho phân trang động Khi sử dụng
phân trang động, hình ảnh Umode của mỗi quy trình không còn là một phần bộ nhớ liền kề. Thay vào đó, nó bao gồm các
khung trang được phân bổ động, có thể không liền kề nhau. Chúng ta phải sửa đổi mã hạt nhân quản lý hình ảnh tiến
trình để phù hợp với những thay đổi này. Phần sau đây cho thấy các đoạn mã hạt nhân đã được sửa đổi để phù hợp với
sơ đồ phân trang động.

(1). fork1(): fork1() là mã cơ sở của cả kfork() và fork(). Nó tạo ra một tiến trình p mới và thiết lập hình ảnh Umode của nó.

Vì bảng trang cấp 1 (pgdir) phải là một phần duy nhất của bộ nhớ được căn chỉnh 16 KB, chúng ta không thể xây dựng nó bằng cách

phân bổ các khung trang vì các khung trang có thể không liền kề hoặc được căn chỉnh ở ranh giới 16 KB. Vì vậy, chúng tôi vẫn xây

dựng các pgdir proc ở mức 6 MB như trước. Chúng ta chỉ cần sửa đổi fork1() để xây dựng hình ảnh Umode bằng cách phân bổ các khung trang.

// Giả sử: Kích thước ảnh Umode tính bằng MB

#xác định PSIZE 0x400000

PROC *fork1()

int i, j, npgdir, npgtable; int

*pgtable; // tạo

một Proc mới *p: CÙNG NHƯ TRƯỚC p->pgdir = (int *)(0x600000

+ (p->pid-1)*0x4000); // giống như trước // sao chép các mục từ ktable ở 32KB

cho (i=0; i<4096; i++){

p->pgdir[i] = ktable[i];

p->psize = 0x400000; // Kích thước Uimage tính bằng MB

npgdir = p->psize/0x100000; // KHÔNG. của các mục pgdir và pgtables

npgtable = npgdir;

// điền vào các mục pgdir và xây dựng pgtables Umode

cho (i=0; i<npgdir; i++){

pgtable = (int *)palloc(); // cấp phát một trang nhưng chỉ sử dụng 1KB

p->pgdir[2048+i] = (int)pgtable | 0x31; // mục nhập pgdir

cho (j=0; j<256; j++){ // bảng pg

pgtable[j] = (int)((int)palloc() | 0xFFE);

trả lại p;

(2). kfork(): kfork() tạo một quy trình mới với các tham số dòng lệnh ban đầu được truyền trong ngăn xếp Umode ở đầu cao của không gian

địa chỉ ảo. Vì pgdir của người gọi khác với pgdir của quy trình mới nên chúng tôi không thể sử dụng VA(PSIZE) để truy cập vào ngăn

xếp Umode (cấp cao) của quy trình mới. Thay vào đó, chúng ta phải sử dụng khung trang được phân bổ cuối cùng để truy cập ngăn xếp
Umode của nó.
Machine Translated by Google

240 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

(3). fork(): Khi sao chép hình ảnh, chúng ta phải sao chép các khung trang cha sang khung của tiến trình con, như được minh họa bởi đoạn mã

sau.

// sao chép khung trang của cha vào khung trang của con

bản sao void(PROC *cha, PROC *con)

int tôi, j;

int *ppgtable, *cpgtable, *cpa, *ppa;

int npgdir = parent->psize/0x100000;

for (i=0; i < npgdir; i++){ // mỗi hình ảnh có bảng trang npgdir

ppgtable = (int *)(parent->pgdir[i+2048] & 0xFFFFFC00);

cpgtable = (int *) (child->pgdir[i+2048] & 0xFFFFFC00);

for (j=0; j<256; j++){ // sao chép khung bảng trang

ppa = (int *)(ppgtable[j] & 0xFFFFFF000); cpa = (int *)

(cpgtable[j] & 0xFFFFFF000);

memcpy((char *)cpa, (char *)ppa, 4096); // sao chép khung trang 4KB

(4). kexec(): Chúng tôi giả sử rằng kexec() sử dụng cùng vùng hình ảnh Umode của một quy trình. Trong trường hợp này, không cần thay đổi gì,

ngoại trừ quy trình vforked, quy trình này phải tạo các mục pgdir riêng, phân bổ các khung trang và chuyển sang pgdir của chính nó. (5).

trình tải: trình tải hình ảnh phải được sửa đổi để tải tệp hình ảnh vào các khung trang của quy trình. Đã sửa đổi

mã tải (giả) là

// định vị tiêu đề phần ELF như trước; tiêu đề ph->pgogram cho (int i=1, ph=aph; i <=

phnum; ph++, i++){

if (ph->type != 1) break;

lseek(fd, (dài)ph->offset, 0); // đặt offset thành ph->offset

pn = PA(ph->vaddr)/0x1000; // chuyển đổi vaddr thành số trang

đếm = 0;

addr = (char *)(pgtable[pn] & 0xFFFF000); // đang tải địa chỉ while(count < ph->memsize){

đọc(fd, dbuf, BLKSIZE); // đọc bởi 1KB BLKSIZE

memcpy(addr, dbuf, BLKSIZE); // sao chép vào địa chỉ

addr += BLKSIZE;

đếm += BLKSIZE;

(6). kexit(): Khi một quá trình kết thúc, chúng tôi giải phóng các khung trang của nó được sử dụng làm mục nhập pgdir Umode và pgtables trở lại

pfreeList để tái sử dụng.

7.9.4 Trình diễn phân trang động 2 cấp

Hình 7.8 cho thấy kết quả của việc chạy chương trình C7.8, sử dụng phân trang 2 cấp độ động. Như hình minh họa, các mục pgdir và pgtables của

tiến trình P1 đều được phân bổ động.


Machine Translated by Google

7.10 Bản đồ bộ nhớ KMH 241

7.10 Bản đồ bộ nhớ KMH

Các hệ thống mẫu từ C7.1 đến C7.8 sử dụng sơ đồ ánh xạ bộ nhớ Kernel Mapped Low (KML), trong đó không gian chế độ kernel được ánh xạ tới các địa

chỉ ảo thấp và không gian chế độ người dùng được ánh xạ tới các địa chỉ ảo cao. Sơ đồ ánh xạ có thể được đảo ngược. Trong sơ đồ Kernel Mapped

High (KMH), không gian VA của chế độ kernel được ánh xạ ở mức cao, ví dụ: tới 2 GB (0x80000000) và không gian VA ở chế độ người dùng được ánh xạ ở

mức thấp. Trong phần này, chúng tôi sẽ trình bày sơ đồ ánh xạ bộ nhớ KMH, so sánh nó với sơ đồ KML và thảo luận về sự khác biệt giữa hai sơ đồ ánh

xạ.

7.10.1 KMH Sử dụng phân trang tĩnh một cấp

(1). VA cao trong ảnh hạt nhân: Giả sử rằng không gian VA hạt nhân được ánh xạ tới 2 GB. Để cho phép kernel sử dụng địa chỉ ảo cao, lệnh liên kết

phải được sửa đổi để tạo địa chỉ ảo cao. Đối với hình ảnh hạt nhân, lệnh liên kết được sửa đổi là

arm-none-eabi-ld -T t.ld ts.o tới mtx.o -Ttext=0x80010000 -o t.elf

Hình ảnh hạt nhân được tải tại địa chỉ vật lý 0x10000 nhưng VA của nó là 0x800100000. Đối với hình ảnh chế độ người dùng, lệnh liên kết được

thay đổi thành

arm-none-eabi-ld -T u.ld us.o u1.o -Ttext=0x100000 -o u1.elf

Lưu ý rằng địa chỉ ảo bắt đầu của hình ảnh chế độ người dùng không phải là 0 mà là 0x100000 (1 MB). Chúng tôi sẽ giải thích và biện minh cho
điều này sau.

Hình 7.8 Trình diễn phân trang 2 cấp động


Machine Translated by Google

242 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

(2). Chuyển đổi VA sang PA: Mã hạt nhân sử dụng VA nhưng các mục trong bảng trang và địa chỉ cơ sở thiết bị I/O phải sử dụng PA. Vì
thuận tiện, chúng tôi xác định các macro

#define VA(x) ((x) + 0x80000000)

#define PA(x) ((x) - 0x80000000)

chuyển đổi giữa VA và PA.

(3). VA cho Địa chỉ cơ sở I/O: Địa chỉ cơ sở của thiết bị I/O sử dụng PA, phải được ánh xạ lại thành VA. Cho

Bảng mạch đa năng, các thiết bị I/O được đặt trong không gian I/O 2 MB bắt đầu từ PA 256 MB. Cơ sở thiết bị I/O

địa chỉ phải được ánh xạ tới VA bởi macro VA(x).

(4). Bảng trang ban đầu: Vì mã hạt nhân được liên kết biên dịch với VA nên chúng ta không thể thực thi mã C của hạt nhân trước đó

cấu hình MMU để dịch địa chỉ. Bảng trang đầu tiên phải được xây dựng bằng mã hợp ngữ khi

hệ thống bắt đầu.

(5). Tệp ts.s: reset_handler thiết lập bảng trang ban đầu và cho phép MMU dịch địa chỉ. Sau đó nó đặt

lên ngăn xếp chế độ đặc quyền, sao chép bảng vectơ về 0 và gọi hàm main() trong C. Để cho ngắn gọn, chúng ta sẽ chỉ

hiển thị các đoạn mã có liên quan đến ánh xạ bộ nhớ. Mã hợp ngữ hiển thị bên dưới thiết lập một giá trị ban đầu

bảng trang một cấp ở PA = 0x4000 (16 KB) sử dụng các phần 1 MB. Trong bảng trang một cấp, mục nhập thứ 0 trỏ tới

1 MB bộ nhớ vật lý thấp nhất, tạo ra ánh xạ nhận dạng của không gian bộ nhớ 1 MB thấp nhất. Đây là

bởi vì bảng vectơ và địa chỉ mục nhập của trình xử lý ngoại lệ đều nằm trong bộ nhớ vật lý 4 KB thấp nhất.

Trong bảng trang, các mục từ 2048 đến 2048 + 258 ánh xạ VA = (2 GB, 2 GB + 258 MB) tới bộ nhớ vật lý 258 thấp,

bao gồm không gian I/O 2 MB ở mức 256 MB. Không gian chế độ kernel được gán miền 0, với các bit truy cập

được đặt thành AP = 01 (chế độ máy khách) để ngăn quá trình Chế độ người dùng truy cập vào không gian VA của kernel. Sau đó nó đặt TLB

thanh ghi cơ sở và cho phép MMU dịch địa chỉ. Sau đó, hệ thống sẽ chạy với VA trong phạm vi

(2 GB đến 2 GB + 258 MB).

/*************** tập tin ts.s ****************/

reset_handler:

adr r4, MTABLE // r4 trỏ tới MTABLE tại 0x4000=16KB

ldr r5, [r4] // r5 = 0x4000

chuyển động r0, r5 // r0 = Nội dung MTABLE=0x4000 ở 16KB

// xóa mục nhập MTABLE 4096 về 0

mov r1, #4096 // 4096 mục

chuyển r3, #0 // r3=0

1:

str r3, [r0], #4 // lưu r3 vào [r0]; tăng r0 lên 4

phụ r1, r1, #1 // r1--

bgt 1b // vòng lặp r1=4096 lần

// Giả sử: Versatilepb có 256 MB RAM + 2 MB I/O ở mức 256 MB,

// mục 0 của MTABLE phải trỏ tới PA 1 MB thấp nhất do bảng vectơ

mov r6, #(0x1 << 10) // r6=AP=01 (KRW, số người dùng) AP=11: cả KU r/w

hoặc r6, r6, #0x12 // r6 = 0x412 HOẶC 0xC12 nếu AP=11: đã sử dụng 0xC12

di chuyển r3, r6 // r3 chứa 0x412

chuyển động r0, r5 // r0 = 0x4000

str r3, [r0] // điền MTABLE[0] bằng 0x00000412

//******** ánh xạ kernel 258MB VA tới 2GB *************

chuyển động r0, r5 // r0 = 0x4000 nữa

thêm r0, r0,#(2048*4) // thêm 8K cho mục nhập thứ 2048

mov r2, #0x100000 // r2 = gia số 1MB

mov r1, #256 // 256 mục

thêm r1, r1, #2 // thêm 2 mục nữa cho không gian I/O 2MB

di chuyển r3, r6 // r3 = 0x00000412

3:
Machine Translated by Google

7.10 Bản đồ bộ nhớ KMH 243

str r3, [r0], #4 // lưu r3 vào [r0]; tăng r0 lên 4

thêm r3, r3, r2 // tăng r3 thêm 1 triệu

phụ r1,r1, #1 // r1--

bgt 3b // lặp r1=258 lần

// thiết lập thanh ghi cơ sở TLB

chuyển động r0, r5 // r0 = 0x4000

mcr p15, 0, r0, c2, c0, 0 // đặt TTBase với địa chỉ VẬT LÝ 0x4000

mcr p15, 0, r0, c8, c7, 0 // tuôn ra TLB

// đặt miền0: 01=client(kiểm tra quyền) 11=master(không kiểm tra)

chuyển r0, #0x1 // 01 cho chế độ máy khách

mcr p15, 0, r0, c3, c0, 0 // ghi vào miền REG c3

// kích hoạt MMU

mrc p15, 0, r0, c1, c0, 0 // đọc điều khiển REG tới r0

hoặc r0, r0, #0x00000001 // đặt bit0 của r0 thành 1

mcr p15, 0, r0, c1, c0, 0 // ghi vào điều khiển REG c1 ==> MMU bật

không

không

không

mrc p15, 0, r2, c2, c0, 0 // đọc TLB base reg c2 vào r2

chuyển động r2, r2

// thiết lập con trỏ ngăn xếp chế độ đặc quyền, sau đó gọi main() trong C

MTABLE: .word 0x4000 // bảng trang đầu tiên có kích thước 16KB

switchPgdir: // chuyển pgdir sang pgdir của PROC mới; đậu vào r0

mcr p15, 0, r0, c2, c0, 0 // đặt TTBase thành C2

chuyển r1, #0

mcr p15, 0, r1, c8, c7, 0 // tuôn ra TLB

mcr p15, 0, r1, c7, c7, 0 // xóa bộ đệm I và D

mrc p15, 0, r2, c2, c0, 0 // đọc TLB cơ sở reg C2

// đặt tên miền: all 01=client(kiểm tra quyền) 11=master(không kiểm tra)

chuyển r0, #0x5 // 0101=client cho cả miền 0 và 1

mcr p15, 0, r0, c3, c0, 0 // ghi vào miền reg C3

di chuyển máy tính, lr // trở lại

(5). Tệp Kernel.c: Chúng tôi chỉ hiển thị phần ánh xạ bộ nhớ của kernel_init(). Nó tạo ra 64 thư mục trang (trang cấp 1

bảng) ở mức 6 MB cho 64 PROC. pgdir của mỗi proc là 6 MB + pid * 16 KB. Vì chế độ kernel không gian VA của tất cả

các tiến trình đều giống nhau, các mục pgdir trong chế độ kernel của chúng đều giống hệt nhau. Để đơn giản, chúng ta giả sử rằng mọi

Quá trình, ngoại trừ P0, có hình ảnh Umode có kích thước 1 MB, được phân bổ tĩnh tại địa chỉ vật lý (pid+7) MB,

tức là P1 ở mức 8 MB, P2 ở mức 9 MB, v.v. Trong mỗi quy trình pgdir, mục 1 xác định hình ảnh Umode của quy trình. Tương ứng với

điều này, mọi hình ảnh Umode được liên kết biên dịch với VA=0x100000 (1 MB) bắt đầu. Trong quá trình chuyển đổi tác vụ, nếu dòng điện

khác với quy trình tiếp theo, nó gọi switchPdgir() để chuyển pgdir sang quy trình tiếp theo.

/****************** hàm kernel_init() *******************/

kernel_init()

int i, j, *mtable, *MTABLE;

// khởi tạo cấu trúc dữ liệu kernel, tạo P0: CÙNG như trước

printf("xây dựng pgdir ở mức 5MB\n");

// tạo pgdir cho TẤT CẢ PROC ở mức 5 MB;

MTABLE = (int *)0x80004000; // Mtable ban đầu tại PA=0x4000

mtable = (int *)0x80600000; // mtable bắt đầu ở mức 6MB

cho (j=0; j<4096; j++) // xóa các mục mtable về 0

mtable[j] = 0;
Machine Translated by Google

244 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

// pgdir PHẢI ở ranh giới 16K; 6M-7MB có dung lượng cho 64 pgdir

cho (i=0; i<64; i++){ // cho 64 bảng PROC

mtable[0] = MTABLE[0]; // mục 0 cho 1 MB thấp cho vectơ

for (j=2048; j<4096; j++){ // sao chép 2048 mục cuối cùng từ MTABLE

mtable[j] = MTABLE[j];

// ĐÁNH GIÁ: mỗi ảnh Umode có kích thước = 1MB => chỉ cần một mục nếu (i){ // loại trừ P0 không có

ảnh Umode

mtable[1]=(0x800000 + (i-1)*0x100000)|0xC12; // Umode=mục số 1

mtable += 4096; // nâng mtable lên 16KB tiếp theo

printf("chuyển sang pgdir của P0 : ");

switchPgdir((int)0x600000); // tham số phải là PA

bộ lập lịch int()

PROC *cũ=đang chạy;

printf("Proc %d trong bộ lập lịch: ", Running->pid); if (đang chạy->trạng

thái==SẴN SÀNG)

enqueue(&readyQueue, đang chạy);

đang chạy = dequeue(&readyQueue);

printf("lần chạy tiếp theo = %d\n", đang chạy->pid);

if (đang chạy != cũ)

switchPgdir(PA(int)running->pgdir & 0xFFFFFF000);

(6). Sử dụng VA trong Hàm hạt nhân: Tất cả các hàm hạt nhân, chẳng hạn như kfork, fork, trình tải tệp hình ảnh và kexec, đều phải sử dụng VA.

(7). Trình diễn Ánh xạ bộ nhớ KMH: Hình 7.9 cho thấy kết quả chạy chương trình C7.9, trong đó

chứng minh ánh xạ địa chỉ KMH bằng cách sử dụng các phần 1 MB.

Người đọc có thể chạy hệ thống C7.9 và phân nhánh các quy trình khác. Nó sẽ chỉ ra rằng mỗi quy trình chạy trong một vùng PA khác nhau

nhưng tất cả đều có cùng VA = 0x100000. Các biến thể của sơ đồ phân trang một cấp được để lại dưới dạng các dự án lập trình trong phần Sự
cố.

7.10.2 KMH Sử dụng phân trang tĩnh hai cấp

Trong phần này, chúng tôi sẽ trình bày sơ đồ ánh xạ bộ nhớ KMH bằng cách sử dụng phân trang hai cấp. Điều này được thực hiện trong ba bước.

Bước 1: Khi hệ thống khởi động, trước tiên chúng ta thiết lập bảng trang một cấp ban đầu và kích hoạt MMU giống hệt như chương trình C7.9.

Trong môi trường phân trang đơn giản này, chúng ta có thể thực thi mã hạt nhân bằng các địa chỉ ảo cao.

Bước 2: Trong kernel_init(), chúng tôi xây dựng một trang pgdir hai cấp với kích thước 32 KB (0x8000). Mục nhập thứ 0 của pgdir cấp 1 dành

cho bản đồ ID bộ nhớ 1 MB thấp nhất. Chúng tôi xây dựng bảng trang cấp 2 ở mức 48 KB (0xC000) để tạo ánh xạ ID của bộ nhớ 1 MB thấp nhất. Giả

sử RAM 256 MB cộng với 2 MB dung lượng I/O ở mức 256 MB. Chúng tôi xây dựng 258 bảng trang cấp 2 với dung lượng 5 MB. Sau đó, chúng tôi xây

dựng 64 pgdir cấp 1 với dung lượng 6 MB. Mỗi Proc có một pgdir ở mức 5 MB + pid * 16 KB. Bảng trang cấp 2 của mỗi pgdir đều giống nhau. Sau

đó, chúng tôi chuyển pgdir sang 6 MB để sử dụng phân trang hai cấp.
Machine Translated by Google

7.10 Bản đồ bộ nhớ KMH 245

Hình 7.9 Trình diễn KMH sử dụng phân trang tĩnh một cấp

Bước 3: Giả sử mỗi ảnh Umode của tiến trình có kích thước = USIZE MB, được phân bổ tĩnh ở mức 7 MB + pid * 1 MB. Khi tạo một quy
trình mới trong fork1(), chúng tôi xây dựng các mục nhập pgdir cấp 1 Umode và bảng trang cấp 2 ở mức 7 MB + pid*1 KB

Hình 7.10 cho thấy kết quả đầu ra khi chạy chương trình mẫu C7.10, thể hiện sơ đồ ánh xạ KMH
sử dụng phân trang tĩnh hai cấp.

7.10.3 KMH Sử dụng phân trang động hai cấp

Khá dễ dàng để mở rộng phân trang tĩnh KMH hai cấp sang phân trang động. Phần này được để lại như một bài tập trong phần Vấn đề.

7.11 Hệ thống tập tin hỗ trợ hệ thống nhúng

Cho đến nay, chúng tôi đã sử dụng đĩa RAM làm hệ thống tệp. Mỗi hình ảnh quá trình được tải dưới dạng tệp thực thi từ đĩa RAM.
Trong quá trình thực thi, các tiến trình có thể lưu thông tin bằng cách ghi vào đĩa RAM. Vì đĩa RAM được giữ trong bộ nhớ dễ thay
đổi nên tất cả nội dung trên đĩa RAM sẽ biến mất khi tắt nguồn hệ thống. Trong phần này, chúng ta sẽ sử dụng thẻ SD (SDC) làm thiết
bị lưu trữ liên tục để hỗ trợ các hệ thống tệp. Giống như một đĩa cứng, SDC có thể được chia thành các phân vùng. Phần sau đây
cho thấy cách tạo ảnh đĩa phẳng chỉ với một phân vùng. Hình ảnh đĩa thu được có thể được sử dụng làm SDC ảo trong hầu hết các máy
ảo hỗ trợ thẻ SD.
Machine Translated by Google

246 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

7.11.1 Tạo hình ảnh thẻ SD

(1). Tạo một tệp hình ảnh đĩa phẳng gồm 4096 khối 1 KB.

dd if=/dev/zero of=disk.img bs=1024 count=4096

(2). Chia disk.img thành các phân vùng. Cách đơn giản nhất là tạo một phân vùng duy nhất.

fdisk –H 16 –S 512 disk.img # enter n, sau đó nhấn phím enter

(3). Tạo một thiết bị lặp cho disk.img.

lostup –o 1048576 --sizelimit 4193792 /dev/loop1 disk.img

(4). Định dạng phân vùng dưới dạng hệ thống tệp EXT2.

mke2fs –b 1024 đĩa.img 3072

(5). Gắn thiết bị vòng lặp.

gắn kết/dev/loop1/mnt

(6). Điền nó với các tập tin, sau đó umount nó.

Hình 7.10 Trình diễn KMH sử dụng phân trang tĩnh 2 cấp
Machine Translated by Google

7.11 Hệ thống tập tin hỗ trợ hệ thống nhúng 247

mkdir /mnt/khởi động; số lượng /mnt

Trên ảnh đĩa, các cung được tính ở dạng 0. Bước (2) tạo một phân vùng có cung đầu tiên = 2048 (giá trị mặc định bởi fdisk) và

khu vực cuối cùng = (4096 * 2 1) = 8191. Ở Bước (3), độ lệch bắt đầu và giới hạn kích thước đều tính bằng byte = khu vực * kích thước khu vực

(512). Trong Setp (4), kích thước hệ thống tệp là các khối 4096 1024=3072 (1 KB). Vì kích thước hệ thống tệp nhỏ hơn 4096

khối, nó chỉ yêu cầu một nhóm khối, giúp đơn giản hóa cả thuật toán truyền tải hệ thống tệp và việc quản lý
inode và khối đĩa.

7.11.2 Định dạng phân vùng thẻ SD dưới dạng hệ thống tệp

Trong các phần sau, chúng ta sẽ sử dụng ảnh đĩa SDC có nhiều phân vùng. Hình ảnh đĩa được tạo như sau.

(1). Chạy tập lệnh sh mkdisk với tên tệp

# sh script mkdisk: Chạy dưới dạng tên đĩa mkdisk

dd if=/dev/zero of=$1 bs=1024 count=21024 # có kích thước khoảng 2 MB

dd if=MBR of=$1 bs=512 count=1 conv=notrunc # MBR chứa ptable

BEGIN=$(expr 2048 \* 512)

vì tôi trong 1 2 3 4; LÀM

losttup –d /dev/loop$I # xóa thiết bị vòng lặp đang thoát

END=$(expr $BEGIN + 5110000)

losttup –o $BEGIN –sizelimt $END /dev/loop$I $1

gắn kết /dev/loop$I /mnt

(cd /mnt; mkdir bin boot dev, v.v. người dùng tmp)

số lượng /mnt

BEGIN=$(expr $BEGIN + 5120000)

xong

(2). Tệp MBR là hình ảnh MBR chứa bảng phân vùng được tạo bởi fdisk. Thay vì sử dụng fdisk để phân vùng ổ đĩa mới

ảnh đĩa theo cách thủ công, chúng tôi chỉ cần chuyển tệp MBR vào khu vực MBR của ảnh đĩa. Đĩa kết quả

hình ảnh có 4 phân vùng.

Vách ngăn khu vực bắt đầu Khu vực cuối Kích thước (khối 1 KB)

1 2048 10.247 5000

2 12.048 22.047 5000

3 22.048 32.047 5000

4 32.048 42.047 5000

Loại phân vùng không quan trọng nhưng nên đặt thành 0x90 để tránh nhầm lẫn với các hệ điều hành khác, chẳng hạn như

Linux, sử dụng loại phân vùng 0x82–0x83.

7.11.3 Tạo thiết bị vòng lặp cho phân vùng thẻ SD

(3). Tập lệnh mkdisk tạo các thiết bị lặp cho các phân vùng, định dạng từng phân vùng dưới dạng hệ thống tệp EXT2 và điền vào nó

với một số thư mục. Sau khi tạo các thiết bị vòng lặp, mỗi phân vùng có thể được truy cập bằng cách gắn các thiết bị tương ứng của nó vào

thiết bị vòng lặp, như trong


Machine Translated by Google

248 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

gắn kết /dev/loopN MOUNT_POINT # N=1 đến 4

(4). Sửa đổi tập lệnh liên kết biên dịch bằng cách sao chép hình ảnh chế độ Người dùng vào thư mục /bin của phân vùng SDC.

7.12 Hệ thống nhúng với hệ thống tệp SDC

Trong phần này, chúng tôi sẽ trình bày một hệ thống nhúng, ký hiệu là C7.11, hỗ trợ các quy trình động với SDC làm thiết bị lưu trữ dung lượng

lớn. Tất cả hình ảnh chế độ Người dùng đều là các tệp thực thi ELF trong thư mục /bin của hệ thống tệp (EXT2) trên SDC.

Hệ thống sử dụng phân trang động 2 cấp độ. Không gian VA kernel là từ 0 đến 258 MB. Chế độ người dùng Dung lượng VA từ 2 đến 2 GB + kích thước

hình ảnh Umode. Để đơn giản hóa cuộc thảo luận, chúng tôi giả sử rằng mọi kích thước hình ảnh Umode là bội số của 1 MB, ví dụ: 4 MB. Khi tạo một

tiến trình, bảng trang cấp 2 và các khung trang của nó được phân bổ động. Khi một quá trình kết thúc, bảng trang và khung trang của nó được giải

phóng để sử dụng lại. Hệ thống bao gồm các thành phần sau.

(1). Tệp ts.s: Tệp ts.s giống với tệp của Chương trình C7.8.

(2). Tệp kernel.c: Tệp kernel.c cũng giống như tệp của Chương trình C7.8.

7.12.1 Trình điều khiển thẻ SD sử dụng Semaphore

(3). Tệp sdc.c: Tệp này chứa trình điều khiển SDC. Trình điều khiển SDC được điều khiển bằng ngắt nhưng nó cũng hỗ trợ kiểm tra vòng vì lý do

sau. Khi hệ thống khởi động, chỉ có tiến trình ban đầu P0 đang chạy. Sau khi khởi tạo trình điều khiển SDC, nó gọi mbr() để hiển thị các

phân vùng SDC. Vì chưa có tiến trình nào khác nên P0 không thể chuyển sang chế độ ngủ hoặc bị chặn.

Vì vậy, nó sử dụng tính năng bỏ phiếu để đọc các khối SDC. Tương tự, nó cũng sử dụng tính năng thăm dò để tải hình ảnh Umode của P1 từ SDC.

Sau khi tạo P1 và chuyển sang chạy P1, các tiến trình và trình điều khiển SDC sử dụng semaphore để đồng bộ hóa. Trong hệ thống thực, CPU

nhanh hơn nhiều so với các thiết bị I/O. Sau khi thực hiện thao tác I/O cho một thiết bị, một tiến trình thường có nhiều thời gian để tự

tạm dừng để chờ thiết bị bị gián đoạn. Trong trường hợp này, chúng tôi có thể sử dụng chế độ ngủ/thức để đồng bộ hóa quy trình và trình xử

lý ngắt. Tuy nhiên, máy ảo mô phỏng có thể không tuân theo thứ tự thời gian này. Người ta quan sát thấy rằng trên ARM Versatilepb được mô

phỏng trong QEMU, sau khi một quá trình đưa ra thao tác I/O cho SDC, trình xử lý ngắt SDC luôn hoàn thành trước khi quá trình tự tạm dừng.

Điều này làm cho cơ chế ngủ/thức không phù hợp để đồng bộ hóa giữa tiến trình và trình xử lý ngắt SDC. Vì lý do này, nó sử dụng semaphore

để đồng bộ hóa.

Trong trình điều khiển SDC, chúng tôi xác định một semaphore s với giá trị ban đầu là 0. Sau khi thực hiện thao tác I/O, quy trình sử dụng

P(s) để tự chặn, chờ ngắt SDC. Khi bộ xử lý ngắt SDC hoàn tất quá trình truyền dữ liệu, nó sẽ sử dụng V(s) để bỏ chặn quá trình. Vì thứ tự

của P và V trên các đèn hiệu không quan trọng nên việc sử dụng các đèn hiệu sẽ ngăn chặn mọi điều kiện chạy đua giữa các tiến trình và trình

xử lý ngắt. Sau đây liệt kê mã trình điều khiển SDC.

#include "sdc.h"

#xác định FBLK_SIZE 1024

phân vùng int, bsector;

typedef struct semaphore{ // bộ xử lý đơn, không cần spinlock

giá trị int;

PROC * hàng đợi;

}SEMAPHORE;

SEMAPHORE s; // semaphore để đồng bộ hóa trình điều khiển SDC

int P(SEMAPHORE *s)

int sr = int_off();

s->giá trị--;

nếu (s->giá trị < 0){


Machine Translated by Google

7.12 Hệ thống nhúng với hệ thống tệp SDC 249

đang chạy->trạng thái = KHỐI;

enqueue(&s->queue, đang chạy); tswitch();

int_on(sr);

int V(SEMAPHORE *s)

PROC *p;

int sr = int_off();

s->giá trị++;

nếu (s->giá trị <= 0){

p = dequeue(&s->queue);

p->trạng thái = SẴN SÀNG;

enqueue(&readyQueue, p);

int_on(sr);

phân vùng cấu trúc { // bảng phân vùng trong MBR

ổ u8; /* 0x80 - đang hoạt động */

đầu u8; /* đầu bắt đầu */

lĩnh vực u8; /* khu vực bắt đầu */

xi lanh u8; /*xi lanh khởi động */

u8 sys_type; /*kiểu phân vùng */

u8 end_head; /* đầu cuối */

u8 end_sector; /*kết thúc khu vực */

u8 end_xi lanh; /* hình trụ cuối */ int start_sector; /*

khu vực bắt đầu đếm từ 0 */ int nr_sectors; /* số lượng các cung trong phân vùng */

};

int mbr(phân vùng int)

int tôi;

char buf[FBLK_SIZE];

phân vùng cấu trúc *p;

GD *gp; // Con trỏ mô tả nhóm EXT2

bsector = 0;

printf("đọc MBR để hiển thị bảng phân vùng\n");

get_block(0, buf);

p = (phân vùng cấu trúc *)&buf[0x1bE];

printf("Kích thước bắt đầu P#\n");

for (i=1; i<=4; i++){ // ĐÁNH GIÁ: chỉ có 4 phân vùng nguyên tố

printf("%d %d %d\n", i, p->start_sector, p->nr_sector);

nếu (i==phân vùng)

bsector = p->start_sector;

p++;

printf("partition=%d bsector=%d ", phân vùng, bsector);

get_block(2, buf);

gp = (GD *)buf;

iblk = gp->bg_inode_table; bmap = gp-

>bg_block_bitmap; imap = gp->bg_inode_bitmap;

printf("bmap=%d imap=%d iblk=%d ", bmap,

imap, iblk); printf("MBR xong\n");


Machine Translated by Google

250 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

// các biến được chia sẻ giữa tiến trình và trình xử lý ngắt dễ bay hơi char *rxbuf,

*txbuf; biến động int rxcount, txcount,

rxdone, txdone;

int sdc_handler()

trạng thái u32, status_err, *up;

int tôi;

// đọc thanh ghi trạng thái để tìm ra TXempty hoặc RxAvail

trạng thái = *(u32 *)(cơ sở + TÌNH TRẠNG);

if (status & (1<<17)){ // RxFull: đọc 16 u32 cùng một lúc;

//printf("Ngắt RX: ");

up = (u32 *)rxbuf;

status_err = trạng thái & (DCRCFAIL | DTIMEOUT | RXOVERR);

if (!status_err && rxcount) {

//printf("R%d", rxcount);

vì (i = 0; i < 16; i++)

*(up + i) = *(u32 *)(base + FIFO);

lên += 16;

rxcount -= 64;

rxbuf += 64;

trạng thái = *(u32 *)(cơ sở + TÌNH TRẠNG); // xóa ngắt Rx

if (rxcount == 0)

{ do_command(12, 0, MMC_RSP_R1); // dừng truyền

nếu (hasP1)

V(&s); // bởi ngữ nghĩa

khác

rxdone = 1; // bằng cách bỏ phiếu

else if (status & (1<<18)){ // TXempty: ghi 16 u32 mỗi lần //printf("TX ngắt: ");

up = (u32 *)txbuf;

status_err = trạng thái & (DCRCFAIL | DTIMEOUT);

if (!status_err && txcount) {

//printf("W%d", txcount);

vì (i = 0; i < 16; i++)

*(u32 *)(cơ sở + FIFO) = *(up + i);

lên += 16;

txcount -= 64;

txbuf += 64; // nâng cao txbuf cho lần viết tiếp theo

trạng thái = *(u32 *)(cơ sở + TÌNH TRẠNG); // xóa ngắt Tx

nếu (txcount == 0){

do_command(12, 0, MMC_RSP_R1); // dừng truyền

nếu (hasP1)

V(&s); // bởi ngữ nghĩa

khác

txdone = 1; // bằng cách bỏ phiếu

//printf("ghi để xóa thanh ghi\n");

*(u32 *)(base + STATUS_CLEAR) = 0xFFFFFFFF;


Machine Translated by Google

7.12 Hệ thống nhúng với hệ thống tệp SDC 251

// printf("Xử lý ngắt SDC đã hoàn tất\n");

int delay(){ int i; cho (i=0; i<1000; i++); }

int do_command(int cmd, int arg, int resp)

*(u32 *)(cơ sở + ARGUMENT) = (u32)arg; *(u32 *)(cơ sở +

LỆNH) = 0x400 | (resp<<6) | cmd; trì hoãn();

int sdc_init()

u32 RCA = (u32)0x45670000; // RCA mã hóa cứng của QEMU

căn cứ = (u32)0x10005000; // Địa chỉ cơ sở PL180

printf("sdc_init : ");

*(u32 *)(cơ sở + POWER) = (u32)0xBF; // bật nguồn

*(u32 *)(cơ sở + CLOCK) = (u32)0xC6; // CLK mặc định

// gửi chuỗi lệnh init do_command(0, 0,

MMC_RSP_NONE);// trạng thái rảnh

do_command(55, 0, MMC_RSP_R1); // trạng thái sẵn sàng

do_command(41, 1, MMC_RSP_R3); // đối số không được bằng 0

do_command(2, 0, MMC_RSP_R2); // hỏi CID thẻ

do_command(3, RCA, MMC_RSP_R1); // gán RCA

do_command(7, RCA, MMC_RSP_R1); // trạng thái truyền: phải sử dụng RCA

do_command(16, 512, MMC_RSP_R1); // thiết lập độ dài khối dữ liệu

// đặt bit thanh ghi MASK0 ngắt = RxFULL(17)|TxEmpty(18) *(u32 *)(base + MASK0) = (1<<17)|(1<<18);

// khởi tạo semaphore s

s.giá trị = 0; s.queue = 0;

int get_block(int blk, char *buf)

u32 cmd, arg;

rxbuf = buf; rxcount = FBLK_SIZE;

rxdone = 0;

*(u32 *)(cơ sở + DATATIMER) = 0xFFFF0000;

// ghi data_len vào datalength reg

*(u32 *)(cơ sở + DATALENGTH) = FBLK_SIZE;

// 0x93=|9|0011|=|9|DMA=0,0=BLOCK,1=Máy chủ<-Card,1=Bật // *(u32 *)(base + DATACTRL) =

0x93;

cmd = 18; // CMD17 = ĐỌC một khu vực

arg = ((bsector + blk*2)*512); do_command(cmd,

arg, MMC_RSP_R1); // 0x93=|9|0011|=|9|

DMA=0,0=BLOCK,1=Máy chủ<-Card,1=Bật

*(u32 *)(cơ sở + DATAACTRL) = 0x93;

nếu (hasP1)

P(&s); // bởi ngữ nghĩa

khác

while(rxdone == 0); // bằng cách bỏ phiếu

int put_block(int blk, char *buf)

{
Machine Translated by Google

252 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

u32 cmd, arg;

txbuf = buf; txcount = FBLK_SIZE;

txdone = 0;

*(u32 *)(cơ sở + DATATIMER) = 0xFFFF0000;

*(u32 *)(cơ sở + DATALENGTH) = FBLK_SIZE;

cmd = 25; // CMD24 = Viết một cung từ

arg = (u32)((bsector + blk*2)*512); do_command(cmd,

arg, MMC_RSP_R1); // ghi 0x91=|9|0001|=|9|

DMA=0,BLOCK=0,0=Host->Thẻ, Bật

*(u32 *)(cơ sở + DATAACTRL) = 0x91; // Máy chủ-> thẻ

nếu (hasP1)

P(&s); // bởi ngữ nghĩa

khác

while(txdone == 0); // bằng cách bỏ phiếu

7.12.2 Hạt nhân hệ thống sử dụng hệ thống tệp SD

(4). Tệp kernel.c: Tệp kernel.c giống với tệp của Chương trình C6.x.
(5). tập tin tc:

/****************** tập tin tc **********************/

#include "type.h" #include

"string.c" #define FBLK_SZIE

1024

#include "uart.c"

#include "kbd.c"

#include "timer.c"

#include "vid.c"

#include "ngoại lệ.c" #include

"queue.c"

#include "kernel.c"

#include "chờ.c"

#include "fork.c"

#include "exec.c"

#include "svc.c"

#include "loadelf.c"

#include "thread.c"

#include "sdc.c"

int copy_vector_table(){ // GIỐNG như chương trình C6.x }

int mkPtable() { // giống như chương trình C6.x }

int irq_chandler(){// giống như trước nhưng trình xử lý ngắt ADD SDC }

extern int hasP1, phân vùng, bsector; // định nghĩa trong file sdc.c

int chính()

fbuf_init();
Machine Translated by Google

7.12 Hệ thống nhúng với hệ thống tệp SDC 253

printf("Chào mừng đến với WANIX in Arm\n");

printf("Màn hình LCD được khởi tạo: fbuf = %x\n", fb); kbd_init(); uart_init(); // mã

kích hoạt các

ngắt VIC, SIC và

thiết bị, bao gồm SDC kernel_init();

hasP1 = 0; // chỉ có P0 chạy vào thời điểm này // chọn số phân

phân vùng = 2; vùng

sdc_init(); // khởi tạo trình điều khiển SDC

mbr(phân vùng); // hiển thị bảng phân vùng và đặt bsector kfork("/bin/u1"); // tạo P1 với ảnh

Umode

hasP1 = 1;

printf("P0 chuyển sang P1\n");

while(1){ // Quá trình chuyển đổi P0 bất cứ khi nào hàng đợi sẵn sàng không trống

trong khi (readyQueue == 0);

tswitch();

7.12.3 Trình diễn hệ thống tệp SDC

Hình 7.11 thể hiện kết quả đầu ra mẫu khi chạy hệ thống mẫu C7.11.

7.13 Hình ảnh hạt nhân khởi động từ SDC

Thông thường, hệ thống dựa trên ARM có bộ khởi động tích hợp được triển khai trong phần sụn. Khi hệ thống ARM như vậy khởi động,
bộ khởi động chương trình cơ sở tích hợp trước tiên sẽ tải bộ khởi động giai đoạn 2, ví dụ như Das Uboot (UBOOT 2016), từ phân
vùng (FAT) của bộ nhớ flash hoặc SDC và thực thi nó. Sau đó, trình khởi động giai đoạn 2 sẽ khởi động một hệ điều hành thực, chẳng
hạn như Linux, từ một phân vùng khác. Máy ảo ARM Verstilepb được mô phỏng là một ngoại lệ. Khi máy ảo Versatilepb được mô phỏng
khởi động, QEMU tải hình ảnh hạt nhân được chỉ định về 0x10000 và chuyển quyền điều khiển trực tiếp sang hình ảnh hạt nhân đã tải,
bỏ qua giai đoạn khởi động thông thường của hầu hết các máy thực hoặc ảo khác. Trên thực tế, khi Vesatilepb VM khởi động, QEMU chỉ
cần tải một tệp hình ảnh được chỉ định và thực thi hình ảnh đã tải. Nó không biết và cũng không quan tâm đến việc hình ảnh đó là
nhân hệ điều hành hay chỉ là một đoạn mã thực thi. Hình ảnh được tải có thể là một trình khởi động, có thể được sử dụng để khởi
động nhân hệ điều hành thực từ thiết bị lưu trữ. Trong phần này, chúng tôi sẽ phát triển một trình khởi động cho máy ảo Versatilepb
được mô phỏng để khởi động nhân hệ thống từ các phân vùng SDC. Trong sơ đồ này, mỗi phân vùng của SDC là một hệ thống tệp (EXT2).
Hình ảnh nhân hệ thống là một tệp trong thư mục /boot của phân vùng SDC. Khi hệ thống khởi động, QEMU tải bộ khởi động về 0x10000
và thực thi nó trước tiên. Bộ khởi động có thể yêu cầu một phân vùng để khởi động hoặc có thể chỉ khởi động từ một phân vùng mặc
định. Sau đó, nó tải hình ảnh nhân hệ thống từ thư mục /boot trong phân vùng SDC và chuyển quyền điều khiển sang hình ảnh nhân,
khiến nhân hệ điều hành khởi động. Những lợi thế của chương trình này là hai lần. Đầu tiên, nhân hệ thống có thể được tải vào
bất kỳ vị trí bộ nhớ nào và chạy từ đó, khiến nó không còn bị giới hạn ở 0x10000 như QEMU dành riêng. Điều này sẽ làm cho hệ thống
tương thích hơn với các máy thực hoặc ảo khác yêu cầu giai đoạn khởi động. Thứ hai, bộ khởi động có thể thu thập thông tin từ
người dùng và chuyển chúng dưới dạng tham số khởi động tới kernel. Nếu muốn, bộ khởi động cũng có thể thiết lập một môi trường
thực thi thích hợp trước khi chuyển quyền điều khiển sang hạt nhân, giúp đơn giản hóa mã khởi động của hạt nhân. Ví dụ: nếu nhân
hệ thống được biên dịch bằng địa chỉ ảo, thì bộ khởi động có thể thiết lập MMU trước để cho phép hạt nhân khởi động bằng cách sử
dụng trực tiếp các địa chỉ ảo. Sau đây cho thấy tổ chức của một hệ thống như vậy. Nó bao gồm một bộ khởi động, được QEMU tải về
0x10000 khi máy ảo Versatilepb được mô phỏng khởi động. Sau đó, trình khởi động khởi động hạt nhân hệ thống từ phân vùng SDC và khởi động hạt nh
Đầu tiên, chúng tôi hiển thị các thành phần của chương trình khởi động.
Machine Translated by Google

254 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

Hình 7.11 Đầu ra mẫu của hệ thống có SDC

7.13.1 Chương trình khởi động SDC

(1). file ts.s của booter: Đây là điểm vào của chương trình booter. Nó khởi tạo UART cho cổng I/O nối tiếp trong quá trình khởi động.

Để giữ cho mã khởi động đơn giản, nó không sử dụng các ngắt. Vì vậy, trình điều khiển UART sử dụng tính năng thăm dò cho cổng I/O nối tiếp.

Trình khởi động tải hình ảnh hạt nhân từ phân vùng SDC lên 1 MB. Sau đó nó nhảy tới đó để khởi động kernel.

/*************** tệp ts.s của booter *******************/

.global reset_handler, chính

reset_handler:

/*khởi tạo ngăn xếp SVC */

ldr sp, =svc_stack_top

BL uart_init

BL chính

mov pc, #0x100000 // chuyển sang kernel đã tải ở mức 1MB

(2). File tc của Booter: Đây là chức năng chính của chương trình booter.

#include "type.h"

#include "string.c"

#include "uart.c"

#include "sdc.c"

#include "boot.c"

int chính()

{
Machine Translated by Google

7.13 Hình ảnh hạt nhân khởi động từ SDC 255

printf("Chào mừng đến với ARM EXT2 Booter\n");

sdc_init();

mbr(); // đọc và hiển thị bảng phân vùng // ảnh kernel khởi

khởi động(); động từ một phân vùng printf("BACK FROM booter: enter a

key\n");

ugetc();

(3). Tệp sd.c của Booter: Tệp này triển khai trình điều khiển SDC của booter. Nó cung cấp một

getblk(int blk, char *địa chỉ)

chức năng tải một khối (1 KB) từ SDC vào địa chỉ bộ nhớ được chỉ định. Để giữ cho bộ khởi động đơn giản, trình điều khiển
SDC của bộ khởi động sử dụng tính năng thăm dò cho khối I/O.

(4). Tệp boot.c: Tệp này triển khai bộ khởi động SDC. Đối với máy ảo ARM Versatilepb được mô phỏng, bộ khởi động là một hình
ảnh riêng biệt. Nó được QEMU tải tới 0x10000 và bắt đầu thực thi từ đó. Sau đó, nó khởi động một hình ảnh hạt nhân trong
thư mục /boot của phân vùng SDC. Hàm mbr() hiển thị bảng phân vùng của SDC và nhắc số phân vùng để khởi động. Nó ghi phân
vùng và số khu vực bắt đầu thành 0x200000 (2 MB) để lấy kernel. Sau đó, nó gọi boot(), định vị tệp ảnh kernel trong thư
mục /boot và tải ảnh kernel về 0x100000 (1 MB).

/***************** tập tin boot.c của booter *******************/

int bmap, imap, iblk, blk, offset; phân vùng cấu

trúc {

ổ u8; /* 0x80 - đang hoạt động */

đầu u8; /* đầu bắt đầu */ khu vực u8; /* khu

vực bắt đầu */

xi lanh u8; /*xi lanh khởi động */

u8 sys_type; /*kiểu phân vùng */

u8 end_head; /* đầu cuối */

u8 end_sector; /*kết thúc khu vực */

u8 end_xi lanh; /* kết thúc hình trụ */

u32 start_sector; /* khu vực bắt đầu đếm từ 0 */

u32 nr_sector; /* số lượng các cung trong phân vùng */

};

char buf[1024], buf1[1024], buf2[1024];

int mbr()

int tôi, pno, bno;

int *phân vùng = (int *)0x200000;

int *ngành = (int *)0x200004;

dòng char[8], c;

phân vùng cấu trúc *p;

GD *gp;

bsector = 0;

printf("đọc MBR để hiển thị bảng phân vùng\n"); get_block(0, buf); p =

(phân vùng cấu trúc

*)&buf[0x1bE]; printf("Kích thước bắt đầu P#\n");

cho (i=1; i<=4; i++){

printf("%d %d %d\n", i, p->start_sector, p->nr_sector);

p++;

printf("nhập số phân vùng [1-4]:");

c = ugetc();
Machine Translated by Google

256 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

pno = c - '0';

if (pno < 1 || phân vùng > 4) pno = 2; // sử

dụng phân vùng mặc định 2

p = (phân vùng cấu trúc *)&buf[0x1bE]; cho (i=1; i<=4;

i++){ if (i==pno){

bno = p->start_sector; phá vỡ;

p++;

printf("partition=%d bsector = %d\n", phân vùng, bsector); *phân vùng = pno; *bsector =

bno; get_block(2, buf); // đọc khối mô tả nhóm

gp = (GD *)buf; // bộ mô tả nhóm truy cập 0

iblk = gp->bg_inode_table;

bmap = gp->bg_block_bitmap;

imap = gp->bg_inode_bitmap;

printf("bmap=%d imap=%d iblk=%d\n", bmap, imap, iblk);

printf("MBR xong\n");

tìm kiếm int(INODE *ip, char *name)

int tôi;

ký tự c, *cp;

TRỰC TIẾP *dp;

cho (i=0; i<12; i++){

if (ip->i_block[i]){

get_block(ip->i_block[i], buf2);

dp = (DIR *)buf2;

cp = buf2;

trong khi (cp < &buf2[1024]){

c = dp->name[dp->name_len]; // lưu byte cuối cùng dp->name[dp->name_len]

= 0; printf("%s ", dp->name); if

( strcmp(dp->name, name) == 0 ){

printf("đã tìm thấy %s\n", tên);

return(dp->inode);

dp->name[dp->name_len] = c; // khôi phục byte cuối cùng đó

cp += dp->rec_len;

dp = (TRỰC TIẾP *)cp;

printf("serach thất bại\n");

trả về 0;

khởi động()

int i, ino, blk, iblk, count;

char *cp, *name[2],*location;

u32 *lên;

GD *gp;

INODE *ip;

TRỰC TIẾP *dp;


Machine Translated by Google

7.13 Hình ảnh hạt nhân khởi động từ SDC 257

name[0] = "khởi động";

tên[1] = "hạt nhân";

mbr();

/* đọc blk#2 để lấy phần mô tả nhóm 0 */

get_block(2, buf1);

gp = (GD *)buf1;

iblk = (u16)gp->bg_inode_table; getblk(iblk,

buf1); ip = (INODE *)buf1 + // đọc khối khối inode đầu tiên

1; /*tìm kiếm tên hệ thống */ // ip-> inode gốc #2

cho (i=0; i<2; i++){

ino = search(ip, name[i]) - 1;

nếu (ino < 0)

trả về 0;

get_block(iblk+(ino/8), buf1); // đọc khối inode của ino

ip = (INODE *)buf1 + (ino % 8);

/* đọc khối gián tiếp vào b2 */

nếu (ip->i_block[12]) // chỉ khi có khối gián tiếp

get_block(ip->i_block[12], buf2);

vị trí = (char *)0x100000;

đếm = 0;

cho (i=0; i<12; i++){

get_block(ip->i_block[i], vị trí);

uputc('*');

vị trí += 1024;

đếm++;

if (ip->i_block[12]){ // chỉ khi file có khối gián tiếp

up = (u32 *)buf2; trong

khi(*lên){

get_block(*up, location); uputc('.');

vị trí += 1024;

lên++; đếm++;

printf("tải xong\n", count);

(5). Hình ảnh hạt nhân và chế độ người dùng: Hình ảnh hạt nhân và chế độ người dùng được tạo bởi các tệp sh script
sau, tạo các tệp hình ảnh và sao chép chúng vào phân vùng SD. Lưu ý rằng VA khởi động của hạt nhân là 0x100000
(1 MB) và VA khởi đầu của hình ảnh Umode là 0x80000000 (2 GB).

# tập lệnh mkkernel

arm-none-eabi-as -mcpu=arm926ej-s ts.s -o ts.o arm-none-eabi-gcc -c

-mcpu=arm926ej-s tc -o to

arm-none-eabi-ld -T t.ld ts.o tới -Ttext=0x100000 -o kernel.elf

arm-none-eabi-objcopy -O hạt nhân nhị phân kernel.elf

cho tôi trong 1 2 3 4

LÀM

mount /dev/loop$I /mnt # giả sử phân vùng SDC là thiết bị lặp cp -av kernel /mnt/boot/

số lượng /mnt
Machine Translated by Google

258 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

xong

# tập lệnh mku: mku u1; mku u2, v.v.

arm-none-eabi-as -mcpu=arm926ej-s us.s -o us.o arm-none-eabi-gcc -c

-mcpu=arm926ej-s -o $1.o $1.c

arm-none-eabi-ld -T u.ld us.o $1.o -Ttext=0x80000000 -o $1

cho tôi trong 1 2 3 4

LÀM

mount /dev/loop$I /mnt cp -av $1 /

mnt/bin/

số lượng /mnt

xong

7.13.2 Trình diễn hạt nhân khởi động từ SDC

Hệ thống mẫu C7.12 thể hiện khả năng khởi động nhân hệ điều hành từ SDC.
Hình 7.12 hiển thị màn hình UART của booter. Nó hiển thị bảng phân vùng SDC và yêu cầu phân vùng để khởi động. Nó định
vị tệp hình ảnh hạt nhân, /boot/kernel, trong phân vùng SDC và tải hình ảnh hạt nhân về 0x100000 (1 MB).
Sau đó, nó gửi CPU để thực thi mã hạt nhân đã tải. Khi kernel khởi động, nó sử dụng phân trang tĩnh 2 cấp. Vì hạt nhân
được liên kết biên dịch với các địa chỉ thực nên nó có thể thực thi tất cả mã trực tiếp khi khởi động. Trong trường hợp
này, booter không cần xây dựng bất kỳ bảng trang nào cho kernel. Các bảng trang sẽ được chính kernel xây dựng khi nó khởi động.
Hình 7.13 hiển thị màn hình khởi động của kernel. Trong kernel_init(), nó khởi tạo cấu trúc dữ liệu kernel, xây dựng
bảng trang 2 cấp cho các tiến trình và chuyển pgdir sang sử dụng phân trang tĩnh 2 cấp.

Hình 7.12 Trình diễn bộ khởi động SDC


Machine Translated by Google

7.13 Hình ảnh hạt nhân khởi động từ SDC 259

Hình 7.13 Trình diễn khởi động hạt nhân hệ điều hành từ SDC

7.13.3 Khởi động hạt nhân từ SDC bằng phân trang động

Hệ thống mẫu C7.13 thể hiện khả năng khởi động hạt nhân sử dụng phân trang động 2 cấp độ. Phần booter vẫn giống như trước. Thay
vì phân trang tĩnh, kernel sử dụng phân trang động 2 cấp. Hình 7.14 hiển thị màn hình khởi động của kernel.
Như hình minh họa, tất cả các mục trong bảng trang của P1 đều được cấp phát động các khung trang.

Hình 7.14 Khởi động hạt nhân hệ điều hành từ SDC với phân trang động
Machine Translated by Google

260 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

7.13.4 Khởi động hai giai đoạn

Trong nhiều hệ thống dựa trên ARM, việc khởi động nhân hệ điều hành bao gồm hai giai đoạn. Khi hệ thống ARM khởi động, bộ tải khởi động tích hợp

trong chương trình cơ sở của hệ thống sẽ tải bộ khởi động từ thiết bị lưu trữ, chẳng hạn như bộ nhớ flash hoặc thẻ SD và chuyển quyền điều

khiển sang bộ khởi động đã tải. Sau đó, trình khởi động sẽ tải nhân hệ điều hành (OS) từ thiết bị có khả năng khởi động và chuyển quyền điều khiển

sang nhân hệ điều hành, khiến nhân hệ điều hành khởi động. Trong trường hợp này, bộ tải khởi động tích hợp của hệ thống là bộ khởi động giai đoạn

1, tải và thực thi bộ khởi động giai đoạn 2, được thiết kế để khởi động một hình ảnh nhân hệ điều hành cụ thể. Bộ khởi động giai đoạn 2 có thể

được cài đặt trên SDC để khởi động các hình ảnh hạt nhân khác nhau. Một số bo mạch ARM yêu cầu bộ khởi động giai đoạn 2 phải được cài đặt trong

phân vùng DOS, nhưng hình ảnh hạt nhân có khả năng khởi động có thể nằm trong một phân vùng khác. Nguyên lý và kỹ thuật cài đặt booter vào SDC

cũng giống như cài đặt booter vào đĩa cứng hoặc ổ USB thông thường. Bạn đọc có thể tham khảo Chap. 3 về Khởi động hệ điều hành của Wang (2015)

để biết thêm chi tiết. Trong phần này, chúng tôi trình diễn trình khởi động hai giai đoạn cho máy ảo ARM Versatilepb được mô phỏng. Đầu tiên,

chúng tôi hiển thị các đoạn mã của bộ khởi động giai đoạn 1.

7.13.4.1 Trình khởi động giai đoạn 1

(1). ts.s của bộ khởi động giai đoạn 1: khởi tạo UART0 cho I/O nối tiếp, gọi main() để tải bộ khởi động giai đoạn 2 lên 2 MB. Sau đó nhảy tới

bộ khởi động giai đoạn 2.

.chữ

.code 32

reset_handler:

ldr sp, =svc_stack_top // đặt ngăn xếp SVC

bl uart_init // khởi tạo UART0 cho I/O nối tiếp

bl chính // gọi hàm main() trong C

mov máy tính, #0x200000 // chuyển sang booter giai đoạn 2 với dung lượng 2MB

(2). Chức năng boot() của bộ khởi động giai đoạn 1: tải bộ khởi động giai đoạn 2 từ SDC lên 2 MB và nhảy để thực thi bộ khởi động giai đoạn 2 tại
2 MB.

int boot1() // boot1() trong file boot.c

int tôi;

char *p = (char *)0x200000;

cho (i=1; i<10; i++){ // tải 10 khối từ SDC

getblk(i, p);

printf("%c", '.');

p += 1024;

printf("\ntải xong: chuyển sang bộ khởi động giai đoạn 2\n");

(3). tc của bộ khởi động giai đoạn 1.

#include "type.h"

#include "string.c"
#include "uart.c"

#include "sdc.c"

#include "boot.c"

int chính()

printf("Bộ khởi động giai đoạn 1\n");


Machine Translated by Google

7.13 Hình ảnh hạt nhân khởi động từ SDC 261

sdc_init(); // khởi tạo trình điều khiển SDC

boot1(); // tải bộ khởi động giai đoạn 2 lên 2MB


}

7.13.4.2 Bộ khởi động Giai đoạn

2 Bộ khởi động giai đoạn 2 giống với bộ khởi động trong Sect. 7.13.1. Nó được cài đặt vào phần trước của SDC, phần này sẽ được bộ

khởi động giai đoạn 1 tải để thực thi. Kích thước khởi động giai đoạn 2 là khoảng 8 KB. Trên SDC có các phân vùng, phân vùng 1 bắt

đầu từ khu vực 2048, do đó các khối 1 đến 1023 là các khoảng trống trên SDC. Bộ khởi động giai đoạn 2 được cài đặt vào khối 1 đến 8

của SDC bằng lệnh dd sau.

dd if=booter2.bin of=../sdc bs=1024 count=8 seek=1 conv=notrunc

7.13.5 Trình diễn khởi động hai giai đoạn

Hệ thống mẫu C7.14 thể hiện khả năng khởi động hai giai đoạn. Hệ thống được chạy từ thư mục khởi động giai đoạn 1 bởi

qemu-system-arm -M linh hoạtpb -m 512M -sd ../sdc -kernel booter1.bin \


-serial mon:stdio

Hình 7.15 hiển thị màn hình của bộ khởi động 2 giai đoạn.

Hình 7.16 hiển thị màn hình khởi động kernel sau khi khởi động bằng bộ khởi động 2 giai đoạn.

Hình 7.15 Màn hình của bộ khởi động hai giai đoạn
Machine Translated by Google

262 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

Hình 7.16 Trình diễn bộ khởi động 2 giai đoạn

7.14 Tóm tắt

Chương này đề cập đến việc quản lý quy trình, cho phép chúng ta tạo và chạy các quy trình một cách linh hoạt trong các hệ thống nhúng. Để giữ

cho hệ thống đơn giản, nó chỉ hiển thị các chức năng quản lý quy trình cơ bản, bao gồm tạo quy trình, chấm dứt quy trình, đồng bộ hóa quy trình

và chờ kết thúc quy trình con. Trong suốt chương này, nó cho thấy cách sử dụng quản lý bộ nhớ để cung cấp cho mỗi quy trình một không gian địa

chỉ ảo ở chế độ Người dùng riêng tư, tách biệt với các quy trình khác và được bảo vệ bởi phần cứng MMU. Các sơ đồ quản lý bộ nhớ sử dụng cả các

phần một cấp và phân trang tĩnh và động hai cấp. Ngoài ra, nó còn thảo luận về các khái niệm và kỹ thuật nâng cao của vfork và thread. Cuối cùng,

nó cho thấy cách sử dụng thẻ SD để lưu trữ cả tệp hình ảnh chế độ người dùng và hạt nhân trong hệ thống tệp SDC cũng như cách khởi động hạt nhân

hệ thống từ các phân vùng SDC. Với nền tảng này, chúng tôi sẵn sàng trình bày thiết kế và triển khai hệ điều hành có mục đích chung cho các hệ

thống nhúng.

Danh sách các chương trình mẫu

C7.1: Chế độ hạt nhân và người dùng

C7.2: Nhiệm vụ trong cùng một miền C7.3:

Nhiệm vụ với từng miền riêng lẻ

C7.4: Fork-exec

C7.5: Vfork

C7.6: Threads

C7.7: phân trang tĩnh 2 cấp KML C7.8:

phân trang động 2 cấp KML C7.9: phân trang

tĩnh 1 cấp KMH C7.10 : Phân trang tĩnh 2

cấp KMH C7.11: Phân trang với SDC C7.12:

Khởi động kernel hệ điều hành

từ SDC C7.13: Khởi động kernel với phân trang

động 2 cấp C7.14: Khởi động 2 giai đoạn


Machine Translated by Google

7.14 Tóm tắt 263

Các vấn đề

1. Trong chương trình ví dụ C7.1, cả tswitch() và svc_handler() đều sử dụng

stmfd sp!, {r0-r12, lr}

lưu tất cả các thanh ghi CPU, điều này có thể không cần thiết vì mã được tạo của hầu hết các trình biên dịch ARM C sẽ bảo toàn các thanh ghi

r4–r12 trong các lệnh gọi hàm. Giả sử rằng cả tswitch() và svc_handler() đều sử dụng

stmfd sp!, {r0-r3, lr}

để lưu các thanh ghi CPU. Viết lại tswitch(), svc_handler và kfork() của hệ thống. Xác minh rằng hệ thống sửa đổi hoạt động.

2. Sửa đổi hệ thống mẫu C7.1 để sử dụng kích thước hình ảnh Umage 4 MB.

3. Trong chương trình ví dụ C7.1, nó xây dựng các bảng trang cấp 1 của tất cả (64) PROC một cách tĩnh trong vùng bộ nhớ 6 MB.

Sửa đổi nó để xây dựng bảng trang cấp 1 của quy trình một cách linh hoạt, tức là chỉ khi một quy trình được tạo.

4. Trong chương trình ví dụ C7.1, mỗi tiến trình chạy trong một vùng Umode khác nhau nhưng con trỏ ngăn xếp Umode của mỗi tiến trình
được khởi tạo là

Giải thích tại sao và nó hoạt động như thế nào?

#define UIAMGE_SIZE 0x100000

p->usp = (int *)VA(UIMAGE_SIZE);

5. Trong chương trình ví dụ C7.1, giả sử rằng không gian VA của cả Kmode và Umode là 2 GB. Sửa đổi nó thành 1 GB

Dung lượng Kmode VA và dung lượng Umode VA 3 GB.

6. Đối với hệ thống mẫu C7.4, triển khai cây tiến trình và sử dụng nó trong kexit() và kwait().

7. Đối với hệ thống mẫu C7.4, hãy sửa đổi hàm kexit() để triển khai chính sách sau.

(1). Một tiến trình kết thúc phải xử lý các ZOMBIE con của nó trước tiên, nếu có.

(2). Một tiến trình không thể kết thúc cho đến khi tất cả các tiến trình con đã kết thúc.

Thảo luận về ưu điểm và nhược điểm của các phương án này.

8. Trong tất cả các chương trình ví dụ, mỗi cấu trúc PROC có một kstack cấp phát tĩnh 4 KB.

(1). Triển khai trình quản lý bộ nhớ đơn giản để phân bổ/phân bổ bộ nhớ một cách linh hoạt. Khi hệ thống khởi động, hãy đặt trước một

Vùng 1 MB, ví dụ bắt đầu từ 4 MB, là vùng bộ nhớ trống. Chức năng

char *malloc(kích thước int)

phân bổ một phần bộ nhớ trống có kích thước tính bằng 1 KB byte. Khi một vùng bộ nhớ không còn cần thiết nữa, nó sẽ được giải phóng trở lại

vùng bộ nhớ trống bằng cách

void mfree(char *địa chỉ, kích thước int)

Thiết kế cấu trúc dữ liệu để thể hiện bộ nhớ trống hiện có. Sau đó triển khai các hàm malloc() và mfree().

(2). Sửa đổi trường kstack của cấu trúc PROC thành con trỏ số nguyên

int *kstack;

và sửa đổi hàm kfork() thành

int kfork(int func, int ưu tiên, int stack_size)

phân bổ động vùng bộ nhớ stack_size cho tác vụ mới.

(3). Khi một tác vụ kết thúc, vùng ngăn xếp của nó (cuối cùng) phải được giải phóng. Làm thế nào để thực hiện điều này? Nếu bạn nghĩ bạn có thể

chỉ cần giải phóng vùng ngăn xếp trong kexit(), hãy suy nghĩ lại cẩn thận.
Machine Translated by Google

264 7 Cuộc gọi hệ thống và quy trình chế độ người dùng

9. Sửa đổi chương trình ví dụ C7.5 để hỗ trợ kích thước hình ảnh Umode lớn, ví dụ 4 MB.

10. Trong chương trình ví dụ C7.5, giả sử rằng kích thước hình ảnh Umode không phải là bội số của 1 MB, ví dụ: 1,5 MB.

Hiển thị cách thiết lập bảng trang quy trình cho phù hợp với kích thước hình ảnh mới.

11. Sửa đổi chương trình ví dụ C7.10 để sử dụng phân trang tĩnh 2 cấp.

12. Sửa đổi chương trình ví dụ C7.10 để ánh xạ không gian VA kernel thành [2 GB, 2 GB+512 MB].

13. Sửa đổi chương trình ví dụ C7.11 để sử dụng phân trang động hai cấp độ.

Người giới thiệu

ARM MMU: ARM926EJ-S, Hướng dẫn tham khảo kỹ thuật ARM946E-S, Trung tâm thông tin ARM 2008 Buttlar, D,
Farrell, J, Nichols, B., "Lập trình PThreads, Tiêu chuẩn POSIX để xử lý đa nhiệm tốt hơn", O'Reilly Media, 1996 Card , R.,
Theodore Ts'o, T., Stephen Tweedie, S., "Thiết kế và triển khai Hệ thống tệp mở rộng thứ hai", web.mit. edu/tytso/www/linux/ext2intro.html,
1995
Cao, M., Bhattacharya, S, Tso, T., "Ext4: Thế hệ tiếp theo của hệ thống tệp Ext2/3", Trung tâm Công nghệ Linux của IBM, 2007.
ELF: Đặc tả Định dạng liên kết và thực thi tiêu chuẩn giao diện công cụ (TIS) Phiên bản 1.2, 1995 EXT2:
www.nongnu.org/ext2-doc/ext2.html, 2001 Sổ tay
hướng dẫn dành cho nhà phát triển phần mềm kiến trúc Intel 64 và IA-32, Tập 3,
1992 Sổ tay tham khảo dành cho lập trình viên bộ xử lý
Intel i486, 1990 trang Linux Man: https://www.kernel.org/doc/
man-pages, Pthread 2016: https://computing.llnl.gov/
tutorials/pthreads, 2015 POSIX.1C, Phần mở rộng luồng,
IEEE Std 1003.1c, 1995 Silberschatz, A., PA Galvin, PA, Gagne, G, "Khái niệm hệ điều hành, Phiên bản thứ 8", John Wiley
& Sons, Inc. 2009 UBOOT, Das U- KHỞI ĐỘNG, http://www.denx.de/wiki/U-
BootUboot, 2016 Wang, KC, "Thiết kế và triển khai hệ điều hành MTX", Springer Publishing International AG, 2015
Machine Translated by Google

Mục đích chung Hoạt động nhúng số 8


Hệ thống

8.1 Hệ điều hành có mục đích chung

Hệ điều hành mục đích chung (GPOS) là một hệ điều hành hoàn chỉnh hỗ trợ quản lý quy trình, quản lý bộ nhớ, thiết bị I/O, hệ thống
tệp và giao diện người dùng. Trong GPOS, các quy trình được tạo động để thực hiện các lệnh của người dùng. Để bảo mật, mỗi quy
trình chạy trong một không gian địa chỉ riêng, tách biệt với các quy trình khác và được bảo vệ bởi phần cứng quản lý bộ nhớ. Khi
một tiến trình hoàn thành một nhiệm vụ cụ thể, nó sẽ kết thúc và giải phóng tất cả tài nguyên của nó cho hệ thống để tái sử dụng.
GPOS phải hỗ trợ nhiều loại thiết bị I/O, chẳng hạn như bàn phím và màn hình cho giao diện người dùng cũng như các thiết bị lưu
trữ dung lượng lớn. GPOS phải hỗ trợ hệ thống tệp để lưu và truy xuất cả chương trình thực thi và dữ liệu ứng dụng. Nó cũng
phải cung cấp giao diện người dùng để người dùng truy cập và sử dụng hệ thống một cách thuận tiện.

8.2 Hệ điều hành có mục đích chung nhúng

Trong những ngày đầu, các hệ thống nhúng tương đối đơn giản. Một hệ thống nhúng thường bao gồm một bộ vi điều khiển, được sử
dụng để giám sát một số cảm biến và tạo tín hiệu để điều khiển một số bộ truyền động, chẳng hạn như bật đèn LED hoặc kích hoạt
rơle để điều khiển các thiết bị bên ngoài. Vì lý do này, chương trình điều khiển của các hệ thống nhúng đời đầu cũng rất đơn
giản. Chúng được viết dưới dạng cấu trúc chương trình siêu vòng lặp hoặc hướng sự kiện. Tuy nhiên, khi sức mạnh tính toán và
nhu cầu về các hệ thống đa chức năng tăng lên, các hệ thống nhúng đã trải qua một bước nhảy vọt vượt bậc cả về ứng dụng lẫn độ
phức tạp. Để đáp ứng nhu cầu ngày càng tăng về các chức năng bổ sung và sự phức tạp của hệ thống, các phương pháp tiếp cận truyền
thống đối với thiết kế hệ điều hành nhúng không còn phù hợp nữa. Các hệ thống nhúng hiện đại cần phần mềm mạnh hơn. Hiện nay,
nhiều thiết bị di động trên thực tế là những cỗ máy tính toán có công suất cao, có khả năng chạy các hệ điều hành chính thức. Một
ví dụ điển hình là điện thoại thông minh sử dụng lõi ARM với bộ nhớ trong gig byte và thẻ micro SD nhiều gig byte để lưu trữ và
chạy các phiên bản Linux thích ứng, chẳng hạn như (Android 2016). Xu hướng thiết kế hệ điều hành nhúng hiện nay rõ ràng đang
chuyển động theo hướng phát triển hệ điều hành đa chức năng phù hợp với môi trường di động. Trong chương này, chúng ta sẽ thảo
luận về việc thiết kế và triển khai các hệ điều hành có mục đích chung cho các hệ thống nhúng.

8.3 Chuyển GPOS hiện có sang hệ thống nhúng

Thay vì thiết kế và triển khai GPOS cho các hệ thống nhúng từ đầu, một cách tiếp cận phổ biến đối với GPOS nhúng là chuyển hệ điều
hành hiện có sang các hệ thống nhúng. Ví dụ về phương pháp này bao gồm chuyển Linux, FreeBSD, NetBSD và Windows sang các hệ thống
nhúng. Trong số này, việc chuyển Linux sang các hệ thống nhúng đặc biệt là một cách làm phổ biến. Ví dụ: Android (2016) là hệ điều
hành dựa trên nhân Linux. Nó được thiết kế chủ yếu cho các thiết bị di động màn hình cảm ứng, chẳng hạn như điện thoại thông
minh và máy tính bảng. Máy tính bo mạch đơn Raspberry PI dựa trên ARM chạy phiên bản Debian Linux được điều chỉnh, được gọi là
Raspbian (Raspberry PI-2 2016). Tương tự, cũng có những tác phẩm được công bố rộng rãi chuyển FreeBSD (2016) và NetBSD (Sevy
2016) sang các hệ thống dựa trên ARM.
Khi chuyển GPOS sang hệ thống nhúng, có hai loại chuyển. Loại đầu tiên có thể được phân loại là chuyển theo hướng thủ tục.
Trong trường hợp này, nhân GPOS đã được điều chỉnh phù hợp với nền tảng dự định, chẳng hạn như dựa trên ARM

© Springer International Publishing AG 2017 265


KC Wang, Hệ điều hành nhúng và thời gian thực, DOI
10.1007/978-3-319-51517-5_8
Machine Translated by Google

266 8 Hệ điều hành nhúng có mục đích chung

hệ thống. Công việc chuyển mạng chủ yếu liên quan đến cách cấu hình các tệp tiêu đề (tệp .h) và các thư mục trong cây mã
nguồn của GPOS gốc, sao cho nó sẽ biên dịch liên kết tới một nhân mới cho kiến trúc máy đích. Trên thực tế, hầu hết công
việc được báo cáo về việc chuyển Linux sang các hệ thống dựa trên ARM đều thuộc loại này. Loại chuyển thứ hai là điều chỉnh
GPOS được thiết kế cho một kiến trúc cụ thể, ví dụ như Intel x86, sang một kiến trúc khác, chẳng hạn như ARM. Trong trường
hợp này, công việc chuyển thường yêu cầu thiết kế lại và trong nhiều trường hợp, triển khai hoàn toàn khác các thành phần
chính trong nhân hệ điều hành gốc để phù hợp với kiến trúc mới. Rõ ràng, loại chuyển thứ hai khó hơn và đầy thách thức hơn
nhiều so với chuyển theo hướng thủ tục vì nó đòi hỏi kiến thức chi tiết về những khác biệt về kiến trúc cũng như sự hiểu
biết đầy đủ về nội bộ hệ điều hành. Trong cuốn sách này, chúng ta sẽ không xem xét việc chuyển mạng theo hướng thủ tục.
Thay vào đó, chúng tôi sẽ trình bày cách phát triển GPOS nhúng cho kiến trúc ARM ngay từ đầu.

8.4 Phát triển GPOS nhúng cho ARM

PMTX (Wang 2015) là một GPOS nhỏ giống Unix ban đầu được thiết kế cho các PC chạy Intel x86. Nó chạy trên các PC đơn bộ xử
lý ở chế độ được bảo vệ 32 bit bằng cách sử dụng phân trang động. Nó hỗ trợ quản lý quy trình, quản lý bộ nhớ, trình điều
khiển thiết bị, hệ thống tệp EXT2 tương thích với Linux và giao diện người dùng dựa trên dòng lệnh. Hầu hết các bộ xử lý
ARM chỉ có một lõi. Trong chương này, chúng ta sẽ tập trung vào cách điều chỉnh PMTX cho các hệ thống dựa trên ARM CPU đơn.
CPU đa lõi và hệ thống đa bộ xử lý sẽ được đề cập sau trong Chương. 9. Để dễ tham khảo, chúng tôi sẽ ký hiệu hệ thống kết
quả là EOS, dành cho Hệ điều hành nhúng.

8.5 Tổ chức của EOS

8.5.1 Nền tảng phần cứng

EOS có thể chạy trên mọi hệ thống dựa trên ARM hỗ trợ các thiết bị I/O phù hợp. Vì hầu hết người đọc có thể không có hệ
thống phần cứng dựa trên ARM thực sự nên chúng tôi sẽ sử dụng ARM Versatilepb VM (ARM Versatilepb 2016) mô phỏng trong QEMU
làm nền tảng để triển khai và thử nghiệm. Máy ảo Versatilepb được mô phỏng hỗ trợ các thiết bị I/O sau.

(1). SDC: EOS sử dụng SDC làm thiết bị lưu trữ dung lượng lớn chính. SDC là một đĩa ảo, được tạo như sau.

dd if=/dev/zero of=$1 bs=4096 count=33280 # 512+32768 Khối 4KB # tạo phân vùng 1 = [2048 đến

đĩa fdisk 266239] cung

losttup -o $(expr 2048 \* 512) --sizelimit $(expr 266239 \* 512) \

/dev/loop1 $1

mke2fs -b 4096 /dev/loop1 32768 # mke2fs với khối 32K 4KB mount /dev/loop1 /mnt (cd /mnt;

mkdir bin boot dev v.v. người # gắn kết như thiết bị vòng lặp

dùng) # điền DIR

số lượng /mnt

Để đơn giản, SDC ảo chỉ có một phân vùng, bắt đầu từ khu vực (mặc định fdisk) 2048. Sau khi tạo SDC ảo, chúng tôi thiết
lập một thiết bị lặp cho phân vùng SDC và định dạng nó dưới dạng hệ thống tệp EXT2 với khối 4 KB kích thước và một nhóm
khối. Nhóm khối đơn trên hình ảnh SDC đơn giản hóa cả quá trình truyền tải hệ thống tệp cũng như các thuật toán quản lý
khối inode và khối đĩa. Sau đó, chúng tôi gắn thiết bị vòng lặp và điền vào đó các DIR và tệp, làm cho thiết bị sẵn sàng để
sử dụng. Kích thước hệ thống tệp kết quả là 128 MB, đủ lớn cho hầu hết các ứng dụng. Đối với các hệ thống tệp lớn hơn, SDC
có thể được tạo bằng nhiều nhóm khối hoặc nhiều phân vùng. Sơ đồ sau đây hiển thị nội dung SDC.

---------|--------Phân vùng 1 --------------------- ---| |M|booter|siêu|gd |. . . |bmap|imap|inodes

|khối dữ liệu | |------------------------------------------------- -------------- |< --------

EXT2 FS --------------- -------- >| |-- bin : tập tin lệnh thực thi nhị phân
Machine Translated by Google

8.5 Tổ chức của EOS 267

|– boot : hình ảnh kernel có thể khởi động

|-- dev : các tập tin đặc biệt (thiết bị I/O)

|-- v.v : tập tin passwd

|-- người dùng: thư mục chính của người dùng

Trên SDC, khu vực MBR (0) chứa bảng phân vùng và phần đầu của bộ khởi động. Phần còn lại của

booter được cài đặt trong các cung 2 thành booter_size, giả sử rằng kích thước booter không quá 2046 cung hoặc 1023 KB (The

kích thước booter thực tế nhỏ hơn 10 KB). Trình khởi động được thiết kế để khởi động hình ảnh hạt nhân từ hệ thống tệp EXT2 trong SDC

vách ngăn. Khi hạt nhân EOS khởi động, nó gắn phân vùng SDC làm hệ thống tệp gốc và chạy trên phân vùng SDC.

(2). LCD: LCD là thiết bị hiển thị chính. Màn hình LCD và bàn phím đóng vai trò là bảng điều khiển hệ thống.

(3). Bàn phím: đây là thiết bị bàn phím của máy ảo Versatilepb. Nó là thiết bị đầu vào cho cả console và UART
thiết bị đầu cuối nối tiếp.

(4). UART: đây là (4) UART của máy ảo Versatilepb. Chúng được sử dụng làm thiết bị đầu cuối nối tiếp để người dùng đăng nhập. Mặc dù nó

rất khó có khả năng một hệ thống nhúng sẽ có nhiều người dùng, mục đích của chúng tôi là chứng tỏ rằng hệ thống EOS có khả năng

hỗ trợ nhiều người dùng cùng một lúc.

(5). Bộ hẹn giờ: VersatilepbVM có bốn bộ hẹn giờ. EOS sử dụng bộ đếm thời gian0 để cung cấp cơ sở thời gian cho việc lập kế hoạch quy trình, dịch vụ hẹn giờ

cũng như các sự kiện về thời gian chung, chẳng hạn như để duy trì Thời gian trong ngày (TOD) ở dạng đồng hồ treo tường.

8.5.2 Cây tệp nguồn EOS

Các tệp nguồn của EOS được tổ chức dưới dạng cây tệp.

EOS

|– BOOTER |– : bộ khởi động giai đoạn 1 và giai đoạn 2

type.h, include.h, mk script

|– kernel : tập tin nguồn hạt nhân

|– fs : tập tin hệ thống tập tin

|– driver : tập tin trình điều khiển thiết bị

|– NGƯỜI DÙNG : lệnh và chương trình chế độ người dùng

BOOTER : thư mục này chứa mã nguồn của booter giai đoạn 1 và giai đoạn 2

type.h : Các kiểu cấu trúc dữ liệu hạt nhân EOS, các tham số hệ thống và hằng số

include.h : hằng số và nguyên mẫu hàm

mk : sh script để biên dịch lại EOS và cài đặt hình ảnh có thể khởi động vào phân vùng SDC

8.5.3 Tệp hạt nhân EOS

———————————— Kernel: Phần quản lý quy trình ————————————————

gõ.h : các kiểu cấu trúc dữ liệu hạt nhân, chẳng hạn như PROC, tài nguyên, v.v.

ts.s : đặt lại trình xử lý, tswitch, mặt nạ ngắt, mã vào/ra trình xử lý ngắt, v.v.

eoslib : các hàm thư viện hạt nhân; hoạt động bộ nhớ và chuỗi.

————————————— Tập tin hạt nhân —————————————————————————————

hàng đợi.c : các hàm thao tác enqueue, dequue, printQueue và list

chờ đã.c : chức năng ksleep, kwakeup, kwait, kexit

bộ nạp.c : Trình tải tập tin hình ảnh thực thi ELF

mem.c : bảng trang và chức năng quản lý khung trang

ngã ba.c : chức năng kfork, fork, vfork

thực thi.c : hàm kexec


Machine Translated by Google

268 8 Hệ điều hành nhúng có mục đích chung

chủ đề.c : chủ đề và chức năng mutex

tín hiệu.c : tín hiệu và xử lý tín hiệu

ngoại trừ.c : các trình xử lý ngoại lệ data_abort, prefetch_abort và undef

ống.c : tạo đường ống và chức năng đọc/ghi đường ống

mes.c : chức năng gửi/recv tin nhắn

syscall.c : các hàm gọi hệ thống đơn giản

svc.c : bảng định tuyến tòa nhà

kernel.c : khởi tạo kernel

tc : mục nhập chính, khởi tạo, các phần của bộ lập lịch quy trình

-------------- Trình điều khiển thiết bị --------------------------------

lcd.c : trình điều khiển hiển thị bảng điều khiển

pv.c : hoạt động ngữ nghĩa

hẹn giờ.c : hẹn giờ và chức năng dịch vụ hẹn giờ

kbd.c : trình điều khiển bàn phím console

uart.c : Trình điều khiển cổng nối tiếp UART

sd.c : Trình điều khiển SDC

-------------- Hệ thống tập tin ----------------------------------

fs : triển khai hệ thống tệp EXT2

đệm.c : chặn bộ đệm I/O của thiết bị (SDC)


————————————————————————————————————————————————————— —————————————————

EOS được triển khai chủ yếu bằng C, với ít hơn 2% mã hợp ngữ. Tổng số dòng trong hạt nhân EOS là

khoảng 14.000.

8.5.4 Khả năng của EOS

Hạt nhân EOS bao gồm quản lý quy trình, quản lý bộ nhớ, trình điều khiển thiết bị và hệ thống tệp hoàn chỉnh. Nó

hỗ trợ việc tạo và chấm dứt quá trình động. Nó cho phép tiến trình thay đổi các hình ảnh thực thi để thực thi các

các chương trình. Mỗi quy trình chạy trong không gian địa chỉ ảo riêng tư ở chế độ Người dùng. Quản lý bộ nhớ theo hai cấp độ động

phân trang. Lập kế hoạch quy trình theo mức độ ưu tiên của cả quy trình theo thời gian và quy trình động. Nó hỗ trợ một hệ thống tập tin EXT2 hoàn chỉnh

hoàn toàn tương thích với Linux. Nó sử dụng bộ đệm I/O thiết bị khối giữa hệ thống tệp và trình điều khiển SDC để cải thiện

hiệu quả và hiệu suất. Nó hỗ trợ nhiều người dùng đăng nhập từ bảng điều khiển và thiết bị đầu cuối nối tiếp. Giao diện người dùng sh

hỗ trợ thực thi các lệnh đơn giản với chuyển hướng I/O, cũng như nhiều lệnh được kết nối bằng đường ống. Nó thống nhất

xử lý ngoại lệ bằng xử lý tín hiệu và cho phép người dùng cài đặt bộ thu tín hiệu để xử lý các ngoại lệ trong chế độ Người dùng.

8.5.5 Trình tự khởi động của EOS

Trình tự khởi động của EOS như sau. Đầu tiên, chúng tôi liệt kê thứ tự logic của trình tự khởi động. Sau đó chúng tôi giải thích từng bước
chi tiết.

(1). Khởi động hạt nhân EOS

(2). Thực thi reset_handler để khởi tạo hệ thống

(3). Định cấu hình các ngắt có vectơ và trình điều khiển thiết bị

(4). kernel_init: khởi tạo cấu trúc dữ liệu kernel, tạo và chạy tiến trình ban đầu P0

(5). Xây dựng pgdir và pgtables cho các tiến trình sử dụng phân trang động hai cấp

(6). Khởi tạo hệ thống tập tin và gắn kết hệ thống tập tin gốc

(7). Tạo quy trình INIT P1; chuyển tiến trình sang chạy P1

(số 8). P1 phân nhánh các quy trình đăng nhập trên bảng điều khiển và thiết bị đầu cuối nối tiếp để cho phép người dùng đăng nhập.

(9). Khi người dùng đăng nhập, quá trình đăng nhập sẽ thực thi trình thông dịch lệnh sh.

(10). Người dùng nhập lệnh để sh thực thi.

(11). Khi người dùng đăng xuất, quy trình INIT sẽ tạo ra một quy trình đăng nhập khác trên thiết bị đầu cuối.
Machine Translated by Google

8.5 Tổ chức của EOS 269

(1). Khởi động SDC: Hệ thống phần cứng dựa trên ARM thường có bộ tải khởi động tích hợp được triển khai trong phần sụn. Khi một

Hệ thống dựa trên ARM khởi động, bộ tải khởi động tích hợp sẽ tải và thực thi bộ khởi động giai đoạn 1 dưới dạng thiết bị flash hoặc, trong nhiều trường hợp.

trường hợp, phân vùng FAT trên SDC. Bộ khởi động giai đoạn 1 tải ảnh hạt nhân và chuyển quyền điều khiển sang ảnh hạt nhân. Dành cho EOS

trên máy ảo ARM Versatilpb, trình tự khởi động cũng tương tự. Đầu tiên, chúng tôi phát triển trình khởi động giai đoạn 1 dưới dạng một chương trình độc lập.

Sau đó, chúng tôi thiết kế bộ khởi động giai đoạn 2 để khởi động hình ảnh hạt nhân EOS từ phân vùng EXT2. Trên SDC, phân vùng 1 bắt đầu từ

khu vực 2048. 2046 khu vực đầu tiên là miễn phí, không được hệ thống tệp sử dụng. Kích thước khởi động giai đoạn 2 nhỏ hơn

10 KB. Nó được cài đặt trong các khu vực từ 2 đến 20 của SDC. Khi máy ảo ARM Versatilepb khởi động, QEMU sẽ tải bộ khởi động giai đoạn 1

thành 0x10000 (64 KB) và thực thi nó trước. Bộ khởi động giai đoạn 1 tải bộ khởi động giai đoạn 2 từ SDC lên 2 MB và chuyển

kiểm soát nó. Trình khởi động giai đoạn 2 tải tệp hình ảnh hạt nhân EOS (/boot/kernel) lên 1 MB và nhảy tới 1 MB để thực thi

mã khởi động của kernel. Trong quá trình khởi động, cả bộ khởi động giai đoạn 1 và giai đoạn 2 đều sử dụng cổng UART cho giao diện người dùng và SDC đơn giản

trình điều khiển để tải các khối SDC. Để giữ cho bộ khởi động đơn giản, cả trình điều khiển UART và SDC đều sử dụng tính năng bỏ phiếu cho I/O. Các

Người đọc có thể tham khảo mã nguồn trong thư mục booter1 và booter2 để biết chi tiết. Nó cũng chỉ ra cách cài đặt giai đoạn 2

bộ khởi động tới SDC.

8.5.6 Quản lý quy trình trong EOS

Trong hạt nhân EOS, mỗi tiến trình hoặc luồng được biểu diễn bằng cấu trúc PROC bao gồm ba phần.

. lĩnh vực quản lý quy trình

. con trỏ tới cấu trúc tài nguyên trên mỗi quy trình

. Con trỏ ngăn xếp chế độ kernel tới trang 4KB được phân bổ động dưới dạng kstack

8.5.6.1 PROC và cấu trúc tài nguyên

Trong EOS, cấu trúc PROC được định nghĩa là

quy trình cấu trúc typedef{

struct proc *next; // con trỏ proc tiếp theo

int *ksp; // lúc 4 giờ

int *đồng ý; // lúc 8 giờ: Umode usp tại syscall

int *upc; // lúc 12: upc tại syscall

int *ucpsr; // lúc 16: Umode cpsr tại syscall

int trạng thái; // trạng thái đang diễn ra

int sự ưu tiên; // lập lịch ưu tiên

int pid; // xử lý ID

int ppid; // ID tiến trình cha

int sự kiện; // sự kiện ngủ

int mã thoát; // mã thoát

int chia đôi; // liệu quy trình có bị VFROKED hay không

int thời gian; // lát cắt thời gian

int CPU; // Số tích tắc thời gian CPU được sử dụng trong MỘT giây

int kiểu; // QUY TRÌNH hoặc CHUYỆN

int tạm ngừng; // giây để tạm dừng

struct proc *parent; // con trỏ PROC cha

struct proc *proc; // xử lý ptr của các luồng trong PROC

struct pres *res; cấu trúc // con trỏ tài nguyên trên mỗi tiến trình

semaphore *sem; // ptr tới quá trình semaphore Bị CHẶN trên

int *kstack; // con trỏ tới ngăn xếp Kmode

}THỦ TỤC;
Machine Translated by Google

270 8 Hệ điều hành nhúng có mục đích chung

Trong cấu trúc PROC, trường tiếp theo được sử dụng để liên kết các PROC trong các danh sách liên kết hoặc hàng đợi khác nhau. Trường ksp đã được lưu

con trỏ ngăn xếp chế độ kernel của tiến trình. Khi một tiến trình giải phóng CPU, nó sẽ lưu các thanh ghi CPU vào kstack và lưu ngăn xếp

con trỏ trong ksp. Khi một tiến trình lấy lại được CPU, nó sẽ tiếp tục chạy từ khung ngăn xếp được trỏ bởi ksp

Các trường usp, upc và ucpsr dùng để lưu Umode sp, pc và cpsr trong quá trình xử lý ngắt syscall và IRQ. Cái này

là do bộ xử lý ARM không tự động xếp chồng Umode sp và cpsr trong SWI (cuộc gọi hệ thống) và IRQ

(ngắt) ngoại lệ. Vì cả lệnh gọi và ngắt hệ thống đều có thể kích hoạt chuyển đổi quy trình, nên chúng tôi phải lưu quy trình Umode

ngữ cảnh theo cách thủ công. Ngoài các thanh ghi CPU được lưu trong ngăn xếp SVC hoặc IRQ, chúng tôi cũng lưu Umode sp và cpsr trong

cấu trúc PROC Các trường pid, ppid, mức độ ưu tiên và trạng thái là hiển nhiên. Trong hầu hết các hệ điều hành lớn, mỗi tiến trình được gán một

pid từ một loạt các số pid. Trong EOS, chúng tôi chỉ cần sử dụng chỉ mục PROC làm pid quy trình, giúp đơn giản hóa kernel

mã và cũng làm cho việc thảo luận trở nên dễ dàng hơn. Khi một tiến trình kết thúc, nó phải đánh thức tiến trình cha nếu tiến trình sau bị

chờ đợi đứa trẻ kết thúc. Trong cấu trúc PROC, con trỏ cha trỏ tới PROC cha. Điều này cho phép người sắp chết

quá trình tìm cha mẹ của nó một cách nhanh chóng. Trường sự kiện là giá trị sự kiện khi một tiến trình chuyển sang chế độ ngủ. Trường exitValue là

trạng thái thoát của một tiến trình. Nếu một quá trình kết thúc bình thường bằng một lệnh gọi tòa nhà exit(value), thì byte thấp của exitValue là giá trị thoát.

Nếu nó kết thúc bất thường bởi một tín hiệu thì byte cao là số tín hiệu. Điều này cho phép tiến trình cha trích xuất lối ra

trạng thái của một ZOMBIE con để xác định xem nó kết thúc bình thường hay bất thường. Trường thời gian là tối đa

khoảng thời gian của một tiến trình và cpu là thời gian sử dụng CPU của nó. Khoảng thời gian xác định một tiến trình có thể chạy trong bao lâu và CPU

thời gian sử dụng được sử dụng để tính toán mức độ ưu tiên lập kế hoạch quy trình. Trường tạm dừng dành cho một quá trình ngủ trong một số

giây. Trong EOS, PROC tiến trình và luồng giống hệt nhau. Trường loại xác định xem PROC là QUY TRÌNH hay

CHỦ ĐỀ. EOS là một hệ thống bộ xử lý đơn (UP), trong đó mỗi lần chỉ có một tiến trình có thể chạy ở chế độ kernel. Đối với quá trình

đồng bộ hóa, nó sử dụng chế độ ngủ/thức trong quản lý quy trình và triển khai các đường ống, nhưng nó sử dụng các ngữ nghĩa trong thiết bị

trình điều khiển và hệ thống tập tin. Khi một tiến trình bị chặn trên một đèn hiệu, trường sem sẽ trỏ đến đèn hiệu đó. Điều này cho phép

kernel để bỏ chặn một tiến trình khỏi hàng đợi semaphore, nếu cần. Ví dụ: khi một tiến trình chờ đầu vào từ một

cổng nối tiếp, nó sẽ bị chặn trong hàng đợi tín hiệu đầu vào của trình điều khiển cổng nối tiếp. Tín hiệu tắt hoặc phím ngắt sẽ cho phép

quá trình tiếp tục. Con trỏ sem đơn giản hóa thao tác bỏ chặn. Mỗi PROC có một con trỏ res trỏ tới một tài nguyên

cấu trúc, đó là

cấu trúc typedef trước

int uid;

int gid;

u32 đệm, psize; // kích thước hình ảnh tính bằng KB

u32 *pgdir; // cho mỗi con trỏ bảng trang cấp 1

u32 *new_pgdir; // new_pgdir trong quá trình thực thi với kích thước mới

MINODE *cwd; // CWD

ký tự tên[32]; // thực thi tên chương trình

ký tự tty[32]; // đã mở thiết bị đầu cuối /dev/ttyXX

int đếm; // số luồng trong tiến trình

u32 tín hiệu; // 31 tín hiệu=bit 1 đến 31

int sig[NSIG]; // 31 bộ xử lý tín hiệu

OFT *fd[NFD]; // mở bộ mô tả tập tin

struct semaphore mlock; // thông qua

tin nhắn semaphore cấu trúc;

cấu trúc mbuf *mqueue;

} PRES;

Cấu trúc PRES chứa thông tin cụ thể về quy trình. Nó bao gồm uid tiến trình, gid, bảng trang cấp 1 (pgdir) và

kích thước hình ảnh, thư mục làm việc hiện tại, tên tệp đặc biệt của thiết bị đầu cuối, tên chương trình thực thi, tín hiệu và trình xử lý tín hiệu,

hàng đợi tin nhắn và bộ mô tả tệp đã mở, v.v. Trong EOS, cả cấu trúc PROC và PRES đều được phân bổ tĩnh. Nếu muốn,

chúng có thể được xây dựng một cách linh hoạt. Các tiến trình và luồng là các đơn vị thực thi độc lập. Mỗi tiến trình thực hiện trong một

không gian địa chỉ duy nhất. Tất cả các luồng trong một tiến trình đều thực thi trong cùng một không gian địa chỉ của tiến trình. Trong quá trình khởi tạo hệ thống,

mỗi PROCESS PROC được gán một cấu trúc PRES duy nhất được trỏ bởi con trỏ res. Một quy trình cũng là chủ đề chính của

quá trình. Khi tạo một luồng mới, con trỏ proc của nó trỏ đến PROC tiến trình và con trỏ res của nó trỏ đến cùng một luồng

Cấu trúc PRES của quá trình Do đó, tất cả các luồng trong một tiến trình đều chia sẻ cùng một tài nguyên, chẳng hạn như các bộ mô tả tệp đã mở, các tín hiệu

và tin nhắn, v.v. Một số nhân hệ điều hành cho phép các luồng riêng lẻ mở các tệp riêng tư đối với các luồng đó. Trong trường hợp đó, mỗi
Machine Translated by Google

8.5 Tổ chức của EOS 271

Cấu trúc PROC phải có mảng mô tả tệp riêng. Tương tự đối với các tín hiệu và thông báo, v.v. Trong cấu trúc PROC,
kstack là một con trỏ tới ngăn xếp chế độ nhân tiến trình/luồng. Trong EOS, PROC được quản lý như sau.
PROC tiến trình và luồng miễn phí được duy trì trong các danh sách miễn phí riêng biệt để phân bổ và giải phóng.
Trong EOS, là hệ thống UP, chỉ có một hàng đợi sẵn sàng để lập lịch quy trình. Ngăn xếp chế độ lõi của quy trình ban đầu
P0 được phân bổ tĩnh ở mức 8 KB (0x2000). Ngăn xếp chế độ lõi của mọi PROC khác chỉ được phân bổ động khung trang (4
KB) khi cần. Khi một quá trình kết thúc, nó sẽ trở thành ZOMBIE nhưng vẫn giữ cấu trúc PROC, pgdir và kstack, cuối cùng
chúng sẽ được xử lý bởi quy trình gốc trong kwait().

8.5.7 Mã hội của EOS

Tệp ts.s: ts.s là tệp hạt nhân duy nhất trong mã hợp ngữ ARM. Nó bao gồm một số phần riêng biệt về mặt logic. Để dễ thảo
luận và tham khảo, chúng tôi sẽ xác định chúng là ts.s.1 đến ts.s.5. Sau đây, chúng tôi sẽ liệt kê mã ts.s và giải thích
chức năng của các phần khác nhau.

//------------- tập tin ts.s -------------- ---

.chữ

.code 32

.global reset_handler

.vector toàn cầu_bắt đầu, vectơ_end

quy trình .global, quy trình

.global tswitch, bộ lập lịch, đang chạy, goUmode

.global switchPgdir, mkPtable, get_cpsr, get_spsr

.global irq_tswitch, setulr

.global copy_vector, copyistack, irq_handler, vectorInt_init

.global int_on, int_off, khóa, mở khóa

.global get_fault_status, get_fault_addr, get_spsr

8.5.7.1 Trình xử lý đặt lại

// -------------------- ts.s.1. ----------------------------

reset_handler:

// đặt ngăn xếp SVC thành CAO CẤP của proc[0].kstack[]

ldr r0, =proc // r0 trỏ tới Proc's

ldr r1, =procsize // r1 -> procsize

ldr r2,[r1, #0] // r2 = xử lý

thêm r0, r0, r2 // r0 -> cao cấp của Proc[0]

phụ r0, #4 // r0 ->proc[0].kstack

mov r1, #0x2000 // r1 = 8KB

str r1, [r0] // proc[0].kstack ở 8KB

di chuyển sp, r1

chuyển động r4, r0 // r4 là bản sao của r0, trỏ tới đỉnh kstack của PROC0

// chuyển sang chế độ IRQ để đặt ngăn xếp IRQ

msr cpsr, #0xD2 // Chế độ IRQ với ngắt IRQ và FIQ bị tắt

ldr sp, =irq_stack // Vùng 4KB được xác định trong tập lệnh liên kết t.ld

// vào chế độ FIQ để đặt ngăn xếp FIQ

msr cpsr, #0xD1

ldr sp, =fiq_stack // đặt chế độ FIQ sp

// vào chế độ ABT để thiết lập ngăn xếp ABT msr cpsr,

#0xD7

ldr sp, =abt_stack // đặt ngăn xếp chế độ ABT

// vào chế độ UND để thiết lập ngăn xếp UND


Machine Translated by Google

272 8 Hệ điều hành nhúng có mục đích chung

msr cpsr, #0xDB

ldr sp, =und_stack // đặt ngăn xếp chế độ UND

// quay lại chế độ SVC

msr cpsr, #0xD3

// đặt chế độ SVC spsr thành chế độ USER khi bật IRQ

msr spsr, #0x10 // ghi vào chế độ trước spsr

ts.s.1 là reset_handler, bắt đầu thực thi ở chế độ SVC với các ngắt bị tắt và MMU bị tắt. Đầu tiên, nó khởi tạo con trỏ
kstack của proc[0] thành 8 KB (0x2000) và đặt con trỏ ngăn xếp chế độ SVC ở mức cao nhất của proc[0].kstack. Điều này làm cho
kstack của Proc[0] trở thành ngăn xếp thực thi ban đầu. Sau đó, nó khởi tạo con trỏ ngăn xếp của các chế độ đặc quyền khác để
xử lý ngoại lệ. Để chạy các quy trình sau này ở chế độ Người dùng, nó đặt SPSR thành chế độ Người dùng. Sau đó nó tiếp tục
thực thi phần thứ hai của mã hợp ngữ. Trong hệ thống dựa trên ARM thực, ngắt FIQ thường được dành riêng cho sự kiện khẩn cấp,
chẳng hạn như mất điện, có thể được sử dụng để kích hoạt nhân hệ điều hành nhằm lưu thông tin hệ thống vào thiết bị lưu trữ
cố định để phục hồi sau này. Vì hầu hết các máy ảo ARM được mô phỏng không có điều khoản như vậy nên EOS chỉ sử dụng các ngắt
IRQ chứ không sử dụng ngắt FIQ.

8.5.7.2 Bảng trang ban đầu

//---------------- ts.s.2 --------------------------

// sao chép bảng vectơ vào địa chỉ 0 bl copy_vector

// tạo pgdir và pgtable ban đầu ở 16KB // tạo pgdir và pgtable trong

bl mkPtable C

ldr r0, mtable

mcr p15, 0, r0, c2, c0, 0 // đặt TTBR

mcr p15, 0, r0, c8, c7, 0 // tuôn ra TLB

// set DOMAIN 0,1 : 01=chế độ CLIENT(kiểm tra quyền)

chuyển r0, #0x5 // b0101 cho KHÁCH HÀNG

mcr p15, 0, r0, c3, c0, 0

// kích hoạt MMU

mrc p15, 0, r0, c1, c0, 0

hoặc r0, r0, #0x00000001 // đặt bit0

mcr p15, 0, r0, c1, c0, 0 // ghi vào c1

không

không

không

mrc p15, 0, r2, c2, c0

chuyển động r2, r2

// kích hoạt ngắt IRQ, sau đó gọi main() trong C

bà r0, cpsr

bic r0, r0, #0xC0

bà CPSR, r0

BL chính // gọi trực tiếp main() . // main()

B không bao giờ quay lại; trong trường hợp đó, chỉ cần lặp lại ở đây

mtable: .word 0x4000 // pgdir ban đầu ở mức 16KB

ts.s.2: phần thứ hai của mã hợp ngữ thực hiện ba chức năng. Đầu tiên, nó sao chép bảng vectơ sang địa chỉ 0. Sau đó, nó xây
dựng bảng trang một cấp ban đầu để tạo ánh xạ nhận dạng của 258 MB VA thấp tới PA, bao gồm 256 MB RAM cộng với 2 MB không gian
I/O bắt đầu từ 256 MB. Hạt nhân EOS sử dụng sơ đồ ánh xạ bộ nhớ KML, trong đó không gian hạt nhân được ánh xạ tới các địa chỉ
VA thấp. Bảng trang ban đầu được xây dựng ở 0x4000 (16 KB) bởi hàm mkPtable() (trong tệp tc). Nó sẽ là bảng trang của tiến
trình ban đầu P0, chỉ chạy ở chế độ Kernel. Sau khi thiết lập bảng trang ban đầu, nó sẽ cấu hình và kích hoạt MMU để dịch địa
chỉ VA sang PA. Sau đó nó gọi main() để tiếp tục khởi tạo kernel trong C.
Machine Translated by Google

8.5 Tổ chức của EOS 273

8.5.7.3 Nhập và thoát cuộc gọi hệ thống

/******************** ts.3 ************************* *****/

// Điểm vào trình xử lý SVC (SWI)

svc_entry: // tham số syscall nằm trong r0-r3: không chạm vào

stmfd sp!, {r0-r12, lr}

// truy cập đang chạy PROC

ldr r5, = đang chạy // r5 = &đang chạy

ldr r6, [r5, #0] // r6 -> PROC đang chạy

bà r7, spsr // lấy spsr, đó là Umode cpsr

str r7, [r6, #16] // lưu spsr vào đang chạy->ucpsr

// sang chế độ SYS để truy cập Umode usp, upc

bà r7, cpsr // r7 = Chế độ SVC cpsr

di chuyển r8, r7 // lưu một bản sao trong r8

orr r7, r7, #0x1F // r7 = chế độ SYS

msr cpsr, r7 // thay đổi cpsr sang chế độ SYS

// bây giờ ở chế độ SYS, sp và lr giống như chế độ Người dùng

str sp, [r6, #8] // lưu usp vào đang chạy->usp

str lr, [r6, #12] // lưu upc vào đang chạy->upc

// chuyển về chế độ SVC

msr cpsr, r8

// lưu kmode sp vào khi chạy->ksp ở mức thấp nhất là 4;

// được sử dụng trong fork() để sao chép kstack của cha mẹ sang kstack của con

str sp, [r6, #4] // đang chạy->ksp = sp

// kích hoạt ngắt IRQ

bà r7, cpsr

bic r7, r7, #0xC0 // I và F bit=0 bật IRQ,FIQ

msr cpsr, r7

bl svc_handler // gọi trình xử lý SVC trong C

// thay thế r0 đã lưu trên ngăn xếp bằng giá trị trả về từ svc_handler()

thêm sp, sp, #4 stmfd // bật r0 đã lưu ra khỏi ngăn xếp một cách hiệu quả

sp!,{r0} // đẩy r là r0 đã lưu vào Umode

goUmode:

// vô hiệu hóa các ngắt IRQ

bà r7, cpsr

orr r7, r7, #0xC0 // Các bit I và F=1 che giấu IRQ,FIQ

msr cpsr, r7 // ghi vào cpsr

bl kpsig // xử lý các tín hiệu nổi bật

bl dời lại // sắp xếp lại quá trình

// truy cập đang chạy PROC

ldr r5, = đang chạy // r5 = &đang chạy

ldr r6, [r5, #0] // r6 -> PROC đang chạy

// xem chế độ SYS để truy cập chế độ người dùng usp

bà r2, cpsr // r2 = Chế độ SVC cpsr

chuyển động r3, r2 // lưu một bản sao vào r3

orr r2, r2, #0x1F // r2 = chế độ SYS

msr cpsr, r2 // chuyển sang chế độ SYS

ldr sp, [r6, #8] // khôi phục usp đang chạy->usp

msr cpsr, r3 // quay lại chế độ SVC

// thay thế pc trong kstack bằng p->upc

mov r3, sp

thêm r3, r3, #52 // offset = 13*4 byte từ sp

ldr r4, [r6, #12]

str r4, [r3]


Machine Translated by Google

274 8 Hệ điều hành nhúng có mục đích chung

// quay lại chạy proc trong Umode

ldmfd sp!, {r0-r12, pc}^

8.5.7.4 Trình xử lý IRQ

// Điểm vào trình xử lý IRQ

irq_handler: // Điểm vào IRQ

phụ lr, lr, #4

stmfd sp!, {r0-r12, lr} // lưu tất cả các reg Umode trong ngăn xếp IRQ

// có thể chuyển đổi nhiệm vụ khi kết thúc quá trình xử lý IRQ; lưu thông tin Umode

bà r0, spsr

và r0, #0x1F

cmp r0, #0x10 // kiểm tra xem có ở Umode không

chúc mừng bạn // không cần lưu bối cảnh Umode nếu KHÔNG ở Umode

// truy cập đang chạy PROC

ldr r5, =đang chạy // r5=&đang chạy

ldr r6, [r5, #0] // r6 -> PROC của quá trình chạy

bà r7, spsr

str r7, [r6, #16]

// sang chế độ SYS để truy cập Umode usp=r13 và cpsr

bà r7, cpsr // r7 = Chế độ SVC cpsr

di chuyển r8, r7 // lưu một bản sao của cpsr vào r8

orr r7, r7, #0x1F // r7 = chế độ SYS

msr cpsr, r7 // thay đổi cpsr sang chế độ SYS

// bây giờ ở chế độ SYS, r13 giống như chế độ Người dùng sp r14=chế độ người dùng lr

str sp, [r6, #8] // lưu usp vào proc.usp ở offset 8

str lr, [r6, #12] // lưu upc vào proc.upc ở offset 12

// chuyển về chế độ IRQ

msr cpsr, r8

noUmode:

bl irq_chandler // gọi irq_handler() trong C ở chế độ SVC

// kiểm tra chế độ

bà r0, spsr

và r0, #0x1F

cmp r0, #0x10 // kiểm tra xem có ở Umode không

bne kiret

// Proc ở Umode khi IRQ ngắt: xử lý tín hiệu, có thể chuyển đổi

bl kpsig

bl dời lại // lên lịch lại: có thể chuyển đổi

// HIỆN TẠI đang chạy PROC trở về Umode

ldr r5, =đang chạy // r5=&đang chạy

ldr r6, [r5, #0] // r6 -> PROC của quá trình chạy

// khôi phục Umode.[sp,pc,cpsr] từ PROC đã lưu.[usp,upc,ucpsr]

ldr r7, [r6, #16] // r7 = Umode cpsr đã lưu

// khôi phục spsr về Umode cpsr đã lưu

msr spsr, r7

// vào chế độ SYS để truy cập chế độ người dùng sp

bà r7, cpsr // r7 = Chế độ SVC cpsr

di chuyển r8, r7 // lưu một bản sao của cpsr vào r8

orr r7, r7, #0x1F // r7 = chế độ SYS

msr cpsr, r7 // thay đổi cpsr sang chế độ SYS


Machine Translated by Google

8.5 Tổ chức của EOS 275

// bây giờ ở chế độ SYS; khôi phục Umode usp

ldr sp, [r6, #8] // đặt usp trong Umode = Running->usp

// quay lại chế độ IRQ

msr cpsr, r8 // quay lại chế độ IRQ

kiret:

ldmfd sp!, {r0-r12, pc}^ // quay lại Umode

ts.s.3: Phần thứ ba của mã hợp ngữ chứa các điểm vào của trình xử lý ngoại lệ SWI (SVC) và IRQ. Cả hai trình xử lý SVC và IRQ đều khá độc đáo

do các chế độ hoạt động khác nhau của kiến trúc bộ xử lý ARM. Vì vậy chúng tôi sẽ giải thích chúng chi tiết hơn.

Mục nhập cuộc gọi hệ thống: svc_entry là điểm vào của trình xử lý ngoại lệ SWI, được sử dụng cho các cuộc gọi hệ thống tới hạt nhân EOS. Khi

vào, trước tiên, nó sẽ lưu bối cảnh quy trình (Umode) trong ngăn xếp quy trình Kmode (chế độ SVC). Các tham số lệnh gọi hệ thống (a,b,c,d) được

chuyển vào các thanh ghi r0–r3, không được thay đổi. Vì vậy mã chỉ sử dụng các thanh ghi r4–r10. Đầu tiên, nó cho phép r6 trỏ đến cấu trúc PROC

của tiến trình. Sau đó, nó lưu spsr hiện tại, tức là cpsr Umode, vào PROC.ucpsr. Sau đó nó chuyển sang chế độ SYS để truy cập vào các thanh ghi

Umode. Nó lưu Umode sp và pc vào PROC.usp và PROC.upc tương ứng. Do đó, trong quá trình gọi hệ thống, bối cảnh Umode của tiến trình được lưu

như sau.

Thanh ghi Umode r0½ r12; r14 được lưu trong PROC:kstack

Umode sp ½ ; máy tính; cpsr được lưu trong PROC:½usp; upc; ucpsr

Ngoài ra, nó cũng lưu Kmode sp trong PROC.ksp, được sử dụng để sao chép kstack gốc sang kstack con trong fork(). Sau đó, nó cho phép ngắt

IRQ và gọi svc_chandler() để xử lý lệnh gọi hệ thống. Mỗi syscall (ngoại trừ kexit) trả về một giá trị, giá trị này thay thế r0 đã lưu trong

kstack làm giá trị trả về Umode.

Thoát cuộc gọi hệ thống: goUmode là mã thoát cuộc gọi hệ thống. Nó cho phép quy trình đang chạy hiện tại, có thể hoặc không phải là quy trình

ban đầu đã thực hiện lệnh gọi hệ thống, quay trở lại Umode. Đầu tiên, nó vô hiệu hóa các ngắt IRQ để đảm bảo rằng toàn bộ mã goUmode được thực

thi trong phần quan trọng. Sau đó, nó cho phép tiến trình đang chạy hiện tại kiểm tra và xử lý mọi tín hiệu nổi bật. Việc xử lý tín hiệu trong

kiến trúc ARM cũng khá độc đáo, điều này sẽ được giải thích sau. Nếu quy trình vẫn tồn tại tín hiệu, nó sẽ gọi reschedule() để lên lịch lại các

quy trình, quy trình này có thể chuyển đổi quy trình nếu sw_flag được đặt, nghĩa là có các quy trình trong ReadyQueue có mức độ ưu tiên cao

hơn. Sau đó tiến trình đang chạy sẽ khôi phục [usp, upc, cpsr] từ cấu trúc PROC của nó và trở về Umode bằng cách

ldmfd sp!; fr0r12; pcg^

Khi quay lại Umode, r0 chứa giá trị trả về của syscall.

Mục nhập IRQ: irq_handler là điểm vào của các ngắt IRQ. Không giống như các cuộc gọi tổng hợp chỉ có thể bắt nguồn từ Umode, các ngắt IRQ có

thể xảy ra ở Umode hoặc Kmode. EOS là một hệ điều hành đơn bộ xử lý. Hạt nhân EOS không có tính ưu tiên, có nghĩa là nó không chuyển đổi tiến

trình khi ở chế độ hạt nhân. Tuy nhiên, nó có thể chuyển đổi quy trình nếu quy trình bị gián đoạn đang chạy trong Umode. Điều này là cần thiết

để hỗ trợ lập kế hoạch quy trình theo mức độ ưu tiên động và thời gian. Chuyển đổi tác vụ trong chế độ ARM IRQ đặt ra một vấn đề đặc biệt mà

chúng tôi sẽ giải thích ngay sau đây. Khi truy cập irq_handler, trước tiên nó sẽ lưu bối cảnh của quá trình bị gián đoạn trong ngăn xếp chế độ

IRQ. Sau đó nó kiểm tra xem ngắt có xảy ra trong Umode hay không.

Nếu vậy, nó cũng lưu Umode [usp, upc, cpsr] vào cấu trúc PROC. Sau đó nó gọi irq_chandler() để xử lý ngắt.

Trình xử lý ngắt hẹn giờ có thể đặt cờ quy trình chuyển đổi, sw_flag, nếu phần thời gian của quy trình đang chạy hiện tại đã hết hạn. Tương tự,

trình xử lý ngắt thiết bị cũng có thể đặt sw_flag nếu nó đánh thức hoặc bỏ chặn các tiến trình có mức độ ưu tiên cao hơn. Khi kết thúc quá

trình xử lý ngắt, nếu ngắt xảy ra trong Kmode hoặc sw_flag bị tắt thì sẽ không có chuyển đổi tác vụ, do đó quá trình sẽ trở lại bình thường về

điểm gián đoạn ban đầu. Tuy nhiên, nếu xảy ra gián đoạn trong Umode và sw_flag được đặt, kernel sẽ chuyển tiến trình để chạy tiến trình có mức

ưu tiên cao nhất.

8.5.7.5 IRQ và ưu tiên quy trình Không giống như

các tòa nhà cao tầng, vốn luôn sử dụng kstack quy trình trong chế độ SVC, việc chuyển đổi tác vụ trong chế độ IRQ rất phức tạp bởi thực tế là,

trong khi xử lý ngắt sử dụng ngăn xếp IRQ, việc chuyển đổi tác vụ phải được thực hiện ở chế độ SVC, điều này sử dụng quá trình kstack. Trong

trường hợp này, chúng ta phải thực hiện các thao tác sau một cách thủ công.
Machine Translated by Google

276 8 Hệ điều hành nhúng có mục đích chung

(1). Chuyển khung ngăn xếp INTERRUPT từ ngăn xếp IRQ sang kstack xử lý (SVC);

(2). Làm phẳng ngăn xếp IRQ để tránh bị tràn;

(3). Đặt con trỏ ngăn xếp chế độ SVC thành khung ngăn xếp INTERRUPT trong quá trình kstack.

(4). Chuyển sang chế độ SVC và gọi tswitch() để từ bỏ CPU, thao tác này sẽ đẩy khung ngăn xếp TIẾP TỤC lên kstack tiến trình

được trỏ bởi PROC.ksp đã lưu.

(5). Khi quá trình lấy lại CPU, nó sẽ tiếp tục ở chế độ SVC bằng khung ngăn xếp TIẾP TỤC và quay trở lại nơi nó được gọi
tswitch() trước đó.

(6). Khôi phục Umode [usp, cpsr] từ [usp, ucpsr] đã lưu trong cấu trúc PROC

(7). Quay trở lại Umode bằng khung ngăn xếp INTERRUPT trong kstack.

Chuyển đổi tác vụ ở chế độ IRQ được triển khai trong đoạn mã irq_tswitch(), điều này có thể được giải thích tốt nhất bằng cách sau

sơ đồ.

(1) Sao chép khung INTERRUPT từ ngăn xếp IRQ sang ngăn xếp SVC, đặt SVC_sp,

IRQ_stack SVC_stack

-------------------- sao chép ---------------------

|ulr|ur12 - ur0| ===> |ulr|ur12 – ur0|


--------------------
-------------|-------

|-- Khung INT -| SVC_sp

(2). Gọi tswitch() để loại bỏ CPU, đẩy khung TIẾP TỤC lên ngăn xếp SVC và đặt PROC.ksp đã lưu

trỏ đến khung ngăn xếp sơ yếu lý lịch.

|-- Khung INT -|-TIẾP TỤC khung-|


--------------------------------------

SVC_stack =|ulr|ur12 – ur0|klr|kr12 – kr0|

----------------------------|----------

SVC_sp = PROC.ksp

(3). Khi tiến trình được lên lịch chạy, nó sẽ tiếp tục ở chế độ SVC và trở về trạng thái

ở đây: // trở về từ tswitch()

khôi phục Umode.[usp,cpsr] từ PROC.[usp,ucpsr]

ldmfd sp, {r0-r12, pc}^ // quay lại Umode

// ---------------- ts.s.4 --------------------------

chuyển đổi: // tswitch() trong Kmode

bà r0, cpsr // vô hiệu hóa các ngắt

orr r0, r0, #0xC0 // I và F bit=1: che giấu IRQ, FIQ

mrs cpsr, r0 // Ngắt I và F bị vô hiệu hóa

stmfd sp!, {r0-r12, lr} // lưu ngữ cảnh vào kstack

ldr r0, = đang chạy // r0=&running; truy cập đang chạy-> PROC

ldr r1, [r0, #0] // r1->chạy PROC

str sp, [r1, #4] // đang chạy->ksp = sp

lịch trình bl // gọi lịch trình() để chọn lần chạy tiếp theo

ldr r0, = đang chạy // tiếp tục chạy HIỆN TẠI

ldr r1, [r0, #0] // r1->runningPROC

ldr sp, [r1, #4] // sp = đang chạy->ksp

bà r0, cpsr // vô hiệu hóa các ngắt


Machine Translated by Google

8.5 Tổ chức của EOS 277

bic r0, r0, #0xC0 // cho phép ngắt I và F

bà CPSR, r0

ldmfd sp!, {r0-r12, pc}

irq_tswitch: // irq_tswitch: chuyển đổi tác vụ ở chế độ IRQ

mov r0, sp // r0 = dòng điện ở chế độ IRQ sp

bl copyistack // chuyển khung INT từ ngăn xếp IRQ sang ngăn xếp SVC

bà r7, spsr // r7 = IRQ mode spsr, phải là Umode cpsr

// làm phẳng ngăn xếp irq

ldr sp, =irq_stack_top

// chuyển sang chế độ SVC

bà r0, cpsr

bic r1, r0, #0x1F // r1 = r0 = 5 bit thấp nhất của cspr bị xóa về 0

orr r1, r1, #0x13 // HOẶC ở 0x13=10011 = chế độ SVC

msr cpsr, r1 ldr // ghi vào cspr, vì vậy bây giờ ở chế độ SVC

r5, =running // r5 = &running

ldr r6, [r5, #0] // r6 -> PROC đang chạy

// ngăn xếp svc đã có khung irq, đặt SVC sp thành kstack[-14]

ldr sp, [r6, #4] // Chế độ SVC sp= &running->kstack[SSIZE-14]

bl tswitch // chuyển tác vụ ở chế độ SVC

ldr r5, =đang chạy // r5=&đang chạy

ldr r6, [r5, #0] // r6 -> PROC đang chạy

ldr r7, [r6, #16] // r7 = Umode cpsr đã lưu

// khôi phục spsr về Umode cpsr đã lưu

msr spsr, r7

// vào chế độ SYS để truy cập chế độ người dùng sp

bà r7, cpsr // r7 = Chế độ SVC cpsr

di chuyển r8, r7 // lưu một bản sao của cpsr vào r8

orr r7, r7, #0x1F // r7 = chế độ SYS

msr cpsr, r7 // thay đổi cpsr sang chế độ SYS

// bây giờ ở chế độ SYS; khôi phục Umode usp

ldr sp, [r6, #8] ldr lr, // khôi phục usp

[r6, #12] // khôi phục upc; THỰC SỰ cần điều này?

// quay lại chế độ SVC

msr cpsr, r8 // quay lại chế độ IRQ

ldmfd sp!, {r0-r12, pc}^ // trả về qua khung INT trong ngăn xếp SVC

switchPgdir: // chuyển pgdir sang pgdir của PROC mới; đậu vào r0

// r0 chứa địa chỉ pgdir của PROC mới

mcr p15, 0, r0, c2, c0, 0 // đặt TTBase

chuyển r1, #0

mcr p15, 0, r1, c8, c7, 0 // xóa TLB

mcr p15, 0, r1, c7, c10, 0 // xóa bộ đệm

mrc p15, 0, r2, c2, c0, 0

// đặt tên miền: all 01=client(kiểm tra quyền)

chuyển r0, #0x5 //01|01 cho KHÁCH HÀNG|khách hàng


mcr p15, 0, r0, c3, c0, 0

di chuyển máy tính, lr // trở lại

ts.s.4: Phần thứ tư của mã hợp ngữ thực hiện chuyển đổi nhiệm vụ. Nó bao gồm ba chức năng. tswitch() nó cho nhiệm vụ

chuyển đổi ở Kmode, irq_tswitch() dành cho chuyển đổi tác vụ ở chế độ IRQ và switchPgdir() dành cho chuyển đổi quá trình pgdir trong khi thực hiện tác vụ

công tắc. Vì tất cả các chức năng này đã được giải thích trước đó nên chúng tôi sẽ không lặp lại chúng ở đây.

//----------ts.s.5 -----------------

// Chức năng mặt nạ/vạch mặt ngắt IRQ

int_on: // int int_on(int cpsr)

msr cpsr, r0

di chuyển máy tính, lr


Machine Translated by Google

278 8 Hệ điều hành nhúng có mục đích chung

int_off: // int cpsr = int_off();

bà r4, cpsr

chuyển động r0, r4

orr r4, r4, #0x80 // bit set có nghĩa là MASK tắt ngắt IRQ

msr cpsr, r4

di chuyển máy tính, lr

mở khóa: // kích hoạt IRQ trực tiếp

bà r4, cpsr

bic r4, r4, #0x80 // bit xóa có nghĩa là ngắt IRQ UNMASK

msr cpsr, r4

di chuyển máy tính, lr

kho a: // vô hiệu hóa IRQ trực tiếp

bà r4, cpsr

orr r4, r4, #0x80 // bit set có nghĩa là MASK tắt ngắt IRQ

msr cpsr, r4

di chuyển máy tính, lr

get_cpsr:

bà r0, cpsr

di chuyển máy tính, lr

get_spsr:

bà r0, spsr

di chuyển máy tính, lr

setulr:// setulr(oldPC): đặt Umode lr=oldPC cho bộ bắt tín hiệu()

mrs r7, cpsr // sang chế độ SYS

di chuyển r8, r7 // lưu cpsr vào r8

hoặc r7, #0x1F //

msr cpsr, r7

// hiện đang ở chế độ SYS

mov lr, r0 // đặt Umode lr thành oldPC

msr cpsr, r8 // quay lại chế độ ban đầu

di chuyển máy tính, lr // trở lại

vectơ_start:

PC LDR, reset_handler_addr

Máy tính LDR, undef_handler_addr

Máy tính LDR, svc_handler_addr

PC LDR, tìm nạp trước_abort_handler_addr

PC LDR, dữ liệu_abort_handler_addr

B .

Máy tính LDR, irq_handler_addr

PC LDR, fiq_handler_addr

đặt lại_handler_addr: .word reset_handler

undef_handler_addr: .word undef_abort_handler

svc_handler_addr: .word svc_entry

prefetch_abort_handler_addr: .word prefetch_abort_handler

dữ liệu_abort_handler_addr: .word data_abort_handler

irq_handler_addr: .word irq_handler

fiq_handler_addr: .word fiq_handler

vectơ_end:

// kết thúc tập tin ts.s

Phần cuối cùng của mã hợp ngữ thực hiện nhiều chức năng tiện ích khác nhau, chẳng hạn như khóa/mở khóa, int_off/int_on và nhận
Thanh ghi trạng thái CPU, v.v. Lưu ý sự khác biệt giữa khóa/mở khóa và int_off/int_on. Trong khi đó khóa/mở khóa vô hiệu hóa/bật
IRQ ngắt vô điều kiện, int_off vô hiệu hóa các ngắt IRQ nhưng trả về CPSR ban đầu, được khôi phục trong int_on.
Machine Translated by Google

8.5 Tổ chức của EOS 279

Những điều này cần thiết trong trình xử lý ngắt của thiết bị, chạy với các ngắt bị vô hiệu hóa nhưng có thể đưa ra thao tác V trên các ngữ nghĩa để bỏ

chặn các quy trình.

8.5.8 Tệp hạt nhân của EOS

Phần 2. Tệp tc: Tệp tc chứa hàm main(), được gọi từ reset_handler khi hệ thống khởi động.

8.5.8.1 Hàm main()


Hàm main() bao gồm các bước sau

(1). Khởi tạo trình điều khiển màn hình LCD để printf() hoạt động.

(2). Khởi tạo bộ đệm I/O của thiết bị khối cho I/O tệp trên SDC.

(3). Định cấu hình bộ điều khiển và thiết bị ngắt VIC, SIC cho các ngắt có vectơ.

(4). Khởi tạo trình điều khiển thiết bị và bắt đầu hẹn giờ.

(5). Gọi kernel_init() để khởi tạo cấu trúc dữ liệu kernel. Tạo và chạy quy trình ban đầu P0. Xây dựng pgdir và pgtables

cho các quá trình. Xây dựng danh sách khung trang miễn phí cho phân trang động. Chuyển pgdir sang sử dụng phân trang động hai cấp độ.

(6). Gọi fs_init() để khởi tạo hệ thống tệp và gắn kết hệ thống tệp gốc.

(7). Tạo quy trình INIT P1 và tải /bin/init dưới dạng hình ảnh Umode của nó.

(số 8). P0 chuyển tác vụ để chạy tiến trình INIT P1.

P1 phân nhánh các quy trình đăng nhập trên bảng điều khiển và thiết bị đầu cuối nối tiếp để người dùng đăng nhập. Sau đó nó đợi bất kỳ con ZOMBIE nào,

bao gồm các quy trình đăng nhập cũng như bất kỳ quy trình mồ côi nào, ví dụ như trong các đường ống nhiều giai đoạn. Khi quá trình đăng nhập khởi động, hệ thống sẽ

sẵn sàng để sử dụng.

/************************ tập tin tc ************************* ***/

#include "../type.h"

int chính()

fbuf_init(); //khởi tạo bộ đệm khung LCD: driver/vid.c

printf("Chào mừng đến với WANIX in Arm\n");

binit(); // Bộ đệm I/O: fs/buffer.c

vectorInt_init(); // Các ngắt có vectơ: driver/int.c

irq_init(); // Cấu hình VIC,SIC,deviceIRQs: driver/int.c

kbd_init(); // Khởi tạo trình điều khiển KBB: driver/kbd.c

uart_init(); // khởi tạo UART: lái xe/uart.c

bộ đếm thời gian_init(); // khởi tạo bộ đếm thời gian: trình điều khiển/bộ đếm thời gian.c

bộ đếm thời gian_start(0); // bắt đầu hẹn giờ0 trình điều khiển/bộ đếm thời gian.c

sdc_init(); // khởi tạo trình điều khiển SDC: driver/sdc.c

kernel_init(); // khởi tạo cấu trúc kernel:kernel/kernel.c

fs_init(); // khởi tạo FS và gắn kết hệ thống tập tin gốc

kfork("/bin/init"); // tạo INIT Proc P1: kernel/fork.c

printf("P0 chuyển sang P1\n");

trong khi(1){ // mã P0

while(!readyQueue); // vòng lặp nếu không có chương trình nào có thể chạy được

tswitch(); // Chuyển tác vụ P0 nếu hàng đợi sẵn sàng không trống

}
Machine Translated by Google

280 8 Hệ điều hành nhúng có mục đích chung

8.5.8.2 Khởi tạo hạt nhân


Hàm kernel_init(): Hàm kernel_init() bao gồm các bước sau.

(1). Khởi tạo cấu trúc dữ liệu kernel. Chúng bao gồm các danh sách PROC miễn phí, một hàng đợi sẵn sàng để lập lịch quy trình và một Danh sách

ngủ FIFO chứa các quy trình SLEEP.

(2). Tạo và chạy quy trình ban đầu P0, chạy trong Kmode với mức ưu tiên thấp nhất là 0. P0 cũng là quy trình nhàn rỗi, chạy nếu không có quy

trình có thể chạy nào khác, tức là khi tất cả các quy trình khác đang ngủ hoặc bị chặn. Khi P0 tiếp tục hoạt động, nó thực thi một vòng lặp

chờ bận cho đến khi hàng đợi sẵn sàng không trống. Sau đó, nó chuyển quy trình để chạy một quy trình sẵn sàng với mức ưu tiên cao nhất. Thay

vì vòng chờ bận rộn, P0 có thể đặt CPU ở trạng thái WFI tiết kiệm năng lượng với các ngắt được bật.

Sau khi xử lý một ngắt, nó sẽ cố gắng chạy lại một quy trình sẵn sàng, v.v.

(3). Xây dựng một pgdir Kmode ở 32 KB và bảng trang 258 cấp 2 ở 5 MB. Xây dựng các pgdir cấp 1 cho (64) quy trình trong vùng 6 MB và các bảng

trang cấp 2 liên quan của chúng trong vùng 7 MB. Chi tiết về pgdir và bảng trang sẽ được giải thích trong phần tiếp theo về quản lý bộ nhớ.

(4). Chuyển pgdir sang pgdir cấp 1 mới với kích thước 32 KB để sử dụng phân trang 2 cấp.

(5). Xây dựng một pfreeList chứa các khung trang miễn phí từ 8 MB đến 256 MB và triển khai các hàm palloc()/pdealloc() để hỗ trợ phân trang

động.

(6). Khởi tạo đường ống và bộ đệm thông báo trong kernel.

(7). Quay lại main(), gọi fs_init() để khởi tạo hệ thống tệp và gắn kết hệ thống tệp gốc. Sau đó nó tạo và chạy tiến trình INIT P1.

/********************* tập tin kernel.c **********************/

#include "../type.h"

PROC proc[NPROC+NTHREAD];

PRES nhấn [NPROC];

PROC *danh sách tự do, *tfreeList, *readyQueue, *sleepList, *running;;

int sw_flag;

int procsize = sizeof(PROC);

OFT thường [NOFT];

Ống PIPE[NPIPE];

int kernel_init()

int tôi, j;

PROC *p; char *cp;

printf("kernel_init()\n"); for (i=0;

i<NPROC; i++){ // khởi tạo PROC trong freeList p = &proc[i]; p->pid = tôi;

p->trạng thái = MIỄN PHÍ;

p->ưu tiên = 0; p->ppid

= 0; p->res =

&pres[i]; // res trỏ tới pres[i] p->next = p + 1; // umode pgdir và

pagetable của proc[i] có

kích thước 6MB + pid*16KB p->res->pgdir = (int *)(0x600000 + (p->pid-1)*0x4000);

proc[NPROC-1].next = 0;

freeList = &proc[0];

// mã tương tự để khởi tạo tfreeList cho các procs NTHREAD

hàng đợi sẵn sàng = 0;

Danh sách ngủ = 0;

// tạo P0 làm tiến trình chạy ban đầu;

p = đang chạy = get_proc(&freeList);


Machine Translated by Google

8.5 Tổ chức của EOS 281

p->trạng thái = SẴN SÀNG;

p->res->uid = p->res->gid = 0; p->res->tín hiệu

= 0;

p->res->name[0] = 0;

p->thời gian = 10000; p- // tùy ý vì P0 không có giới hạn thời gian

>res->pgdir = (int *)0x8000; // pgdir của P0 có kích thước 32KB for (i=0; i<NFD; i+

+) // xóa mảng mô tả tập tin

p->res->fd[i] = 0;

for (i=0; i<NSIG; i++) // xóa tín hiệu

p->res->sig[i] = 0;

build_ptable(); // trong tệp mem.c

printf("chuyển pgdir sang sử dụng phân trang 2 cấp: ");

switchPgdir(0x8000);

// xây dựng danh sách pfreelist: khung trang miễn phí bắt đầu từ 8MB end = 256MB

pfreeList = free_page_list((int *)0x00800000, (int *)0x10000000);

pipe_init(); // khởi tạo các đường ống trong kernel

mbuf_init(); // khởi tạo bộ đệm tin nhắn trong kernel

8.5.8.3 Chức năng lập kế hoạch quy trình

bộ lập lịch int()

PROC *cũ = đang chạy;

if (running->pid == 0 && đang chạy->status == BLOCK){// chỉ P0

mở khóa();

while(!readyQueue);

trở lại;

if (đang chạy->trạng thái==SẴN SÀNG)

enqueue(&readyQueue, đang chạy);

đang chạy = dequeue(&readyQueue);

if (đang chạy != cũ){

switchPgdir((int)running->res->pgdir);

đang chạy->thời gian = 10; // lát thời gian = 10 tích tắc; // tắt cờ

sw_flag = 0; tác vụ chuyển đổi

lịch trình int(PROC *p)

nếu (p->trạng thái ==SẴN SÀNG)

enqueue(&readyQueue, p); if (p->ưu

tiên > đang chạy->ưu tiên) sw_flag = 1;

int sắp xếp lại()

nếu (sw_flag)

tswitch();

Các hàm còn lại trong tc bao gồm lịch trình(), lịch trình() và lập lịch lại(), là các phần của bộ lập lịch quy
trình trong hạt nhân EOS. Trong hàm lập lịch (), một vài dòng mã đầu tiên chỉ áp dụng cho quy trình ban đầu P0. Khi mà
Machine Translated by Google

282 8 Hệ điều hành nhúng có mục đích chung

hệ thống khởi động, P0 thực thi mount_root() để mount hệ thống tập tin gốc. Nó sử dụng bộ đệm I/O để đọc SDC, điều này khiến P0 chặn

trên bộ đệm I/O cho đến khi thao tác đọc hoàn tất. Vì chưa có tiến trình nào khác nên P0 không thể chuyển tiến trình khi nó bị chặn.

Vì vậy, nó bận rộn chờ đợi cho đến khi bộ xử lý ngắt SDC thực thi V để bỏ chặn nó. Ngoài ra, chúng tôi có thể sửa đổi trình điều khiển

SDC để sử dụng tính năng thăm dò trong quá trình khởi động hệ thống và chuyển sang chế độ điều khiển ngắt sau khi P0 đã tạo P1. Điểm

bất lợi là nó sẽ làm cho trình điều khiển SDC hoạt động kém hiệu quả hơn vì nó phải kiểm tra cờ trên mỗi thao tác đọc.

8.5.9 Chức năng quản lý quy trình

8.5.9.1 fork-exec EOS

hỗ trợ việc tạo quy trình động bằng fork, tạo ra một quy trình con có hình ảnh Umode giống hệt như quy trình gốc.

Nó cho phép quá trình thay đổi hình ảnh bằng cách thực thi. Ngoài ra, nó còn hỗ trợ các thread trong cùng một tiến trình. Chúng được

thực hiện trong các tập tin sau.


tệp fork.c: tệp này chứa fork1(), kfork(), fork() và vfork(). fork1() là mã chung của tất cả các hàm fork khác. Nó tạo ra một Proc

mới với pgdir và pgtables. kfork() chỉ được sử dụng bởi P0 để tạo ra quá trình INIT P1. Nó tải tệp hình ảnh Umode (/bin/init) của P1

và khởi tạo kstack của P1 để sẵn sàng chạy trong Umode. fork() tạo một tiến trình con có hình ảnh Umode giống hệt với tiến trình gốc.

vfork() giống như fork() nhưng không sao chép hình ảnh.

Tệp exec.c: tệp này chứa kexec(), cho phép một quá trình thay đổi hình ảnh Umode thành một tệp thực thi khác và

truyền tham số dòng lệnh cho hình ảnh mới.

Tệp thread.c: tệp này triển khai các luồng trong một tiến trình và đồng bộ hóa các luồng bằng mutexes.

8.5.9.2 thoát-chờ
Hạt nhân EOS sử dụng chế độ ngủ/thức để đồng bộ hóa quy trình trong quản lý quy trình cũng như trong đường ống. Quản lý quy trình

được triển khai trong tệp Wait.c, chứa các chức năng sau.

ksleep(): tiến trình chuyển sang chế độ ngủ khi có sự kiện. PROC đang ngủ được duy trì trong FIFO

sleepList để thức dậy theo thứ tự

kwakeup(): đánh thức tất cả các PROC đang ngủ trong một sự kiện

kexit(): chấm dứt tiến trình trong kernel

kwait(): đợi tiến trình con ZOMBIE, trả về trạng thái pid và thoát của nó

8.5.10 Ống

Hạt nhân EOS hỗ trợ các đường dẫn giữa các quy trình liên quan. Một đường ống là một cấu trúc bao gồm các trường sau.

ống cấu trúc typedef {

char *buf; // bộ đệm dữ liệu: khung trang được cấp phát động;

int đầu, đuôi; // chỉ số bộ đệm

dữ liệu int, phòng; // bộ đếm để đồng bộ hóa

int nreader, nwriter; // số READER, WRITER trên pipe

int bận; // trạng thái của đường ống

}ĐƯỜNG ỐNG;

Tòa nhà int r = pipe(int pd[]);

tạo một đường ống trong kernel và trả về hai bộ mô tả tệp trong pd[2], trong đó pd[0] để đọc từ đường ống và pd[1] để ghi vào đường

ống. Bộ đệm dữ liệu của đường ống là một trang 4 KB được phân bổ động, trang này sẽ được giải phóng khi đường ống được giải phóng.

Sau khi tạo một đường ống, quy trình này thường phân nhánh một quy trình con để chia sẻ đường ống, tức là cả quy trình cha và con đều

có cùng các bộ mô tả đường ống pd[0] và pd[1]. Tuy nhiên, trên cùng một đường ống, mỗi tiến trình phải là READER hoặc WRITER, nhưng

không được cả hai. Vì vậy, một trong các quy trình được chọn làm BỘ VIẾT ống và quy trình còn lại làm ĐỌC ống.

Ống WRITER phải đóng pd[0] của nó, chuyển hướng thiết bị xuất chuẩn của nó (fd = 1) sang pd[1], để thiết bị xuất chuẩn của nó được

kết nối với đầu ghi của ống. Pipe READER phải đóng pd[1] của nó và chuyển hướng stdin (fd = 0) của nó sang pd[0], để stdin của nó được

kết nối với đầu đọc của pipe. Sau đó, hai quá trình được kết nối bằng đường ống. Các quy trình READER và WRITER trên cùng một đường

ống được đồng bộ hóa bằng chế độ ngủ/thức. Chức năng đọc/ghi ống được triển khai trong tệp pipe.c. Đóng ống
Machine Translated by Google

8.5 Tổ chức của EOS 283

các hàm mô tả được triển khai trong tệp open_close.c của hệ thống tệp. Một đường ống được giải phóng khi tất cả các bộ mô tả tệp

trên đường ống bị đóng. Để biết thêm thông tin về cách triển khai pipe, người đọc có thể tham khảo (Chương 6.14, Wang 2015) hoặc

tệp pipe.c để biết chi tiết.

8.5.11 Truyền tin nhắn

Ngoài các đường ống, hạt nhân EOS còn hỗ trợ giao tiếp giữa các quá trình bằng cách truyền tin nhắn. Cơ chế truyền tin nhắn bao gồm

các thành phần sau.

(1). Một tập hợp bộ đệm thông báo NPROC (MBUF) trong không gian kernel.

(2). Mỗi quy trình có một hàng đợi tin nhắn trong PROC.res.mqueue, chứa các tin nhắn được gửi đến nhưng quy trình chưa nhận được.

Tin nhắn trong hàng đợi tin nhắn được sắp xếp theo mức độ ưu tiên.

(3). send(char *msg, int pid): gửi tin nhắn đến tiến trình đích bằng pid. (4).

recv(char *msg): nhận hàng đợi tin nhắn của mẫu tin nhắn.

Trong EOS, việc truyền tin nhắn là đồng bộ. Quá trình gửi sẽ chờ nếu không có bộ đệm tin nhắn trống. Quá trình nhận sẽ chờ nếu

không có tin nhắn nào trong hàng đợi tin nhắn của nó. Quá trình đồng bộ hóa trong gửi/recv được thực hiện bằng ngữ nghĩa. Sau đây

liệt kê tệp mes.c.

/*************** tệp mes.c: Truyền tin nhắn *************/ #include "../type.h" /**** ****

loại bộ đệm tin nhắn trong

type.h ******** typedef struct mbuf{

struct mbuf *next; // con trỏ mbuf tiếp theo // pid người

người gửi int; gửi // mức độ ưu

ưu tiên int; tiên của tin nhắn

văn bản char[128]; // nội dung tin nhắn

} MBUF;

****************************************************/

MBUF mbuf[NMBUF], *freeMbuflist; // mbuf miễn phí; NMBUF=NPROC

khóa SEMAPHORE; // semaphore để truy cập độc quyền vào mbuf[ ]

int mbuf_init()

int tôi; MBUF *mp;

printf("mbuf_init\n");

for (i=0; i<NMBUF; i++){ // khởi tạo mbufs

mp = &mbuf[i];

mp->tiếp theo = mp+1;

mp->ưu tiên = 1; // cho enqueue()/dequeue()

freeMbuflist = &mbuf[0];

mbuf[NMBUF-1].next = 0;

mlock.value = 1; mlock.queue = 0;

MBUF *get_mbuf() // cấp phát một mbuf

MBUF *mp;

P(&mlock);

mp = freeMbuflist;

nếu (mp)

freeMbuflist = mp->next;

V(&mlock);

trả lại mp;

}
Machine Translated by Google

284 8 Hệ điều hành nhúng có mục đích chung

int put_mbuf(MBUF *mp) // giải phóng một mbuf

mp->văn bản[0] = 0;

P(&mlock);

mp->next = freeMbuflist; freeMbuflist

= mp;

V(&mlock);

int ksend(char *msg, int pid) // gửi tin nhắn tới pid

MBUF *mp; PROC *p;

// xác thực pid người nhận if ( pid

<= 0 || pid >= NPROC){

printf("sendMsg : pid mục tiêu không hợp lệ %d\n", pid);

trả về -1;

p = &proc[pid];

if (p->status == MIỄN PHÍ || p->status == ZOMBIE){

printf("Proc mục tiêu không hợp lệ %d\n", pid);

trả về -1;

mp = get_mbuf();

nếu (mp==0){

printf("không còn mbuf\n");

trả về -1;

mp->người gửi = đang chạy->pid;

strcpy(mp->văn bản, tin nhắn); // sao chép văn bản từ Umode sang mbuf

// gửi mp tới hàng đợi tin nhắn của người nhận

P(&p->res->mlock); enqueue(&p-

>res->mqueue, mp);

V(&p->res->mlock);

V(&p->res->tin nhắn); // thông báo cho người nhận

trả về 1;

int krecv(char *msg) // nhận tin nhắn từ mqueue RIÊNG

MBUF *mp;

P(&running->res->message); // chờ tin nhắn

P(&running->res->mlock);

mp = (MBUF *)dequeue(&running->res->mqueue);

V(&running->res->mlock);

nếu (mp){ // chỉ khi nó có tin nhắn

strcpy(tin nhắn, mp->văn bản); // sao chép nội dung tin nhắn vào Umode

put_mbuf(mp); // giải phóng mbuf

trả về 1;

trả về -1; // nếu proc bị tín hiệu tắt => không có tin nhắn

}
Machine Translated by Google

8.5 Tổ chức của EOS 285

8.5.12 Trình diễn việc truyền tin nhắn

Trong thư mục USER, các chương trình send.c và recv.c được sử dụng để thể hiện khả năng truyền tin nhắn của EOS. Người đọc
có thể kiểm tra tin nhắn gửi/recv như sau.

(1). đăng nhập vào bảng điều khiển. Nhập dòng lệnh recv &. Quá trình sh yêu cầu một đứa trẻ chạy lệnh recv nhưng không đợi quá
trình recv kết thúc để người dùng có thể tiếp tục nhập lệnh. Vì chưa có tin nhắn nào nên quá trình recv sẽ bị chặn trên hàng
đợi tin nhắn của nó trong kernel.
(2). Chạy lệnh gửi. Nhập pid của máy nhận và một chuỗi văn bản sẽ được gửi đến quy trình recv, cho phép nó tiếp tục. Ngoài ra,
người đọc cũng có thể đăng nhập từ một thiết bị đầu cuối khác để chạy lệnh gửi.

8.6 Quản lý bộ nhớ trong EOS

8.6.1 Bản đồ bộ nhớ của EOS

Phần sau đây hiển thị bản đồ bộ nhớ của hệ thống EOS.

------------------- Bản đồ bộ nhớ của EOS ---------------------

0-2MB : Hạt nhân EOS

2MB-4MB : Bộ đệm khung hiển thị LCD

4MB-5MB : Vùng dữ liệu của 256 bộ đệm I/O

5MB-6MB : Bảng trang Kmode cấp 2; 258 (1KB) pgtable

6MB-7MB : pgdirs cho (64) quy trình, mỗi quy trình pgdir=16KB

7MB-8MB : chưa sử dụng; để mở rộng, ví dụ tới 128 pgdir PROC

8MB-256MB: khung trang miễn phí cho phân trang động

256-258 MB: Dung lượng I/O 2 MB


-------------------------------------------------- ---------

Mã hạt nhân EOS và cấu trúc dữ liệu chiếm 2 MB bộ nhớ vật lý thấp nhất. Vùng bộ nhớ từ 2 đến 8 MB được hạt nhân EOS sử dụng
làm bộ đệm hiển thị LCD, bộ đệm I/O, bảng trang cấp 1 và cấp 2 của quy trình, v.v. Vùng bộ nhớ từ 8 đến 256 MB là miễn phí. Các
khung trang miễn phí từ 8 đến 256 MB được duy trì trong pfreeList để phân bổ/xử lý khung trang một cách linh hoạt.

8.6.2 Không gian địa chỉ ảo

EOS sử dụng sơ đồ ánh xạ không gian địa chỉ ảo KML, trong đó không gian hạt nhân được ánh xạ tới Địa chỉ ảo (VA) thấp và không
gian chế độ Người dùng được ánh xạ tới VA cao. Khi hệ thống khởi động, Đơn vị quản lý bộ nhớ (MMU) sẽ tắt, do đó mọi địa chỉ
đều là địa chỉ thực hoặc địa chỉ vật lý. Vì hạt nhân EOS được liên kết biên dịch với các địa chỉ thực nên nó có thể thực thi
trực tiếp mã C của hạt nhân. Đầu tiên, nó thiết lập bảng trang một cấp ban đầu ở mức 16 KB để tạo ánh xạ nhận dạng từ VA sang
PA và cho phép MMU dịch VA sang PA.

8.6.3 Chế độ hạt nhân Pgdir và Bảng trang

Trong reset_handler, sau khi khởi tạo con trỏ ngăn xếp của các chế độ đặc quyền khác nhau để xử lý ngoại lệ, nó sẽ xây dựng một
pgdir mới có kích thước 32 KB và các bảng trang cấp 2 liên quan có kích thước 5 MB. 258 mục nhập thấp của pgdir mới trỏ đến
các bảng trang cấp 2 của chúng ở mức 5 MB+i*1 KB (0<=i<258). Mỗi bảng trang chứa 256 mục, mỗi mục trỏ tới một khung trang 4 KB
trong bộ nhớ. Tất cả các mục khác của pgdir đều là 0. Trong pgdir mới, các mục 2048–4095 dành cho không gian VA ở chế độ Người dùng.
Vì 2048 mục cao nhất đều là 0, nên pgdir chỉ phù hợp với không gian VA kernel 258 MB. Nó sẽ là pgdir của
Machine Translated by Google

286 8 Hệ điều hành nhúng có mục đích chung

quá trình ban đầu P0, chỉ chạy trong Kmode. Nó cũng là nguyên mẫu của tất cả các pgdir khác vì các mục Kmode của chúng đều giống hệt

nhau. Sau đó, nó chuyển sang pgdir mới để sử dụng phân trang 2 cấp trong Kmode.

8.6.4 Xử lý bảng trang chế độ người dùng

Mỗi quy trình có pgdir ở mức 6 MB+pid*16 KB. 258 mục thấp của tất cả các pgdir đều giống hệt nhau vì không gian Kmode VA của chúng

giống nhau. Số lượng mục pgdir cho Umode VA phụ thuộc vào kích thước hình ảnh Umode, do đó phụ thuộc vào kích thước tệp hình ảnh

thực thi. Để đơn giản, chúng tôi đặt kích thước hình ảnh Umode, USZIE, thành 4 MB, đủ lớn cho tất cả các chương trình Umode được

sử dụng để thử nghiệm và trình diễn. Umode pgdir và bảng trang của một tiến trình chỉ được thiết lập khi tiến trình đó được tạo.

Khi tạo một quy trình mới trong fork1(), chúng tôi tính toán số lượng bảng trang Umode cần thiết là npgdir = USIZE/1 MB. Các mục

pgdir của Umode trỏ đến các khung trang được phân bổ động npgdir. Mỗi bảng trang chỉ sử dụng không gian 1 KB thấp của khung trang

(4 KB). Các thuộc tính của các mục nhập pgdir Umode được đặt thành 0x31 cho miền 1. Trong thanh ghi Kiểm soát truy cập miền, các bit

truy cập của cả hai miền 0 và 1 được đặt thành b01 cho chế độ máy khách, chế độ này sẽ kiểm tra các bit Quyền truy cập (AP) của

trang các mục trong bảng. Mỗi bảng trang Umode chứa các con trỏ tới 256 khung trang được cấp phát động. Các thuộc tính của các mục

trong bảng trang được đặt thành 0xFFE cho AP = 11 cho tất cả các trang con (1 KB) trong mỗi trang để cho phép truy cập R|W ở chế độ

Người dùng.

8.6.5 Chuyển đổi PGdir trong khi chuyển đổi quy trình

Trong quá trình chuyển đổi quy trình, chúng tôi chuyển pgdir từ quy trình hiện tại sang quy trình tiếp theo và xóa bộ đệm đệm TLB

cũng như I và D. Điều này được thực hiện bởi hàm switchPgdir() trong ts.s.

8.6.6 Phân trang động

Trong tệp mem.c, các hàm free_page_list(), palloc() và pdealloc() thực hiện phân trang động. Khi hệ thống khởi động, chúng tôi xây

dựng một pfreeList để xâu chuỗi tất cả các khung trang trống từ 8 đến 256 MB vào một danh sách liên kết. Trong quá trình vận hành hệ

thống, palloc() phân bổ khung trang trống từ pfreeList và pdealloc() giải phóng khung trang trở lại pfreeList để sử dụng lại. Phần
sau đây hiển thị tệp mem.c.

/****************** tệp mem.c: Phân trang động ****************/

int *pfreeList, *cuối cùng;

int mkPtable() // được gọi từ ts.s, tạo ptable ban đầu ở 16KB

int tôi;

int *ut = (int *)0x4000; // ở mức 16KB

u32 mục = 0 | 0x41E; // AP=01(Kmode R|W; Umode NO) domaian=0

cho (i=0; i<4096; i++) // xóa 4096 mục về 0

ut[i] = 0;

for (i=0; i<258; i++){ // điền vào bản đồ ID 258 mục thấp tới PA

ut[i] = mục nhập;

mục += 0x100000;

int *palloc() // cấp phát khung trang

int *p = pfreeList; if (p)

pfreeList

= (int *)*p;

trả lại p;

}
Machine Translated by Google

8.6 Quản lý bộ nhớ trong EOS 287

void pdealloc(int *p) // giải phóng một khung trang

*cuối cùng = (int)(*p);

*p = 0;

cuối cùng = p;

// xây dựng pfreeList các khung trang miễn phí int

*free_page_list(int *startva, int *endva)

int *p;

printf("build pfreeList: start=%x end=%x : ", startva, endva); pfreeList = startva;

p = bắt đầu;

while(p < (int *)(endva-1024)){

*p = (int)(p + 1024);

p += 1024;

cuối cùng = p;

*p = 0;

trở lại bắt đầu;

int build_ptable()

int *mtable = (int *)0x8000; // pgdir mới có kích thước 32KB int i, j,

*pgdir, paddr; printf("xây dựng

pgdir Kmode ở mức 32KB\n"); cho (i=0; i<4096; i++){

// loại bỏ mtable[ ]

mtable[i] = 0;

printf("xây dựng pgtables Kmode trong 5MB\n");

cho (i=0; i<258; i++){ // trỏ tới 258 pgtables trong 5MB

pgtable = (int *)(0x500000 + i*1024); mtable[i] =

(int)pgtable | 0x11; // Mục nhập 1KB trong 5MB paddr = i*0x100000 | 0x55E; cho

(j=0; j<256; j++){ // AP=01010101 CB=11 loại=10

pgtable[j] = paddr + j*4096; // tăng thêm 4KB

printf("xây dựng 64 pgdir có dung lượng 6MB\n");

cho (i=0; i<64; i++){

pgdir = (int *)(0x600000 + i*0x4000); // 16KB mỗi cái

for (j=0; j<4096; j++){ // loại bỏ pgdir[ ]

pgdir[j] = 0;

for (j=0; j<258; j++){ // sao chép 258 mục từ mtable[]

pgdir[j] = mtable[j];

}
Machine Translated by Google

288 8 Hệ điều hành nhúng có mục đích chung

8.7 Ngoại lệ và xử lý tín hiệu

Trong quá trình vận hành hệ thống, bộ xử lý ARM nhận ra sáu loại ngoại lệ, đó là FIQ, IRQ, SWI, data_abort, prefecth_abort và các ngoại lệ không

xác định. Trong số này, FIQ và IRQ dành cho các ngắt và SWI dành cho các cuộc gọi hệ thống. Vì vậy, các ngoại lệ thực sự duy nhất là data_abort,

prefeth_abort và các ngoại lệ không xác định, xảy ra trong các trường hợp sau.

Sự kiện data_abort xảy ra khi bộ điều khiển bộ nhớ hoặc MMU chỉ ra rằng địa chỉ bộ nhớ không hợp lệ đã được

đã truy cập. Ví dụ: cố gắng truy cập VA không hợp lệ.

Sự kiện prefetch_abort xảy ra khi nỗ lực tải lệnh dẫn đến lỗi bộ nhớ. Ví dụ: nếu 0x1000 là

nằm ngoài phạm vi VA thì BL 0x1000 sẽ gây ra việc hủy bỏ tìm nạp trước ở địa chỉ lệnh tiếp theo 0x1004.

Một sự kiện (lệnh) không xác định xảy ra khi lệnh được tìm nạp và giải mã không có trong tập lệnh ARM và

không có bộ đồng xử lý nào yêu cầu hướng dẫn.

Trong tất cả các hệ thống giống Unix, các ngoại lệ được chuyển đổi thành tín hiệu và được xử lý như sau.

8.7.1 Xử lý tín hiệu trong Unix/Linux

(1). Tín hiệu trong PROC quy trình: Mỗi PROC có một vectơ 32 bit, ghi lại các tín hiệu được gửi đến một quy trình. Trong vectơ bit, mỗi bit (trừ

bit 0) biểu thị một số tín hiệu. Tín hiệu n xuất hiện nếu bit n của vectơ bit là 1. Ngoài ra, nó còn có vectơ bit MASK để che các tín hiệu tương

ứng. Một tập hợp các tòa nhà cao tầng, chẳng hạn như sigmask, sigsetmask, siggetmask, sigblock, v.v. có thể được sử dụng để thiết lập, xóa và

kiểm tra vectơ bit MASK. Tín hiệu đang chờ xử lý chỉ có hiệu lực nếu nó không bị che khuất. Điều này cho phép một tiến trình trì hoãn việc xử lý

các tín hiệu bị che giấu, tương tự như việc CPU che giấu một số ngắt nhất định.

(2). Bộ xử lý tín hiệu: Mỗi tiến trình PROC có một mảng xử lý tín hiệu, int sig[32]. Mỗi mục nhập của mảng sig[32] chỉ định cách xử lý tín hiệu

tương ứng, trong đó 0 nghĩa là DEFault, 1 nghĩa là IGNore, giá trị khác 0 nghĩa là bằng chức năng bắt tín hiệu (trình xử lý) được cài đặt sẵn

trong Umode.

(3). Lỗi và tín hiệu bẫy: Khi một tiến trình gặp một ngoại lệ, nó sẽ bẫy trình xử lý ngoại lệ trong nhân hệ điều hành.

Trình xử lý bẫy chuyển đổi nguyên nhân ngoại lệ thành số tín hiệu và gửi tín hiệu đến quy trình đang chạy. Nếu ngoại lệ xảy ra trong chế độ kernel,

nguyên nhân có thể là do lỗi phần cứng hoặc rất có thể là do lỗi trong mã kernel, thì quy trình này không thể làm được gì. Vì vậy, nó chỉ in ra

thông báo lỗi PANIC và dừng lại. Hy vọng vấn đề có thể được tìm ra và khắc phục trong bản phát hành kernel tiếp theo. Nếu ngoại lệ xảy ra trong

chế độ Người dùng, quy trình sẽ xử lý tín hiệu bằng hàm xử lý tín hiệu trong mảng sig[] của nó. Đối với hầu hết các tín hiệu, hành động mặc định

của một quy trình là chấm dứt, với kết xuất bộ nhớ tùy chọn để gỡ lỗi. Một quy trình có thể thay thế hành động mặc định bằng IGNore(1) hoặc bộ thu

tín hiệu, cho phép nó bỏ qua tín hiệu hoặc xử lý nó ở chế độ Người dùng.

(4). Thay đổi Trình xử lý tín hiệu: Một quy trình có thể sử dụng syscall

int r ¼ tín hiệuð int số tín hiệu; void Þ xử lý ;

để thay đổi chức năng xử lý của một số tín hiệu đã chọn ngoại trừ SIGKILL(9) và SIGSTOP(19). Tín hiệu 9 được dành làm biện pháp cuối cùng để loại

bỏ một tiến trình đang chạy và tín hiệu 19 cho phép một tiến trình dừng một tiến trình con trong quá trình gỡ lỗi. Trình xử lý được cài đặt, nếu

không phải là 0 hoặc 1, phải là địa chỉ mục nhập của hàm trong Không gian người dùng của biểu mẫu

void catcher int tín hiệu ð Þf số . . .. . .. . .. . .g

(5). Xử lý tín hiệu: Một quá trình kiểm tra và xử lý tín hiệu bất cứ khi nào nó ở Kmode. Đối với mỗi số tín hiệu nổi bật n, trước tiên quy trình

sẽ xóa tín hiệu. Nó thực hiện hành động mặc định nếu sig[n] = 0, điều này thường khiến quá trình kết thúc.

Nó bỏ qua tín hiệu nếu sig[n] = 1. Nếu tiến trình có chức năng bắt được cài đặt sẵn cho tín hiệu, nó sẽ tìm nạp địa chỉ của bộ bắt và đặt lại bộ

bắt đã cài đặt về DEFault (0). Sau đó, nó điều khiển đường dẫn trở về theo cách nó quay trở lại để thực thi chức năng bắt trong Umode, truyền

tham số là số tín hiệu. Khi chức năng bắt kết thúc, nó sẽ quay trở lại điểm gián đoạn ban đầu, tức là đến nơi cuối cùng nó đã vào Kmode. Do đó,

quá trình này phải đi đường vòng để thực thi chức năng bắt trước. Sau đó nó tiếp tục thực hiện bình thường.

(6). Đặt lại bộ thu tín hiệu do người dùng cài đặt: Các chức năng bắt tín hiệu do người dùng cài đặt nhằm mục đích xử lý các lỗi bẫy trong mã

chương trình của người dùng. Vì chức năng bắt cũng được thực thi trong Umode nên nó có thể lại phạm phải loại lỗi bẫy tương tự. Nếu vậy, quá

trình này sẽ kết thúc bằng một vòng lặp vô hạn, nhảy giữa Umode và Kmode mãi mãi. Để ngăn chặn điều này, quy trình thường đặt lại trình xử lý về

DEFault (0) trước khi thực thi chức năng bắt. Điều này ngụ ý rằng chức năng bắt do người dùng cài đặt chỉ có hiệu lực đối với một lần xuất hiện

tín hiệu. Để bắt một lần xuất hiện khác của cùng một tín hiệu, chương trình Umode
Machine Translated by Google

8.7 Ngoại lệ và xử lý tín hiệu 289

phải cài lại cái bẫy. Tuy nhiên, cách xử lý các bộ thu tín hiệu do người dùng cài đặt không đồng nhất vì nó khác nhau giữa các phiên bản Unix khác

nhau. Chẳng hạn, trong BSD, bộ xử lý tín hiệu không được đặt lại nhưng tín hiệu tương tự bị chặn trong khi thực hiện bộ thu tín hiệu. Bạn đọc

quan tâm có thể tham khảo các trang man về tín hiệu và sigaction của Linux để biết thêm chi tiết.

(7). Tín hiệu giữa các quá trình: Ngoài việc xử lý các ngoại lệ, tín hiệu cũng có thể được sử dụng để liên lạc giữa các quá trình.

Một tiến trình có thể sử dụng syscall

int r ¼ kill pid ð Þ ;số tín hiệu ;

để gửi tín hiệu đến một quy trình khác được xác định bởi pid, khiến quy trình sau thực thi chức năng bắt được cài đặt sẵn trong Umode.

Cách sử dụng phổ biến của hoạt động tiêu diệt là yêu cầu quá trình được nhắm mục tiêu chấm dứt, do đó thuật ngữ tiêu diệt (hơi gây hiểu lầm). Nói

chung, chỉ những quy trình liên quan, ví dụ như những quy trình có cùng uid, mới có thể gửi tín hiệu cho nhau. Tuy nhiên, quy trình siêu người dùng

(uid=0) có thể gửi tín hiệu đến bất kỳ quy trình nào. Tòa nhà hủy diệt có thể sử dụng pid không hợp lệ, có nghĩa là các cách truyền tín hiệu khác

nhau. Ví dụ: pid = 0 gửi tín hiệu đến tất cả các quy trình trong cùng một nhóm quy trình, pid = -1 cho tất cả các quy trình có pid > 1, v.v. Người

đọc có thể tham khảo các trang man của Linux về signal/kill để biết thêm chi tiết.

(số 8). Signal và Wakeup/Unblock: kill chỉ gửi tín hiệu đến một tiến trình đích. Tín hiệu không có hiệu lực cho đến khi quá trình đích chạy. Khi gửi

tín hiệu đến một quy trình đích, có thể cần phải đánh thức/bỏ chặn quy trình đích nếu quy trình đó ở trạng thái SLEEP hoặc BLOCKed. Ví dụ: khi một

quá trình chờ đầu vào đầu cuối, có thể không đến trong một thời gian dài, nó được coi là có thể bị gián đoạn, nghĩa là nó có thể được đánh thức

hoặc bỏ chặn bởi các tín hiệu đến. Mặt khác, nếu một quy trình bị chặn đối với I/O SDC, quy trình sẽ sớm xuất hiện, thì quy trình đó sẽ không bị gián

đoạn và không được bỏ chặn bằng tín hiệu.

8.8 Xử lý tín hiệu trong EOS

8.8.1 Tín hiệu trong tài nguyên PROC

Trong EOS, mỗi PROC có một con trỏ tới cấu trúc tài nguyên, chứa các trường sau để xử lý tín hiệu và tín hiệu.

tín hiệu int; ==31 tín hiệu; bit 0 không được sử

dụng: int sig 32 ½ ; ==trình xử lý tín hiệu: 0 ¼ mặc định; 1 ¼ bỏ qua; một người bắt khác ở Umode:

Để đơn giản, liều EOS không hỗ trợ việc che dấu tín hiệu. Nếu muốn, người đọc có thể thêm mặt nạ tín hiệu vào hạt nhân EOS.

8.8.2 Nguồn gốc tín hiệu trong EOS

(1). Phần cứng: EOS hỗ trợ phím Control-C từ các thiết bị đầu cuối, được chuyển đổi thành tín hiệu ngắt SIGINT(2) được gửi đến tất cả các quy trình

trên thiết bị đầu cuối và bộ hẹn giờ ngắt quãng, được chuyển đổi thành tín hiệu cảnh báo SIGALRM(14) được gửi tới thiết bị đầu cuối. quá trình.

(2). Bẫy: EOS hỗ trợ các ngoại lệ data_abort, tìm nạp trước và hướng dẫn không xác định.

(3). Từ quy trình khác: EOS hỗ trợ lệnh gọi tòa nhà kill(pid, signal), nhưng nó không thực thi việc kiểm tra quyền. Do đó, một tiến trình có thể

giết chết bất kỳ tiến trình nào. Nếu tiến trình đích ở trạng thái SLEEP, kill() sẽ đánh thức nó. Nếu quy trình đích bị CHẶN đối với các đầu vào

trong KBD của trình điều khiển UART, thì nó cũng được bỏ chặn.

8.8.3 Cung cấp tín hiệu cho quá trình

Lệnh hủy tòa nhà cung cấp tín hiệu cho một quy trình đích. Thuật toán của kill syscall là

/*************** Thuật toán kill syscall ****************/


int kkill(int pid, int sig_number)
Machine Translated by Google

290 8 Hệ điều hành nhúng có mục đích chung

(1). xác nhận số tín hiệu và pid; (2). kiểm tra quyền

giết; // không được thi hành, có thể giết bất kỳ pid nào (3). đặt proc.signal.[bit_sig_number] thành

1; (4). nếu Proc là SLEEP, pid đánh thức; (5). nếu proc bị CHẶN

đối với đầu vào đầu cuối, hãy bỏ chặn proc;

8.8.4 Thay đổi bộ xử lý tín hiệu trong hạt nhân

Tòa nhà signal() thay đổi chức năng xử lý của một tín hiệu được chỉ định. Thuật toán của syscall tín hiệu là

/*********** Thuật toán tín hiệu syscall ****************/

int ksignal(int sig_number, int *catcher)

(1). xác thực số sig, ví dụ không thể thay đổi tín hiệu số 9; (2). int oldsig = đang chạy-

>sig[sig_number]; (3). đang chạy->sig[sig_number] = người bắt;

(4). trả lại chữ ký cũ;

8.8.5 Xử lý tín hiệu trong hạt nhân EOS

CPU thường kiểm tra các ngắt đang chờ xử lý khi kết thúc thực hiện lệnh. Tương tự như vậy, chỉ cần cho phép quá trình kiểm tra các tín

hiệu đang chờ xử lý khi kết thúc quá trình thực thi Kmode, tức là khi nó sắp quay trở lại Umode. Tuy nhiên, nếu một quá trình vào Kmode

thông qua một tòa nhà cao tầng, thì trước tiên nó phải kiểm tra và xử lý các tín hiệu. Điều này là do nếu một tiến trình đã có tín hiệu

đang chờ xử lý, điều này có thể khiến nó chết thì việc thực thi lệnh gọi hệ thống sẽ lãng phí thời gian. Mặt khác, nếu một tiến trình vào

Kmode do bị gián đoạn thì trước tiên nó phải xử lý ngắt đó. Thuật toán kiểm tra các tín hiệu đang chờ xử lý là

/************ Thuật toán tín hiệu kiểm tra *************/ int check_sig()

int tôi;

cho (i=1; i<NSIG; i++){

if (running->signal & (1 << i)){ Running->signal

&= *(1 << i);

trả lại tôi;

trả về 0;

Một tiến trình xử lý các tín hiệu nổi bật theo đoạn mã

if (đang chạy->tín hiệu)

psig();

Thuật toán của psig() là


Machine Translated by Google

8.8 Xử lý tín hiệu trong EOS 291

Hình 8.1 Khung ngăn xếp bẫy xử lý

/************** Thuật toán psig() *******************/

int psig(int sig)

int n;

while(n=check_sig()){ // đối với mỗi tín hiệu đang chờ xử lý

(1). xóa PROC.signal[bit_n] đang chạy; // xóa bit tín hiệu

(2). if (đang chạy->sig[n] == 1) // BỎ QUA tín hiệu

Tiếp tục;

(3). if (đang chạy->sig[n] == 0) // Mặc định: chết với dấu #

kexit(n<<8); // byte cao của exitCode=số tín hiệu

(4). // thực thi xử lý tín hiệu trong Umode

sửa lỗi chạy "khung ngăn xếp ngắt" của PROC để nó quay lại thực thi catcher(n) trong Umode;

8.8.6 Bộ bắt tín hiệu điều phối để thực thi ở chế độ người dùng

Trong thuật toán psig(), chỉ có bước (4) là thú vị và đầy thử thách. Vì vậy, chúng tôi sẽ giải thích nó chi tiết hơn. Mục tiêu
của bước (4) là để quá trình quay trở lại Umode để thực thi hàm catcher(int sig). Khi hàm catcher() kết thúc, nó sẽ quay trở lại
điểm mà quá trình cuối cùng đã vào Kmode. Các sơ đồ sau đây cho thấy cách thực hiện những điều này.
Khi một tiến trình bẫy kernel từ Umode, đỉnh ngăn xếp chế độ đặc quyền của nó chứa một "khung ngăn xếp bẫy" bao gồm 14 mục, như
trong Hình 8.1.
Để quá trình quay trở lại thực thi catcher(int sig) với số tín hiệu làm tham số, chúng tôi sửa đổi ngăn xếp bẫy
khung như sau.

(1). Thay thế uLR (tại chỉ mục 1) bằng địa chỉ mục nhập của catcher();
(2). Thay r0 (tại chỉ số 14) bằng số sig, sao cho khi vào catcher(), r0 = sig; (3). Đặt Chế độ
người dùng lr (r14) thành uLR, để khi catcher() kết thúc, nó sẽ quay trở lại điểm gián đoạn ban đầu bằng uLR.

8.9 Trình điều khiển thiết bị

Hạt nhân EOS hỗ trợ các thiết bị I/O sau.


Màn hình LCD: ARM Versatilepb VM sử dụng bộ điều khiển LCD màu ARM PL110 [Bộ điều khiển LCD màu ARM primeCell PL110] làm thiết
bị hiển thị chính. Trình điều khiển LCD được sử dụng trong EOS chính là trình điều khiển được phát triển trong Sect. 2.8.4. Nó có
thể hiển thị cả văn bản và hình ảnh.
Bàn phím: ARM Versatilepb VM bao gồm Giao diện bàn phím chuột (MKI) ARM PL050 cung cấp hỗ trợ cho chuột và bàn phím tương
thích PS/2 [ARM PL050 MKI]. EOS sử dụng giao diện người dùng dòng lệnh. Thiết bị chuột không được sử dụng. Trình điều khiển bàn
phím của EOS là phiên bản cải tiến của trình điều khiển bàn phím đơn giản được phát triển trong
Machine Translated by Google

292 8 Hệ điều hành nhúng có mục đích chung

Giáo phái. 3.6. Nó hỗ trợ cả phím chữ thường và chữ hoa. Nó sử dụng một số phím chức năng làm phím nóng để hiển thị thông tin kernel, chẳng hạn như danh

sách PROC miễn phí, trạng thái xử lý, nội dung của ReadyQueue, hàng đợi semaphore và cách sử dụng bộ đệm I/O, v.v. để gỡ lỗi. Nó cũng hỗ trợ một số chuỗi

phím thoát, chẳng hạn như các phím mũi tên, để mở rộng EOS trong tương lai.

UART: ARM Versatilepb VM hỗ trợ bốn thiết bị PL011 UART cho I/O nối tiếp. Trình điều khiển UART được đề cập trong phần. 3.7. Trong quá trình khởi

động, bộ khởi động EOS sử dụng UART0 cho giao diện người dùng. Sau khi khởi động, nó sử dụng các cổng UART làm thiết bị đầu cuối nối tiếp để người dùng

đăng nhập.

Bộ hẹn giờ: ARM Versatilepb VM chứa hai mô-đun hẹn giờ kép SB804 (ARM 926EJ-S 2016). Mỗi mô-đun hẹn giờ chứa hai bộ định thời, được điều khiển bởi

cùng một đồng hồ. Trình điều khiển hẹn giờ được đề cập trong phần. 3.5. Trong số bốn bộ hẹn giờ, EOS chỉ sử dụng bộ đếm thời gian0 để cung cấp các chức

năng dịch vụ hẹn giờ.

SDC: Trình điều khiển SDC hỗ trợ đọc/ghi nhiều vùng có kích thước khối tệp.

Ngoại trừ màn hình LCD, tất cả các trình điều khiển thiết bị đều được điều khiển bằng ngắt và sử dụng các ngữ nghĩa để đồng bộ hóa

giữa các trình xử lý ngắt và các tiến trình.

8.10 Lập kế hoạch quy trình trong EOS

Lập kế hoạch quy trình trong EOS được thực hiện theo mức độ ưu tiên của quy trình theo thời gian và động. Khi một tiến trình được lên lịch để chạy, nó

sẽ có một khoảng thời gian gồm 5–10 tích tắc hẹn giờ. Trong khi một tiến trình chạy trong Umode, bộ xử lý ngắt hẹn giờ sẽ giảm khoảng thời gian của nó đi

1 tại mỗi tích tắc hẹn giờ. Khi lát thời gian của quy trình hết hạn, nó sẽ đặt cờ quy trình chuyển đổi và gọi resechdule() để chuyển quy trình khi nó

thoát khỏi Kmode. Để giữ cho hệ thống đơn giản, EOS sử dụng sơ đồ ưu tiên đơn giản hóa. Thay vì tính toán lại mức độ ưu tiên của quy trình một cách

linh hoạt, nó chỉ sử dụng hai giá trị ưu tiên riêng biệt. Khi một tiến trình ở Umode, nó chạy với mức độ ưu tiên ở cấp độ người dùng là 128. Khi vào

Kmode, nó tiếp tục chạy với cùng mức độ ưu tiên. Nếu một quá trình bị chặn (do I/O hoặc hoạt động của hệ thống tệp), nó sẽ được cấp mức ưu tiên Kmode

cố định là 256 khi nó được bỏ chặn để chạy lại. Khi một quá trình thoát khỏi Kmode, nó sẽ quay trở lại mức độ ưu tiên của người dùng là 128.

Bài tập 2: Sửa đổi hạt nhân EOS để triển khai mức độ ưu tiên của quy trình động theo thời gian sử dụng CPU.

Dịch vụ hẹn giờ 8.11 trong EOS

Hạt nhân EOS cung cấp dịch vụ hẹn giờ sau cho các quy trình.

(1). tạm dừng (t): quá trình chuyển sang chế độ ngủ trong t giây.

(2). itimer(t): đặt khoảng thời gian là t giây. Gửi tín hiệu SIGALRM (14) đến quy trình khi hết thời gian hẹn giờ.

Để đơn giản hóa cuộc thảo luận, chúng ta giả sử rằng mỗi tiến trình chỉ có một yêu cầu định thời chưa xử lý và đơn vị thời gian là

tính bằng giây trong thời gian thực, tức là bộ đếm thời gian ảo của một tiến trình tiếp tục chạy cho dù tiến trình đó có đang thực thi hay không.

(1). Hàng đợi yêu cầu hẹn giờ: Dịch vụ hẹn giờ cung cấp cho mỗi quy trình một bộ hẹn giờ ảo hoặc logic bằng một bộ hẹn giờ vật lý duy nhất.

Điều này đạt được bằng cách duy trì hàng đợi hẹn giờ để theo dõi các yêu cầu hẹn giờ của quy trình. Phần tử hàng đợi hẹn giờ (TQE) là một
kết cấu

cấu trúc typedef tq{

struct tq *next; // con trỏ phần tử tiếp theo

int thời gian; // thời gian yêu cầu

struct PROC *proc; // con trỏ tới PROC

int (*hoạt động)(); // 0|1|con trỏ hàm xử lý

}TQE;

TQE *tq, tqe[NPROC]; // tq = con trỏ hàng đợi hẹn giờ

Trong TQE, hành động là một con trỏ hàm, trong đó 0 có nghĩa là WAKEUP, 1 có nghĩa là THÔNG BÁO, giá trị khác = địa chỉ mục nhập của hàm xử lý để

thực thi. Ban đầu, hàng đợi hẹn giờ trống. Khi các tiến trình gọi dịch vụ hẹn giờ, các yêu cầu của chúng sẽ được thêm vào hàng đợi hẹn giờ. Hình 8.2 cho

thấy một ví dụ về hàng đợi hẹn giờ.


Machine Translated by Google

Dịch vụ hẹn giờ 8.11 trong EOS 293

Hình 8.2 Hàng đợi yêu cầu hẹn giờ

Tại mỗi giây, trình xử lý ngắt sẽ giảm trường thời gian của mỗi TQE đi 1. Khi thời gian của TQE bằng 0, trình xử lý ngắt sẽ xóa TQE

khỏi hàng đợi hẹn giờ và gọi chức năng hành động của TQE. Ví dụ: sau 5 giây, nó xóa tqe[2] khỏi hàng đợi hẹn giờ và đánh thức P2. Trong

hàng đợi hẹn giờ ở trên, trường thời gian của mỗi TQE chứa thời gian còn lại chính xác. Nhược điểm của sơ đồ này là bộ xử lý ngắt phải

giảm trường thời gian của mỗi TQE. Nói chung, trình xử lý ngắt phải hoàn thành việc xử lý ngắt càng nhanh càng tốt. Điều này đặc biệt

quan trọng đối với bộ xử lý ngắt hẹn giờ. Nếu không, nó có thể mất tích hoặc thậm chí không bao giờ kết thúc. Chúng ta có thể tăng tốc

độ xử lý ngắt hẹn giờ bằng cách sửa đổi hàng đợi hẹn giờ như trong Hình 8.3.

Trong hàng đợi bộ đếm thời gian được sửa đổi, trường thời gian của mỗi TQE có liên quan đến thời gian tích lũy của tất cả các TQE

trước đó. Ở mỗi giây, bộ xử lý ngắt hẹn giờ chỉ cần giảm thời gian của TQE đầu tiên và xử lý bất kỳ TQE nào đã hết thời gian. Với thiết

lập này, việc chèn và xóa TQE phải được thực hiện cẩn thận. Ví dụ: nếu quá trình P4 thực hiện yêu cầu itimer(10), thì TQE của nó phải

được chèn sau TQ[1] với thời gian = 2, điều này sẽ thay đổi thời gian của TQ[3] thành 7. Tương tự, khi P1 gọi itimer( 0) để hủy yêu

cầu hẹn giờ, TQE[1] của nó sẽ bị xóa khỏi hàng đợi hẹn giờ, làm thay đổi thời gian của TQE[3] thành 12, v.v. Người đọc được khuyến

khích tìm ra các thuật toán chung để chèn và xóa TQE.

(2). Hàng đợi hẹn giờ là vùng quan trọng: Cấu trúc dữ liệu hàng đợi hẹn giờ được chia sẻ bởi các tiến trình và trình xử lý ngắt hẹn giờ.

Việc truy cập vào hàng đợi hẹn giờ phải được đồng bộ hóa để đảm bảo tính toàn vẹn của nó. EOS là một hệ điều hành đơn bộ xử lý, chỉ

cho phép một tiến trình thực thi trong kernel tại một thời điểm. Trong hạt nhân EOS, một quy trình không thể bị can thiệp bởi một quy

trình khác, do đó không cần khóa quy trình. Tuy nhiên, trong khi một tiến trình đang thực thi, các ngắt có thể xảy ra. Nếu ngắt hẹn giờ

xảy ra trong khi một quá trình đang sửa đổi hàng đợi hẹn giờ, thì quá trình đó sẽ được chuyển hướng để thực thi trình xử lý ngắt, trình

xử lý này cũng cố gắng sửa đổi hàng đợi hẹn giờ, dẫn đến tình trạng chạy đua. Để ngăn chặn sự can thiệp từ trình xử lý ngắt, quy trình

phải che dấu các ngắt khi truy cập vào hàng đợi hẹn giờ. Sau đây trình bày thuật toán của itimer().

/*************** Thuật toán itimer() **********************/ int itimer(t)

(1). Điền thông tin TQE[pid], ví dụ con trỏ proc, hành động.

(2). kho a(); // che giấu các ngắt

(3). duyệt hàng đợi hẹn giờ để tính toán vị trí chèn TQE; (4). chèn TQE và cập nhật thời gian của TQE

tiếp theo;

(5). mở khóa(); // vạch mặt các ngắt

Hình 8.3 Hàng đợi yêu cầu hẹn giờ được cải thiện
Machine Translated by Google

294 8 Hệ điều hành nhúng có mục đích chung

Hệ thống tập tin 8.12

Hệ điều hành có mục đích chung (GPOS) phải hỗ trợ hệ thống tệp để cho phép người dùng lưu và truy xuất thông tin dưới dạng tệp,

cũng như cung cấp nền tảng để chạy và phát triển các chương trình ứng dụng. Trên thực tế, hầu hết các nhân hệ điều hành đều yêu cầu tệp gốc

hệ thống để chạy. Vì vậy, hệ thống tập tin là một phần không thể thiếu của GPOS. Trong phần này chúng ta sẽ thảo luận về các nguyên tắc của

thao tác tệp, thể hiện thao tác tệp bằng các chương trình mẫu và hiển thị cách triển khai tệp EXT2 hoàn chỉnh

hệ thống trong EOS.

8.12.1 Mức độ hoạt động của tệp

Thao tác với tệp bao gồm năm cấp độ, từ thấp đến cao, như được hiển thị trong hệ thống phân cấp sau.

(1). Cấp độ phần cứng:

Hoạt động tập tin ở cấp độ phần cứng bao gồm

fdisk : chia một thiết bị lưu trữ dung lượng lớn; ví dụ: g: SDC; thành các phân vùng:

mkfs : định dạng phân vùng để sẵn sàng cho hệ thống tệp:

fsck : kiểm tra và sửa chữa hệ thống tập tin:

chống phân mảnh: nén các tập tin trong một hệ thống tập tin:

Hầu hết trong số này là các chương trình tiện ích hướng hệ thống. Một người dùng bình thường có thể không bao giờ cần đến chúng, nhưng chúng không thể thiếu

công cụ để tạo và duy trì hệ thống tập tin.

(2). Các chức năng của hệ thống tệp trong hạt nhân hệ điều hành:

Mọi nhân hệ điều hành có mục đích chung đều cung cấp hỗ trợ cho các hoạt động cơ bản của tệp. Sau đây liệt kê một số chức năng này trong

một nhân hệ thống giống Unix, trong đó tiền tố k biểu thị các chức năng của nhân, dựa vào trình điều khiển thiết bị cho I/O trên các thiết bị thực.

kmmount(), kumount() (gắn kết/umount hệ thống tập tin)

kmkdir(), krmdir() (tạo/xóa thư mục)

kchdir(), kgetcwd() (thay đổi thư mục, lấy tên đường dẫn CWD)

klink(), kunlink() (tệp liên kết cứng/hủy liên kết)

kchmod(), kchown(), ktouch() (thay đổi quyền r|w|x, chủ sở hữu, thời gian)

kcreat(), kopen() (tạo/mở file cho R,W,RW,APPEND)

kread(), kwrite() (đọc/ghi các tập tin đã mở)

klseek(); kclose() (lseek/đóng phần mô tả tệp đã mở)

ksymlink(), kreadlink() (tạo/đọc các tập tin liên kết tượng trưng)

kstat(), kfstat(), klstat() (lấy trạng thái/thông tin tệp)

kopendir(), kreaddir() (mở/đọc thư mục)

(3). Cuộc gọi hệ thống:

Các chương trình ở chế độ người dùng sử dụng lệnh gọi hệ thống để truy cập các chức năng của kernel. Ví dụ, chương trình sau đọc phần thứ hai

1024 byte của một tập tin.

/********* Chương trình ví dụ: đọc 1KB đầu tiên của file *******/

#include <stdio.h>

#include <stdlib.h>
Machine Translated by Google

Hệ thống tập tin 8.12 295

#include <fcntl.h>

int main(int argc, char *argv[ ]) // chạy dưới dạng tên file a.out

int fd, n;

char buf[1024];

if ((fd = open(argv[1], O_RDONLY)) < 0) // nếu mở thất bại

thoát (1);

lseek(fd, (dài)1024, SEEK_SET); // lseek tới byte 1024 // đọc 1024 byte của tệp

n = đọc(fd, buf, 1024);

đóng(fd);

Trong chương trình ví dụ trên, các hàm open(), read(), lseek() và close() là các hàm thư viện C. Mỗi hàm thư viện đưa ra một lệnh gọi hệ thống,

khiến tiến trình vào chế độ kernel để thực thi một hàm kernel tương ứng, ví dụ: open() chuyển đến kopen(), read() chuyển đến kread(), v.v. Khi quá trình

thực hiện xong kernel, nó sẽ trở về chế độ người dùng với kết quả mong muốn. Chuyển đổi giữa chế độ người dùng và chế độ kernel đòi hỏi nhiều hành động

(và thời gian). Do đó, việc truyền dữ liệu giữa kernel và không gian người dùng khá tốn kém. Mặc dù được phép đưa ra lệnh gọi hệ thống read(fd, buf, 1)

để chỉ đọc một byte dữ liệu, nhưng làm như vậy là không khôn ngoan vì một byte dữ liệu đó sẽ phải trả giá đắt. Mỗi khi phải vào chế độ kernel, chúng ta

nên làm nhiều nhất có thể để hành trình trở nên đáng giá. Trong trường hợp đọc/ghi tệp, cách tốt nhất là khớp với chức năng của kernel. Hạt nhân đọc/ghi

tệp theo kích thước khối, nằm trong khoảng từ 1 đến 8 KB. Ví dụ: trong Linux, kích thước khối mặc định là 4 KB đối với đĩa cứng và 1 KB đối với đĩa mềm.

Do đó, mỗi lệnh gọi hệ thống đọc/ghi cũng phải cố gắng truyền một khối dữ liệu tại một thời điểm.

(4). Chức năng I/O của thư viện:

Cuộc gọi hệ thống cho phép các chương trình ở chế độ người dùng đọc/ghi các khối dữ liệu, chỉ là một chuỗi byte. Họ không biết và cũng không quan

tâm đến ý nghĩa của dữ liệu. Chương trình chế độ người dùng thường cần đọc/ghi các ký tự, dòng hoặc bản ghi cấu trúc dữ liệu riêng lẻ, v.v. Chỉ với

các lệnh gọi hệ thống, chương trình chế độ người dùng phải tự thực hiện các thao tác này từ/đến vùng đệm.

Hầu hết người dùng sẽ cho rằng điều này quá bất tiện. Thư viện C cung cấp một tập hợp các hàm I/O tiêu chuẩn để thuận tiện cũng như để đạt hiệu quả

trong thời gian chạy. Các chức năng I/O của thư viện bao gồm:

I/O chế độ FILE: fopen(),fread(); fwrite(),fseek(),fclose(),fflush()

chế độ char I/O: getc(), getchar() ugetc(); putc(), putchar()

chế độ dòng I/O: gets(), fgets(); đặt(), fputs()

I/O được định dạng: scanf(),fscanf(),sscanf(); printf(),fprintf(),sprintf()

Ngoại trừ sscanf()/sprintf() đọc/ghi các vị trí bộ nhớ, tất cả các hàm I/O thư viện khác đều được xây dựng trên

đầu các lệnh gọi hệ thống, tức là cuối cùng chúng đưa ra các lệnh gọi hệ thống để truyền dữ liệu thực tế qua nhân hệ thống.

(5). Lệnh người dùng:

Thay vì viết chương trình, người dùng có thể sử dụng các lệnh Unix/Linux để thực hiện các thao tác trên tệp. Ví dụ về các lệnh của người dùng là

mkdir; rmdir; đĩa CD; pwd; ls; liên kết; hủy liên kết; rm; con mèo; cp; mv; chmod; vân vân:

Mỗi lệnh của người dùng trên thực tế là một chương trình thực thi (ngoại trừ cd), chương trình này thường gọi các hàm I/O của thư viện, do đó,

đưa ra các lệnh gọi hệ thống để gọi các hàm kernel tương ứng. Trình tự xử lý lệnh của người dùng là

Lệnh => Chức năng I/O thư viện => Lệnh gọi hệ thống => Chức năng hạt nhân

HOẶC Lệnh ========================== > Lệnh gọi hệ thống => Chức năng hạt nhân

Hầu hết các hệ điều hành hiện đại đều hỗ trợ Giao diện người dùng đồ họa (GUI), cho phép người dùng thực hiện các thao tác với tệp thông qua GUI.

Ví dụ: nhấp chuột vào biểu tượng tên chương trình có thể thực hiện chương trình. Tương tự, nhấp vào thiết bị trỏ trên biểu tượng tên tệp, sau đó

chọn Sao chép của menu kéo xuống sẽ sao chép nội dung tệp vào chung
Machine Translated by Google

296 8 Hệ điều hành nhúng có mục đích chung

bộ đệm, có thể được chuyển sang tệp đích bằng cách Dán, v.v. Nhiều người dùng đã quá quen với việc sử dụng GUI đến mức họ thường bỏ qua

và không hiểu điều gì đang thực sự diễn ra bên dưới giao diện GUI. Điều này phù hợp với hầu hết những người dùng máy tính chưa có kinh

nghiệm, nhưng không phù hợp với sinh viên khoa học máy tính và kỹ thuật máy tính.

(6). Tập lệnh Sh:

Mặc dù thuận tiện hơn nhiều so với các cuộc gọi hệ thống, nhưng các lệnh phải được nhập thủ công hoặc bằng cách kéo và nhấp liên tục

vào thiết bị trỏ như trong trường hợp sử dụng GUI, việc này rất tẻ nhạt và tốn thời gian. Tập lệnh Sh là các chương trình được viết

bằng ngôn ngữ lập trình sh, có thể được thực thi bởi trình thông dịch lệnh sh. Ngôn ngữ sh bao gồm tất cả các lệnh Unix/Linux hợp lệ. Nó

cũng hỗ trợ các biến và câu lệnh điều khiển, chẳng hạn như if, do, for, while, case, v.v. Trong thực tế, tập lệnh sh được sử dụng rộng

rãi trong lập trình hệ thống trên tất cả các hệ thống giống Unix. Ngoài sh, nhiều ngôn ngữ script khác như Perl và Tcl cũng được sử dụng
rộng rãi.

8.12.2 Thao tác vào/ra tệp

Hình 8.4 thể hiện sơ đồ các thao tác I/O của tập tin. Trong Hình 8.4, phần trên phía trên đường đôi biểu thị không gian kernel và phần

dưới biểu thị không gian người dùng của một tiến trình. Sơ đồ hiển thị chuỗi hành động khi một quá trình đọc/ghi một luồng tệp. Các

luồng điều khiển được xác định bằng các nhãn (1) đến (10), được giải thích bên dưới.

Hình 8.4 Sơ đồ hoạt động của tập tin


Machine Translated by Google

Hệ thống tập tin 8.12 297

——————————————————————— Chế độ người dùng Hoạt động ————————————————————————— ———

(1). Một quy trình ở chế độ Người dùng sẽ thực thi

TẬP TIN *fp = fopen("file", "r"); hoặc FILE *fp = fopen("file", "w");

mở một luồng tệp để ĐỌC hoặc VIẾT.

(2). fopen() tạo cấu trúc FILE trong không gian người dùng (heap) chứa bộ mô tả tệp, fd và fbuf[BLKSIZE]. Nó đưa ra lệnh gọi tòa nhà fd =

open("file", flags = READ or WRITE) tới kopen() trong kernel, xây dựng một OpenTable để thể hiện một phiên bản của tệp đã mở. mptr của

OpenTable trỏ đến INODE của tệp trong bộ nhớ. Đối với các tệp không đặc biệt, mảng i_block của INODE trỏ đến các khối dữ liệu trên thiết bị

lưu trữ. Nếu thành công, fp trỏ đến cấu trúc FILE, trong đó fd là bộ mô tả tệp được trả về bởi lệnh gọi tòa nhà open(). (3). fread(ubuf,

size, nitem, fp): ĐỌC nitem kích thước từng

cái thành ubuf bằng . sao chép dữ liệu từ fbuf của cấu trúc FILE sang ubuf, nếu

đủ thì quay lại;


. nếu fbuf không còn dữ liệu thì thực thi (4a).

(4a). phát hành syscall read(fd, fbuf, BLKSIZE) để đọc một khối tệp từ kernel sang fbuf, sau đó sao chép dữ liệu sang ubuf cho đến khi đủ
hoặc tệp không còn

dữ liệu nữa. (4b). fwrite(ubuf, size, nitem, fp): sao chép dữ liệu từ
. ubuf sang fbuf; if (fbuf có chỗ): chép dữ liệu vào

. fbuf, return; if (fbuf đã đầy): phát hành write(fd, fbuf, BLKSIZE) syscall để ghi một khối vào

kernel, sau đó ghi lại vào fbuf.

Do đó, fread()/fwrite() phát hành các lệnh gọi tòa nhà read()/write() tới kernel, nhưng chúng chỉ làm như vậy khi cần thiết và chúng

chuyển các khối dữ liệu từ/sang kernel trong BLKSIZE để có hiệu quả tốt hơn. Tương tự, các Hàm I/O Thư viện khác, chẳng hạn như fgetc/

fputc, fgets/fputs, fscanf/fprintf, v.v. cũng hoạt động trên fbuf trong cấu trúc FILE, nằm trong không gian người dùng.

==================== Hoạt động ở chế độ hạt nhân =====================

(5). Các hàm hệ thống tệp trong kernel:

Giả sử tòa nhà đọc (fd, fbuf[], BLKSIZE) của tệp không đặc biệt.

(6). Trong một tòa nhà cao tầng read(), fd là một bộ mô tả tệp đã mở, là một chỉ mục trong mảng fd của PROC đang chạy, trỏ tới một OpenTable

biểu thị tệp đã mở.

(7). OpenTable chứa chế độ mở của tệp, một con trỏ tới INODE của tệp trong bộ nhớ và byte offset hiện tại vào tệp để đọc/ghi. Từ phần bù của

OpenTable, hàm kread() của kernel . Tính số khối logic, lbk; . Chuyển đổi khối logic thành

khối vật lý, blk, thông qua mảng

INODE.i_block.

(số 8). Minode chứa INODE trong bộ nhớ của tệp. Mảng INODE.i_block chứa các con trỏ tới các khối đĩa vật lý.

Một hệ thống tệp có thể sử dụng số khối vật lý để đọc/ghi dữ liệu trực tiếp từ/đến các khối đĩa, nhưng những khối này sẽ phát sinh quá nhiều

I/O đĩa vật lý.

Việc xử lý tòa nhà write(fd, fbuf[], BLKSIZE) cũng tương tự, ngoại trừ việc nó có thể phân bổ các khối đĩa mới và tăng kích thước tệp
khi dữ liệu mới được ghi vào tệp.

(9). Để cải thiện hiệu suất I/O của đĩa, nhân hệ điều hành thường sử dụng một bộ bộ đệm I/O làm bộ đệm giữa bộ nhớ kernel và thiết bị I/O để

giảm số lượng I/O vật lý. Chúng ta sẽ thảo luận về bộ đệm I/O trong các phần sau.

Chúng tôi minh họa mối quan hệ giữa các cấp độ khác nhau của thao tác tệp bằng các ví dụ sau.

Ví dụ 1. Chương trình ví dụ C8.1 hướng dẫn cách tạo một thư mục bằng syscall

int mkdir(char *dirname, chế độ int)

/**** Chương trình C8.1: mkdir.c: chạy dưới dạng a.out dirname ****/
Machine Translated by Google

298 8 Hệ điều hành nhúng có mục đích chung

#include <stdio.h>

#include <stdlib.h>

#include <errno.h>

int main(int argc, char *argv[])

{ int r;

nếu (argc < 2){

printf("Cách sử dụng: a.out dirname); exit(1);

nếu ((r = mkdir(argv[1], 0755)) < 0)

perror("mkdir"); // in thông báo lỗi

Để tạo một thư mục, chương trình phải thực hiện lệnh gọi hệ thống mkdir() vì chỉ kernel mới biết cách tạo

thư mục. Cuộc gọi hệ thống được định tuyến đến kmkdir() trong kernel, cố gắng tạo một thư mục mới với tên được chỉ định và

cách thức. Cuộc gọi hệ thống trả về r = 0 nếu thao tác thành công hoặc -1 nếu thất bại. Nếu cuộc gọi hệ thống không thành công, số lỗi nằm ở

(bên ngoài) biến toàn cục errno. Chương trình có thể gọi hàm thư viện perror("mkdir") để in tên chương trình

theo sau là một chuỗi mô tả nguyên nhân gây ra lỗi, ví dụ mkdir: File tồn tại, v.v.

Bài 1: Sửa chương trình C8.1 để tạo nhiều thư mục bằng một lệnh, ví dụ tham số dòng lệnh mkdir dir1 dir2 …. dirn // với

Bài tập 2: Lệnh gọi hệ thống Linux int r = rmdir(char *pathname) loại bỏ một thư mục, thư mục này phải trống. Viết chữ C

chương trình rmdir.c, loại bỏ một thư mục.

Ví dụ 2: Ví dụ này phát triển chương trình ls bắt chước lệnh ls –l của Unix/Linux: Trong Unix/Linxu, stat

cuộc gọi hệ thống

int stat(const char *file_name, struct stat *buf);

int lstat(const char *file_name, struct stat *buf);

trả về thông tin của tập tin được chỉ định. Sự khác biệt giữa stat() và lstat() là cái trước tuân theo các liên kết tượng trưng nhưng
cái sau thì không. Thông tin trả về có cấu trúc stat (được xác định trong stat.h), đó là

chỉ số cấu trúc {

dev_t st_dev; /* thiết bị */

ino_t st_ino; /* số inode */

chế độ_t st_mode; /*loại tệp và quyền truy cập */

nlink_t st_nlink; /*số lượng liên kết cứng */

uid_t st_uid; /* ID người dùng của chủ sở hữu tệp */

gid_t st_gid; /* ID nhóm của chủ sở hữu */

dev_t st_rdev; /* loại thiết bị (nếu là thiết bị inode) */

tắt_t st_size; /* tổng kích thước, tính bằng byte */

blksize_t st_blksize; /* kích thước khối cho I/O hệ thống tập tin */

blkcnt_t st_blocks; /* số lượng cung 512 byte */

thời gian_t thứ_giờ; /*thời điểm truy cập lần cuối*/

thời gian_t st_mtime; /*thời điểm sửa đổi lần cuối */

thời gian_t st_ctime; /* thời điểm thay đổi trạng thái cuối cùng */

};

Trong cấu trúc stat, st_dev xác định thiết bị (số) mà tệp nằm trên đó và st_ino là số inode của nó trên đó

thiết bị. Trường st_mode là số nguyên 2 byte, chỉ định loại tệp, cách sử dụng đặc biệt và các bit quyền để bảo vệ.

Cụ thể, các bit của st_mode là


Machine Translated by Google

Hệ thống tập tin 8.12 299

4 3333

|----|---|---|---|---| |tttt|fff|

rwx|rwx|rwx|

4 bit cao nhất của st_mode xác định loại tệp. Ví dụ: b1000 = tệp REGular, b0100 = DIRectory, b1100 = tệp liên kết
tượng trưng, v.v. Loại tệp có thể được kiểm tra bằng các macro được xác định trước S_ISREG, S_ISDIR, S_ISLNK, v.v.
Ví dụ,

if (S_ISREG(st_mode)) // kiểm tra tệp REGular if (S_ISDIR(st_mode)) //

kiểm tra tệp DIR if (S_ISLNK(st_mode)) // kiểm tra tệp liên kết

tượng trưng

9 bit thấp của st_mode xác định các bit quyền là r (có thể đọc được) w (có thể ghi) x (có thể thực thi) đối với chủ sở hữu tệp,

cùng nhóm với chủ sở hữu và những người khác. Đối với các tập tin thư mục, bit x có nghĩa là cd vào thư mục có được phép hay

không. Trường st_nlink là số lượng liên kết cứng đến tệp, st_size là kích thước tệp tính bằng byte, st_atime, st_mtime và st_ctime

là các trường thời gian. Trong các hệ thống giống Unix, thời gian là thời gian trôi qua tính bằng giây kể từ 00:00:00 ngày 1 tháng

1 năm 1970. Các trường thời gian có thể được chuyển đổi thành chuỗi ở dạng lịch bằng hàm thư viện char *ctime(time_t *time).

Dựa trên thông tin được trả về bởi lệnh gọi hệ thống stat, chúng ta có thể viết chương trình ls.c, hoạt động giống như
lệnh ls –l của Unix/Linux. Chương trình được ký hiệu là C8.2, được hiển thị bên dưới.

/** Chương trình C8.2: ls.c: chạy dưới dạng a.out [tên tệp] **/
#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <sys/stat.h>

#include <time.h>

#include <sys/types.h>

#include <dirent.h>

#include <errno.h>

char *t1 = "xwrxwrxwr-------";

char *t2 = "----------------";

struct stat mystat, *sp;

int ls_file(char *fname) // liệt kê một tập tin

{
struct stat fstat, *sp = &fstat;

int r, tôi;
char sbuf[4096];

r = lstat(fname,sp); // lstat tập tin

nếu (S_ISDIR(sp->st_mode))

printf("%c",'d'); // in kiểu file là d

nếu (S_ISREG(sp->st_mode))

printf("%c",'-'); // in loại tệp dưới dạng -

nếu (S_ISLNK(sp->st_mode))

printf("%c",'l'); // in ra loại file là l

vì (i=8; i>=0; i--){

if (sp->st_mode & (1<<i))

printf("%c", t1[i]); // bit cho phép in dưới dạng rwx

khác

printf("%c", t2[i]); // bit cho phép in dưới dạng -

}
printf("%4d", sp->st_nlink); // số lượng liên kết

printf("%4d ", sp->st_uid printf("%8d // uid

", sp->st_size); // kích thước file strcpy(ftime, ctime(&sp-

>st_ctime));
Machine Translated by Google

300 8 Hệ điều hành nhúng có mục đích chung

ftime[strlen(ftime)-1] = 0; // giết \n ở cuối

printf("%s",ftime); printf("%s", // thời gian ở dạng lịch

basename(fname)); if (S_ISLNK(sp->st_mode)){ // tên cơ sở của tập tin

// nếu liên kết tượng trưng

r = readlink(fname, sbuf, 4096);

printf(" -> %s", sbuf); // -> tên đường dẫn được liên kết

printf("\n");

int ls_dir(char *dname) // liệt kê một DIR

tên char[256]; // Tên tệp EXT2: 1-255 ký tự

TRỰC TIẾP *dp;

struct dirent *ep;

// mở DIR để đọc tên

dp = opendir(dname); // cuộc gọi tòa nhà opendir()

while (ep = readdir(dp)){ // readdir() syscall

strcpy(tên, ep->d_name);

if (!strcmp(name, ".") || !strcmp(name, ".."))

Tiếp tục; // bỏ qua . và .. mục

strcpy(tên, dname);

strcat(tên, "/");

strcat(tên, ep->d_name);

ls_file(tên); // gọi list_file()

int main(int argc, char *argv[])

struct stat mystat, *sp;

int r;

char *s;

tên tệp char[1024], cwd[1024];

s = argv[1]; nếu // ls [tên tệp]

(argc == 1) // không có tham số: ls CWD

s = "./";

sp = &mystat;

if ((r = stat(s, sp)) < 0){ // stat() tòa nhà

lỗi ("ls"); thoát (1);

strcpy(tên file, s);

nếu (s[0] != '/'){ // tên tệp có liên quan đến CWD

getcwd(cwd, 1024); // lấy đường dẫn CWD

strcpy(tên file, cwd);

strcat(tên file, "/");

strcat(tên file,s); // xây dựng $CWD/tên tệp

nếu (S_ISDIR(sp->st_mode))

ls_dir(tên tệp); // liệt kê TRỰC TIẾP

khác

ls_file(tên tệp); // liệt kê một tập tin

Người đọc có thể biên dịch và chạy chương trình C8.2 trên Linux. Nó sẽ liệt kê một tệp hoặc một DIR trong cùng một
định dạng giống như lệnh ls –l của Linux.
Machine Translated by Google

Hệ thống tập tin 8.12 301

Ví dụ 4: Chương trình sao chép tệp: Ví dụ này cho thấy việc triển khai hai chương trình sao chép tệp; một người sử dụng các cuộc gọi hệ thống

và cái còn lại sử dụng các hàm I/O của thư viện. Cả hai chương trình đều chạy dưới dạng a.out src dest, sao chép src sang dest. Chúng tôi liệt kê

các chương trình cạnh nhau để chỉ ra những điểm tương đồng và khác biệt của chúng.

------------ cp.sysall.c ------------|------ cp.libio.c --------- ---


#include <stdio.h> | #include <stdio.h>
#include <stdlib.h> | #include <stdlib.h>
#include <fcntl.h> |
main(int argc, char *argv[ ]) | main(int argc, char *argv[ ])
{ |{
int fd, gd; | TẬP TIN *fp, *gp;
int n; | int n;
char buf[4096]; | char buf[4096];
if (argc < 3) exit(1); | if (argc < 3) exit(1);
fd = open(argv[1], O_RDONLY); | fp = fopen(argv[1], "r");
gd = open(argv[2],O_WRONLY|O_CREAT);| gp = fopen(argc[1], "w+");
if (fd < 0 || gd < 0) exit(2); | if (fp==0 || gp==0) exit(2);
while(n=read(fd, buf, 4096)){ | while(n=fread(buf,1,4096,fp)){
viết(gd, buf, n); fwrite(buf, 1, n, gp);
} | |}
đóng(fd); đóng(gd); | fclose(fp); fclose(gp);
} |}
-------------------------------------------------- -------------------

Chương trình cp.syscall.c hiển thị ở phía bên trái sử dụng các cuộc gọi hệ thống. Đầu tiên, nó đưa ra lời gọi hệ thống open() để mở src

tệp dành cho ĐỌC và tệp đích là VIẾT, tạo tệp đích nếu nó không tồn tại. Các cuộc gọi tòa nhà open() trả về hai

(số nguyên) mô tả tập tin, fd và gd. Sau đó, nó sử dụng vòng lặp để sao chép dữ liệu từ fd sang gd. Trong vòng lặp, nó đưa ra lệnh gọi tòa nhà read() trên fd tới

đọc dữ liệu lên tới 4 KB từ kernel vào bộ đệm cục bộ. Sau đó, nó phát lệnh write() syscall trên gd để ghi dữ liệu từ cục bộ

đệm vào kernel. Vòng lặp kết thúc khi read() trả về 0, cho biết tệp nguồn không còn dữ liệu để đọc.

Chương trình cp.libio.c hiển thị ở phía bên phải sử dụng các chức năng I/O của thư viện. Đầu tiên, nó gọi fopen() để tạo hai file

các luồng, fp và gp, là các con trỏ tới cấu trúc FILE. fopen() tạo cấu trúc FILE (được định nghĩa trong stdio.h) trong

vùng heap của chương trình. Mỗi cấu trúc FILE chứa một bộ đệm cục bộ, char fbuf[BLKSIZE], có kích thước khối tệp và một bộ mô tả tệp

cánh đồng. Sau đó, nó thực hiện lệnh gọi hệ thống open() để lấy bộ mô tả tệp và ghi lại bộ mô tả tệp trong cấu trúc FILE. Sau đó nó

trả về một luồng tệp (con trỏ) trỏ vào cấu trúc FILE. Khi chương trình gọi fread(), nó sẽ cố đọc dữ liệu từ fbuf trong

cấu trúc FILE. Nếu fbuf trống, fread() thực hiện lệnh gọi hệ thống read() để đọc BLKSIZE dữ liệu từ kernel sang fbuf. Sau đó nó

chuyển dữ liệu từ fbuf sang bộ đệm cục bộ của chương trình. Khi chương trình gọi fwrite(), nó ghi dữ liệu từ cục bộ của chương trình

đệm vào fbuf trong cấu trúc FILE. Nếu fbuf đầy, fwrite() thực hiện lệnh gọi hệ thống write() để ghi BLKSIZE dữ liệu từ fbuf

đến hạt nhân. Do đó, các hàm I/O của thư viện được xây dựng dựa trên các lệnh gọi hệ thống, nhưng chúng chỉ thực hiện các lệnh gọi hệ thống khi cần thiết và chúng

truyền dữ liệu từ/đến kernel ở kích thước khối tệp để có hiệu quả tốt hơn. Dựa trên những cuộc thảo luận này, người đọc sẽ có thể

suy ra chương trình nào hiệu quả hơn nếu mục tiêu chỉ là truyền dữ liệu. Tuy nhiên, nếu một chương trình ở chế độ người dùng có ý định

truy cập các ký tự đơn trong tệp hoặc đọc/ghi dòng, v.v. sử dụng các hàm I/O của thư viện sẽ là lựa chọn tốt hơn.

Bài tập 3: Giả sử chúng ta viết lại các vòng truyền dữ liệu trong các chương trình của Ví dụ 3 như sau, cả hai đều truyền một byte với tốc độ

một thời gian.

cp.syscall.c | cp.syscall.c
-------------------------------------------------- ------------

while((n=read(fd, buf, 1)){ | while((n=fgetc(fp))!= EOF){


viết(gd, buf, n); fputc(n, gp);
} | |}
-------------------------------------------------- ------------

Chương trình nào sẽ hiệu quả hơn? Giải thích lý do của bạn.
Machine Translated by Google

302 8 Hệ điều hành nhúng có mục đích chung

Bài tập 4: Khi sao chép tệp, tệp nguồn phải là tệp thông thường và chúng ta không bao giờ nên sao chép tệp vào chính nó. Sửa đổi các

chương trình trong Ví dụ 3 để xử lý các trường hợp này.

8.12.3 Hệ thống tệp EXT2 trong EOS

Trong nhiều năm, Linux đã sử dụng EXT2 (Card et al. 1995; EXT2 2001) làm hệ thống tệp mặc định. EXT3 (ETX3 2015) là phần mở rộng của EXT2.

Phần bổ sung chính cho EXT3 là một tệp nhật ký, ghi lại các thay đổi được thực hiện đối với hệ thống tệp trong nhật ký nhật ký. Nhật ký

cho phép khôi phục nhanh hơn các lỗi trong trường hợp hệ thống tệp gặp sự cố. Hệ thống tệp EXT3 không có lỗi giống hệt với hệ thống tệp

EXT2. Phần mở rộng mới nhất của EXT3 là EXT4 (Cao et al. 2007). Thay đổi chính trong EXT4 là việc phân bổ các khối đĩa. Trong EXT4, số

khối là 48 bit. Thay vì các khối đĩa rời rạc, EXT4 phân bổ các phạm vi khối đĩa liền kề nhau, được gọi là phạm vi. EOS là một hệ thống

nhỏ chủ yếu dành cho việc dạy và học phần bên trong của hệ điều hành nhúng. Dung lượng lưu trữ tệp lớn không phải là mục tiêu thiết kế.

Các nguyên tắc thiết kế và triển khai hệ thống tệp, nhấn mạnh vào tính đơn giản và khả năng tương thích với Linux, là những điểm chính.

Vì những lý do này, chúng tôi chọn ETX2 làm hệ thống tệp. Hỗ trợ cho các hệ thống tệp khác, ví dụ FAT và NTFS, không được triển khai

trong hạt nhân EOS. Nếu cần, chúng có thể được triển khai dưới dạng chương trình tiện ích ở cấp độ người dùng. Phần này mô tả việc

triển khai hệ thống tệp EXT2 trong hạt nhân EOS. Việc triển khai hệ thống tệp EXT2 được thảo luận chi tiết trong (Wang 2015). Để đảm bảo

tính đầy đủ và thuận tiện cho người đọc, chúng tôi đưa thông tin tương tự vào đây.

8.12.3.1 Tổ chức hệ thống tệp Hình 8.5 cho

thấy tổ chức bên trong của hệ thống tệp EXT2. Sơ đồ tổ chức được giải thích bằng các nhãn từ (1) đến (5).

(1) là cấu trúc PROC của tiến trình đang chạy. Mỗi PROC có một trường cwd trỏ đến INODE trong bộ nhớ của Thư mục làm việc hiện tại (CWD)

của PROC. Nó cũng chứa một mảng các bộ mô tả tệp, fd[], trỏ đến các phiên bản tệp đã mở. (2) là con trỏ thư mục gốc của hệ thống tập tin.
Nó trỏ đến

INODE gốc trong bộ nhớ. Khi hệ thống khởi động, một trong các thiết bị được chọn làm thiết bị gốc, thiết bị này phải là hệ thống tệp EXT2

hợp lệ. INODE gốc (inode #2) của thiết bị gốc được tải vào bộ nhớ dưới dạng thư mục gốc (/) của hệ thống tệp. Thao tác này được gọi là

"mount hệ thống tập tin gốc" (3) là một mục openTable. Khi một tiến trình mở một tệp, một mục nhập của mảng fd của PROC sẽ trỏ đến một

openTable, bảng này trỏ tới INODE trong bộ nhớ của tệp đã mở. (4) là INODE trong bộ nhớ. Bất cứ khi nào cần một tệp, INODE của nó sẽ được

tải vào khe minode để tham khảo. Vì INODE là duy

nhất nên mỗi INODE chỉ có thể có một bản sao trong bộ nhớ bất kỳ lúc nào. Trong minode, (dev, ino) xác định INODE đến từ đâu, để ghi INODE

trở lại đĩa nếu được sửa đổi. Trường refCount ghi lại số lượng tiến trình đang sử dụng minode. Trường dirty cho biết INODE đã được sửa

đổi hay chưa. Cờ được gắn cho biết INODE đã được gắn hay chưa và nếu có, mntabPtr trỏ đến mục nhập bảng gắn của hệ thống tệp được gắn.

Trường khóa nhằm đảm bảo rằng INODE trong bộ nhớ chỉ có thể được truy cập bởi một tiến trình tại một thời điểm, ví dụ như khi sửa đổi

INODE hoặc trong quá trình đọc/ghi. (5) là bảng chứa các hệ thống tập tin được gắn kết. Đối với mỗi hệ thống tệp được gắn, một mục trong

bảng gắn được sử dụng để ghi lại thông tin hệ thống tệp được gắn. Trong INODE trong bộ nhớ của điểm gắn kết, cờ được gắn kết được bật

và mntabPtr trỏ tới mục nhập bảng gắn kết.

Trong mục bảng gắn kết, mntPointPtr trỏ trở lại INODE trong bộ nhớ của điểm gắn kết. Như sẽ được trình bày sau, những con trỏ liên kết

đôi này cho phép chúng ta vượt qua các điểm gắn kết khi duyệt qua cây hệ thống tệp. Ngoài ra, một mục trong bảng gắn kết cũng có thể chứa

thông tin khác của hệ thống tệp được gắn kết, chẳng hạn như tên thiết bị, siêu khối, bộ mô tả nhóm và bitmap, v.v. để tham khảo nhanh.

Đương nhiên, nếu bất kỳ thông tin nào như vậy đã bị thay đổi trong khi ở trong bộ nhớ, chúng phải được ghi lại vào thiết bị lưu trữ

khi hệ thống tệp được gắn kết được bỏ qua.

8.12.3.2 Tệp nguồn trong Thư mục EOS/FS Trong cây nguồn hạt

nhân EOS, thư mục FS chứa các tệp triển khai hệ thống tệp EXT2. Các tập tin được tổ chức như sau.
Machine Translated by Google

Hệ thống tập tin 8.12 303

Hình 8.5 Cấu trúc dữ liệu hệ thống tệp EXT2

———————— Các tập tin phổ biến của FS —————————————————————————

type.h : kiểu cấu trúc dữ liệu EXT2

Global.c: biến toàn cục của FS


util.c : các hàm tiện ích phổ biến: getino(), iget(), iput(), search(), v.v.

các chức năng quản lý nút/khối phân bổ_deallocate.c

Việc thực hiện hệ thống tập tin được chia thành ba cấp độ. Mỗi cấp độ xử lý một phần riêng biệt của hệ thống tập tin. Cái này

làm cho quá trình thực hiện được mô đun hóa và dễ hiểu hơn. Cấp 1 triển khai cây hệ thống tệp cơ bản. Nó

chứa các tệp sau, thực hiện các chức năng được chỉ định.

————————————————— Cấp 1 của FS ——————————————————————

mkdir_creat.c : tạo thư mục, tạo tập tin thông thường và đặc biệt

cd_pwd.c : thay đổi thư mục, lấy đường dẫn CWD


rmdir.c : xóa thư mục
link_unlink.c : liên kết cứng và hủy liên kết các tập tin

symlink_readlink.c : tập tin liên kết tượng trưng

stat.c : trả về thông tin tập tin

linh tinh1.c : truy cập, chmod, chown, touch, v.v.


————————————————————————————————————————————————
Machine Translated by Google

304 8 Hệ điều hành nhúng có mục đích chung

Các chương trình cấp người dùng sử dụng chức năng FS cấp 1 bao gồm

mkdir; tạo ra; mknod; rmdir; liên kết; hủy liên kết; liên kết tượng trưng; rm; ls; cd và pwd; vân vân:

Cấp độ 2 thực hiện các chức năng đọc/ghi nội dung tập tin.

———————————————————— Cấp 2 của FS —————————————————————————— ————

open_close_lseek.c : mở tệp để READ|WRITE|APPEND, đóng tệp và lseek

đọc.c : đọc từ một bộ mô tả tập tin đã mở

viết.c : ghi vào bộ mô tả tập tin đã mở

opendir_readdir.c : mở và đọc thư mục

dev_switch_table : đọc/ghi các tập tin đặc biệt

đệm.c : khối quản lý bộ đệm I/O của thiết bị

Cấp 3 thực hiện mount, umount và bảo vệ tập tin.

———————————————————— Cấp 3 của FS —————————————————————————— ———

mount_umount.c : gắn kết/umount hệ thống tập tin

bảo vệ tập tin : kiểm tra quyền truy cập

khóa tập tin : khóa/mở khóa tập tin


————————————————————————————————————————————————————— ———————————————

8.12.4 Thực hiện FS cấp 1

(1). Tệp type.h: Tệp này chứa các kiểu cấu trúc dữ liệu của hệ thống tệp EXT2, chẳng hạn như siêu khối, bộ mô tả nhóm,

cấu trúc mục inode và thư mục. Ngoài ra, nó còn chứa bảng tệp mở, bảng gắn kết, đường ống và cấu trúc PROC
và các hằng số của hạt nhân EOS.

(2). Tệp Global.c: Tệp này chứa các biến toàn cục của hạt nhân EOS. Ví dụ về các biến toàn cục là

minode MINODE[NMINODES]; // trong bộ nhớ INODE

MOUNT mounttab[NMOUNT]; // gắn bảng

OFT thường [NOFT]; // Phiên bản tệp đã mở

(3). Tệp util.c: Tệp này chứa các chức năng tiện ích của hệ thống tệp. Các hàm tiện ích quan trọng nhất là getino(), iget()

và iput(), được giải thích chi tiết hơn.

(3).1. u32 getino(int *dev, char *pathname): getino() trả về số inode của tên đường dẫn. Trong khi duyệt qua một tên đường dẫn

số thiết bị có thể thay đổi nếu tên đường dẫn đi qua (các) điểm gắn kết. Tham số dev được sử dụng để ghi lại kết quả cuối cùng

số thiết bị. Do đó, getino() về cơ bản trả về (dev, ino) của tên đường dẫn. Hàm sử dụng tokenize() để chia nhỏ

tên đường dẫn thành chuỗi thành phần. Sau đó, nó gọi search() để tìm kiếm các chuỗi thành phần trong các minode thư mục liên tiếp.

Search() trả về số inode của chuỗi thành phần nếu nó tồn tại hoặc 0 nếu không.

(3).2. MINODE *iget(in dev, u32 ino): Hàm này trả về một con trỏ tới INODE trong bộ nhớ của (dev, ino). Sự trở lại

minode là duy nhất, tức là chỉ có một bản sao của INODE tồn tại trong bộ nhớ kernel. Ngoài ra, minode bị khóa (bởi

semaphore khóa của minode) để sử dụng độc quyền cho đến khi nó được giải phóng hoặc mở khóa.

(3).3. iput(MINODE *mip): Hàm này giải phóng và mở khóa một minode được trỏ bởi mip. Nếu tiến trình này là tiến trình cuối cùng được sử dụng

minode (refCount = 0), INODE được ghi lại vào đĩa nếu nó bị bẩn (đã sửa đổi).

(3).4. Khóa Minodes: Mỗi minode có một trường khóa, đảm bảo rằng một minode chỉ có thể được truy cập bởi một tiến trình tại

một lúc nào đó, đặc biệt là khi sửa đổi INODE. Unix sử dụng cờ bận và ngủ/thức để đồng bộ hóa các tiến trình truy cập

cùng một minode. Trong EOS, mỗi minode có một semaphore khóa với giá trị ban đầu là 1. Một tiến trình được phép truy cập vào một minode

chỉ khi nó giữ khóa semaphore. Lý do khóa minodes như sau.


Machine Translated by Google

Hệ thống tập tin 8.12 305

Giả sử rằng một tiến trình Pi cần inode của (dev, ino), không có trong bộ nhớ. Pi phải tải inode vào một mục minode. Minode
phải được đánh dấu là (dev, ino) để ngăn các quá trình khác tải lại cùng một nút. Trong khi tải inode từ đĩa, Pi có thể đợi hoàn
thành I/O, quá trình này sẽ chuyển sang một tiến trình khác Pj. Nếu Pj cần chính xác inode đó thì nó sẽ thấy rằng minode cần thiết
đã tồn tại. Nếu không có khóa minode, Pj sẽ tiếp tục sử dụng minode trước khi nó được tải vào. Với khóa, Pj phải đợi cho đến khi
minode được tải, sử dụng rồi được Pi giải phóng. Ngoài ra, khi một quá trình đọc/ghi một tệp đã mở, nó phải khóa minode của tệp
để đảm bảo rằng mỗi thao tác đọc/ghi là nguyên tử.

(4). Tệp phân bổ_deallocate.c: Tệp này chứa các chức năng tiện ích để phân bổ và giải phóng các minode, inode, khối đĩa và các mục
trong bảng tệp mở. Cần lưu ý rằng cả số khối inode và khối đĩa đều đếm từ 1. Do đó, trong bit bitmap i đại diện cho số inode/khối
i+1. (5). Tệp mount_root.c: Tệp này
chứa hàm mount_root(), được gọi trong quá trình khởi tạo hệ thống để gắn kết hệ thống tệp gốc. Nó đọc siêu khối của thiết bị gốc
để xác minh thiết bị là hệ thống tệp EXT2 hợp lệ. Nó tải INODE gốc (ino=2) vào một minode và đặt con trỏ gốc tới minode gốc. Sau
đó, nó mở khóa minode gốc để cho phép tất cả các tiến trình truy cập vào minode gốc. Một mục bảng gắn kết được phân bổ để ghi lại
hệ thống tập tin gốc được gắn kết. Một số tham số chính trên thiết bị gốc, chẳng hạn như các khối khởi đầu của bảng bitmap và
inodes, cũng được ghi lại trong bảng mount để tham khảo nhanh. (6). Tệp mkdir_creat.c: Tệp này lần lượt chứa các hàm mkdir và
create để tạo thư mục và
tạo tệp. mkdir và creat rất giống nhau nên chúng có chung một số mã. Trước khi thảo luận về các thuật toán của mkdir và creat,
trước tiên chúng tôi trình bày cách chèn/xóa mục nhập DIR vào/từ thư mục mẹ. Mỗi khối dữ liệu của một thư mục chứa các mục DIR
có dạng

|tên ino rlen nlen|tên ino rlen nlen| …

trong đó tên là một chuỗi các ký tự nlen không có byte NULL kết thúc. Vì mỗi mục nhập DIR bắt đầu bằng số inode u32 nên rec_len
của mỗi mục nhập DIR luôn là bội số của 4 (để căn chỉnh bộ nhớ). Mục nhập cuối cùng trong khối dữ liệu kéo dài khối còn lại, tức
là rec_len của nó là từ nơi mục nhập bắt đầu đến cuối khối. Trong mkdir và create, chúng tôi giả định như sau.

(Một). Một tệp DIR có tối đa 12 khối trực tiếp. Giả định này là hợp lý vì với kích thước khối 4 KB và tên tệp trung bình là 16 ký
tự, một DIR có thể chứa hơn 3000 mục nhập. Chúng tôi có thể cho rằng không có người dùng nào đặt nhiều mục như vậy vào một thư
mục. (b). Sau
khi được cấp phát, khối dữ liệu của DIR sẽ được giữ lại để sử dụng lại ngay cả khi nó trống.

Với những giả định này, thuật toán chèn và xóa như sau.

/*************** Thuật toán Insert_dir_entry ******************/

(1). need_len = 4*((8+name_len+3)/4); // mục mới cần độ dài

(2). đối với mỗi khối dữ liệu hiện có, hãy làm {

if (khối chỉ có một mục có số inode==0)

nhập mục mới làm mục nhập đầu tiên trong khối;

khác{

(3). đi đến mục cuối cùng trong khối;

lý tưởng_len = 4*((8+tên_entry_len+3)/4);

còn lại = rec_len của mục nhập cuối cùng - Ideal_len;

if (vẫn >= need_len){

cắt rec_len của mục cuối cùng thành Ideal_len;

nhập mục mới làm mục cuối cùng với rec_len = still;

(4). khác{

phân bổ một khối dữ liệu mới;

nhập mục mới làm mục nhập đầu tiên trong khối dữ liệu;
Machine Translated by Google

306 8 Hệ điều hành nhúng có mục đích chung

tăng kích thước của DIR lên BLKSIZE;

khối ghi vào đĩa;

(5). đánh dấu minode của DIR được sửa đổi để ghi lại;

/*************** Thuật toán Xóa_dir_entry (tên) *************/

(1). tìm kiếm (các) khối dữ liệu của DIR để tìm mục nhập theo tên;

(2). if (mục nhập là mục duy nhất trong khối)

xóa số inode của mục nhập về 0;

khác{

(3). if (mục nhập là mục cuối cùng trong khối)

thêm rec_len của mục nhập vào rec_len của mục nhập trước đó;

(4). else{ // mục ở giữa khối

thêm rec_len của mục nhập vào rec_len của mục nhập cuối cùng;

di chuyển tất cả các mục ở cuối sang trái để phủ lên mục đã xóa;

(5). ghi khối trở lại đĩa;

Lưu ý rằng trong thuật toán Delete_dir_entry, một khối trống không được giải phóng mà được giữ lại để sử dụng lại. Điều này ngụ ý rằng một

Kích thước của DIR sẽ không bao giờ giảm. Các sơ đồ thay thế được liệt kê trong phần Vấn đề dưới dạng bài tập lập trình.

8.12.4.1 mkdir-tạo-mknod
mkdir tạo một thư mục trống với khối dữ liệu chứa tệp . và .. mục. Thuật toán của mkdir là

/********* Thuật toán của mkdir **********/

int mkdir(char *tên đường dẫn)

1. if (tên đường dẫn là tuyệt đối) dev = root->dev;

khác dev = cwd->dev của PROC

2. chia tên đường dẫn thành dirname và basename;

3. // dirname phải tồn tại và là DIR:

pino = getino(&dev, dirname);

pmip = iget(dev, pino);

kiểm tra pmip->INODE là TRỰC TIẾP

4. // basename không được tồn tại trong DIR cha:

search(pmip, basename) phải trả về 0;

5. gọi kmkdir(pmip, basename) để tạo DIR; kmkdir() bao gồm 4 bước chính:

5-1. phân bổ INODE và khối đĩa:

ino = ialloc(dev); blk = balloc(dev); mip = iget(dev,ino); //

nạp INODE vào một minode 5-2. khởi tạo mip->INODE dưới dạng INODE TRỰC TIẾP;

mip->INODE.i_block[0] = blk; i_block[] khác là 0; đánh dấu minode đã sửa đổi (bẩn);

iput(mip); // ghi INODE trở lại đĩa

5-3. tạo khối dữ liệu 0 của INODE để chứa . và .. mục;

ghi vào khối đĩa blk.

5-4. enter_child(pmip, ino, tên cơ sở); đi vào

(ino, tên cơ sở) dưới dạng mục nhập TRỰC TIẾP cho INODE gốc;

6. tăng links_count của INODE gốc lên 1 và đánh dấu pmip dirty;

iput(pmip);

}
Machine Translated by Google

Hệ thống tập tin 8.12 307

Creat tạo một tập tin thông thường trống. Thuật toán tạo tương tự như mkdir. Thuật toán tạo như sau.

/****************** Thuật toán tạo () *******************/

tạo (char * tên đường dẫn)

Điều này tương tự như mkdir() ngoại trừ (1).

trường INODE.i_mode được đặt thành loại tệp REG, quyền

các bit được đặt thành 0644 cho rw-r--r-- và

(2). không có khối dữ liệu nào được phân bổ nên kích thước tệp là 0.

(3). Không tăng số lượng liên kết của INODE gốc

Cần lưu ý rằng thuật toán tạo ở trên khác với thuật toán trong Unix/Linux. Các quyền của tệp mới được đặt thành 0644 theo mặc
định và nó không mở tệp ở chế độ VIẾT và trả về bộ mô tả tệp. Trong thực tế, creat hiếm khi được sử dụng như một tòa nhà chọc trời
độc lập. Nó được sử dụng nội bộ bởi hàm kopen(), hàm này có thể tạo một tệp, mở tệp đó để VIẾT và trả về một bộ mô tả tệp. Hoạt
động mở sẽ được mô tả sau.
Mknod tạo một tệp đặc biệt đại diện cho thiết bị char hoặc khối có số thiết bị = (chính, phụ). Các
thuật toán của mknod là

/************ Thuật toán của mknod *************/ mknod(char *name,

int type, int device_number)

Điều này tương tự như create() ngoại trừ

(1). thư mục mẹ mặc định là /dev;

(2). INODE.i_mode được đặt thành loại tệp CHAR hoặc BLK;

(3). INODE.I_block[0] chứa device_number=(chính, phụ);

8.12.4.2 chdir-getcwd-stat Mỗi quy


trình có một Thư mục làm việc hiện tại (CWD), trỏ đến minode CWD của quy trình trong bộ nhớ. chdir (tên đường dẫn) thay đổi CWD
của một tiến trình thành tên đường dẫn. getcwd() trả về tên đường dẫn tuyệt đối của CWD. stat() trả về thông tin trạng thái của
tệp trong cấu trúc STAT. Thuật toán của chdir() là

/************ Thuật toán chdir *************/

int chdir(char *tên đường dẫn)

(1). lấy INODE của tên đường dẫn thành minode; (2). xác minh đó là

TRỰC TIẾP; (3). thay đổi tiến trình đang

chạy CWD thành minode của tên đường dẫn; (4). iput(CWD cũ); trả về 0 cho OK;

getcwd() được triển khai bằng đệ quy. Bắt đầu từ CWD, đưa INODE gốc vào bộ nhớ. Tìm kiếm tên của thư mục hiện tại trong khối dữ
liệu của INODE gốc và lưu chuỗi tên. Lặp lại thao tác cho INODE cha cho đến khi đạt đến thư mục gốc. Xây dựng tên đường dẫn tuyệt
đối của CWD khi trả về. Sau đó sao chép tên đường dẫn tuyệt đối vào không gian người dùng. stat(pathname, STAT *st) trả về thông
tin của tệp trong cấu trúc STAT. Thuật toán stat là

/********* Thuật toán stat *********/ int stat(char *pathname,

STAT *st) // st trỏ tới STAT struct

(1). lấy INODE của tên đường dẫn thành minode; (2). copy

(dev, ino) vào (st_dev, st_ino) của cấu trúc STAT trong Umode
Machine Translated by Google

308 8 Hệ điều hành nhúng có mục đích chung

(3). sao chép các trường khác của INODE sang cấu trúc STAT trong Umode;

(4). iput(minode); chạy lại 0 là OK;

8.12.4.3 rmdir Như

trong Unix/Linux, để rm một DIR, thư mục phải trống vì những lý do sau. Đầu tiên, việc xóa một thư mục không trống có nghĩa là xóa tất

cả các tệp và thư mục con trong thư mục đó. Mặc dù có thể triển khai thao tác rrmdir() để loại bỏ đệ quy toàn bộ cây thư mục, nhưng

thao tác cơ bản vẫn là xóa từng thư mục một.

Thứ hai, một thư mục không trống có thể chứa các tệp đang được sử dụng tích cực, ví dụ: được mở để đọc/ghi, v.v. Việc xóa một
thư mục như vậy rõ ràng là không thể chấp nhận được. Mặc dù có thể kiểm tra xem có bất kỳ tệp đang hoạt động nào trong một
thư mục hay không, nhưng nó sẽ tốn quá nhiều chi phí trong kernel. Cách đơn giản nhất là yêu cầu thư mục cần xóa phải trống.
Thuật toán của rmdir() là

/******** Thuật toán của rmdir **********/

rmdir(char *tên đường dẫn)

1. lấy INODE trong bộ nhớ của tên đường dẫn:

ino = getino(&de, pathanme);

mip = iget(dev,ino);

2. xác minh INODE là DIR (theo trường INODE.i_mode);

minode không BẬN (refCount = 1);

DIR trống (duyệt khối dữ liệu cho số mục = 2);

3. /* lấy ino và inode của cha */

pino = findino(); //lấy pino từ .. mục nhập trong INODE.i_block[0] pmip = iget(mip->dev, pino);

4. /* xóa tên khỏi thư mục mẹ */

findname(pmip, ino, name); // tìm tên từ DIR cha rm_child(pmip, name);

5. /* giải phóng các khối dữ liệu và inode của nó */

cắt ngắn(mip); // giải phóng các khối dữ liệu của INODE

6. giải phóng INODE

idalloc(mip->dev, mip->ino); iput(mip);

7. giảm liên kết gốc_count bằng 1;

đánh dấu cha mẹ là bẩn; iput(pmip);

8. trả về 0 cho THÀNH CÔNG.

8.12.4.4 link-unlink Tệp

link_unlikc.c thực hiện liên kết và hủy liên kết. link(old_file, new_file) tạo liên kết cứng từ new_file đến old_file. Liên kết cứng

chỉ có thể đến các tệp thông thường chứ không phải DIR, vì liên kết đến DIR có thể tạo ra các vòng lặp trong không gian tên hệ thống

tệp. Các tập tin liên kết cứng chia sẻ cùng một nút. Vì vậy, chúng phải ở trên cùng một thiết bị. Thuật toán liên kết là

/********* Thuật toán liên kết **********/

liên kết(tệp_cũ, tệp_mới)

1. // xác minh old_file tồn tại và không phải là TRỰC TIẾP; oino =

getino(&odev, old_file); omip = iget(odev, oino);

kiểm tra loại tệp (không thể là TRỰC

TIẾP).

2. // new_file phải chưa tồn tại:

nion = get(&ndev, new_file) phải trả về 0;

ndev của dirname(newfile) phải giống với odev


Machine Translated by Google

Hệ thống tập tin 8.12 309

3. // tạo mục nhập trong new_parent DIR với cùng ino

pmip -> minode của dirname(new_file); enter_name(pmip,

omip->ino, basename(new_file)); 4. omip->INODE.i_links_count++;

omip->bẩn = 1; iput(omip);

iput(pmip);

hủy liên kết giảm links_count của tệp đi 1 và xóa tên tệp khỏi DIR gốc của nó. Khi links_count của một tệp đạt đến 0, tệp sẽ thực sự bị xóa bằng cách

hủy phân bổ các khối dữ liệu và nút của nó. Thuật toán hủy liên kết() là

/*********** Thuật toán hủy liên kết **********/

hủy liên kết(char *tên tệp)

1. lấy minode của filenmae: ino =

getino(&dev, filename); mip = iget(dev, ino);

kiểm tra xem đó có phải là tệp REG hoặc SLINK không

2. // xóa tên cơ sở khỏi DIR cha

rm_child(pmip, mip->ino, tên cơ sở); pmip->bẩn = 1;

iput(pmip);

3. // giảm link_count của INODE

mip->INODE.i_links_count--;

if (mip->INODE.i_links_count > 0){

mip->bẩn = 1; iput(mip);

4. if (!SLINK file) // giả sử:Tệp SLINK không có khối dữ liệu

cắt ngắn(mip); // giải phóng tất cả các khối dữ liệu

giải phóng INODE;

iput(mip);

8.12.4.5 symlink-readlink

symlink(old_file, new_file) tạo một liên kết tượng trưng từ new_file đến old_file. Không giống như các liên kết cứng, liên kết tượng trưng có thể

liên kết đến bất kỳ thứ gì, kể cả DIR hoặc các tệp không nằm trên cùng một thiết bị. Thuật toán của liên kết tượng trưng là

Thuật toán symlink(old_file, new_file)

1. kiểm tra: old_file phải tồn tại và new_file chưa tồn tại;

2. tạo new_file; thay đổi new_file thành loại SLINK;

3. // giả sử độ dài của tên old_file <= 60 ký tự

lưu trữ tên old_file trong vùng INODE.i_block[] của newfile.

đánh dấu minode của new_file là bẩn;

iput(minode của new_file); 4. đánh

dấu new_file minode cha bị bẩn; iput(minode gốc của

new_file);

readlink(file, buffer) đọc tên tệp đích của tệp SLINK và trả về độ dài của tên tệp đích. Thuật toán của readlink() là
Machine Translated by Google

310 8 Hệ điều hành nhúng có mục đích chung

Thuật toán readlink (file, buffer)

1. lấy INODE của tập tin vào bộ nhớ; xác minh đó là tệp SLINK 2. sao chép tên tệp mục tiêu

trong INODE.i_block vào bộ đệm; 3. return strlen((char *)mip->INODE.i_block);

8.12.4.6 Các chức năng cấp 1 khác

Các chức năng cấp 1 khác bao gồm stat, access, chmod, chown, touch, v.v. Hoạt động của tất cả các chức năng đó đều có cùng
một mẫu:

(1). lấy INODE trong bộ nhớ của một tệp bằng ino = getinod(&dev,

pathname); mip = iget(dev,ino); (2). lấy thông

tin từ INODE hoặc sửa đổi INODE;

(3). nếu INODE được sửa đổi, hãy đánh dấu nó TRỰC TIẾP để ghi lại;

(4). iput(mip);

8.12.5 Thực hiện FS cấp 2

Cấp độ 2 của FS thực hiện các thao tác đọc/ghi nội dung tệp. Nó bao gồm các chức năng sau: mở, đóng, lseek, đọc, viết,
opendir và readdir.

8.12.5.1 open-close-lseek Tệp

open_close_lseek.c thực hiện open(), close() và lseek(). Cuộc gọi hệ thống

int open(char *tên file, cờ int);

mở một tệp để đọc hoặc ghi, trong đó flags = 0|1|2|3|4 tương ứng với R|W|RW|APPEND. Ngoài ra, các cờ cũng có thể được chỉ
định là một trong các hằng số ký hiệu O_RDONLY, O_WRONLY, O_RDWR, có thể được chỉnh sửa theo bit hoặc chỉnh sửa bằng các
cờ tạo tệp O_CREAT, O_APPEND, O_TRUNC. Các hằng số ký hiệu này được định nghĩa trong type.h. Nếu thành công, open() trả về
một bộ mô tả tệp cho các lệnh gọi hệ thống read()/write() tiếp theo. Thuật toán open() là

/************** Thuật toán open() ***********/ int open(file, flags)

1. lấy minode của tệp: ino =

getino(&dev, file);

if (ino==0 && O_CREAT){

tạo (tệp); ino = getino(&dev, file);

mip = iget(dev, ino);

2. kiểm tra quyền truy cập tệp INODE;

đối với tệp không đặc biệt, hãy kiểm tra các chế độ mở không tương thích;

3. phân bổ một mục openTable;

khởi tạo các mục openTable;

đặt byteOffset = 0 cho R|W|RW; đặt thành kích thước tệp cho chế độ PHỤ LỤC;

4. Tìm kiếm mục nhập fd[ ] MIỄN PHÍ có chỉ mục fd thấp nhất trong PROC;

hãy để fd[fd] trỏ đến mục openTable;

5. mở khóa minode;

trả về fd làm bộ mô tả tập tin;

}
Machine Translated by Google

Hệ thống tập tin 8.12 311

Hình 8.6 hiển thị cấu trúc dữ liệu được tạo bởi open(). Trong hình, (1) là cấu trúc PROC của tiến trình gọi open(). Bộ mô
tả tệp được trả về, fd, là chỉ mục của mảng fd[] trong cấu trúc PROC. Nội dung của fd[fd] trỏ đến OFT, trỏ tới minode của tệp.
Số refCount của OFT biểu thị số lượng tiến trình chia sẻ cùng một phiên bản của một tệp đã mở. Khi một tiến trình lần đầu tiên
mở một tệp, nó sẽ đặt refCount trong OFT thành 1. Khi một tiến trình phân nhánh, nó sẽ sao chép tất cả các bộ mô tả tệp đã mở
sang tiến trình con, để tiến trình con chia sẻ tất cả các bộ mô tả tệp đã mở với tiến trình cha, điều này tăng refCount của mỗi
OFT được chia sẻ lên 1. Khi một quá trình đóng bộ mô tả tệp, nó sẽ giảm refCount của OFT đi 1 và xóa mục nhập fd[] của nó thành
0. Khi refCount của OFT đạt đến 0, nó sẽ gọi iput() để loại bỏ minode và giải phóng OFT. Phần bù của OFT là một con trỏ khái
niệm tới vị trí byte hiện tại trong tệp để đọc/ghi. Nó được khởi tạo bằng 0 cho chế độ R|W|RW hoặc kích thước tệp cho chế độ
PHỤ LỤC.
Trong EOS, lseek(fd, location) đặt offset trong OFT của bộ mô tả tệp đã mở thành vị trí byte tương ứng với phần đầu của
tệp. Sau khi được đặt, lần đọc/ghi tiếp theo sẽ bắt đầu từ vị trí bù hiện tại. Thuật toán của lseek() rất tầm thường.
Đối với các tệp được mở để ĐỌC, nó chỉ kiểm tra giá trị vị trí để đảm bảo nó nằm trong giới hạn [0, file_size]. Nếu fd là một
tệp thông thường được mở để WRITE, lseek cho phép độ lệch byte vượt quá kích thước tệp hiện tại nhưng nó không phân bổ bất
kỳ khối đĩa nào cho tệp. Các khối đĩa sẽ được phân bổ khi dữ liệu thực sự được ghi vào tệp. Thuật toán đóng một bộ mô tả tập
tin là

/*************** Thuật toán đóng() *******************/

int đóng(int fd)

(1). kiểm tra fd có phải là bộ mô tả tệp đã mở hợp lệ không;

(2). if (fd[fd] != 0 của PROC){

(3). if (chế độ của openTable == ĐỌC/ghi PIPE)

trả về close_pipe(fd); // đóng bộ mô tả đường ống;

(4). if (--refCount == 0){ // if quá trình cuối cùng sử dụng OFT này

khóa(minodeptr);

iput(minode); // giải phóng minode

(5). xóa fd[fd] = 0; // xóa fd[fd] về 0

(6). trả lại THÀNH CÔNG;

Hình 8.6 Cấu trúc dữ liệu của open()


Machine Translated by Google

312 8 Hệ điều hành nhúng có mục đích chung

8.12.5.2 Đọc các tệp thông


thường Lệnh gọi hệ thống int read(int fd, char buf[], int nbytes) đọc nbyte từ bộ mô tả tệp đã mở vào vùng đệm trong
không gian người dùng. read() gọi kread() trong kernel, thực hiện lệnh gọi hệ thống đọc. Thuật toán của kread() là

/****************** Thuật toán kread() trong kernel ****************/ int kread(int fd, char buf[ ], int

nbyte, int space) //space=K|U

(1). xác nhận fd; đảm bảo thường xuyên được mở cho ĐỌC hoặc RW;

(2). nếu (oft.mode = READ_PIPE)

trả về read_pipe(fd, buf, nbytes);

(3). if (minode.INODE là một tệp đặc biệt)

trả về read_special(device,buf,nbytes);

(4). (tệp thông thường):

trả về read_file(fd, buf, nbytes, space);

/*************** Thuật toán đọc file thông thường ****************/ int read_file(int fd, char *buf, int

nbytes , int không gian)

(1). khóa minode;

(2). đếm = 0; avil = fileSize - offset;

(3). trong khi (nbyte){

tính toán khối logic: lbk = offset/BLKSIZE;

byte bắt đầu trong khối: start = offset % BLKSIZE;

(4). chuyển đổi số khối logic, lbk, thành số khối vật lý,

blk, thông qua mảng INODE.i_block[];

(5). read_block(dev, blk, kbuf); // đọc blk vào kbuf[BLKSIZE];

char *cp = kbuf + bắt đầu;

còn lại = BLKSIZE - bắt đầu;

(6) while (vẫn còn){// sao chép byte từ kbuf[ ] sang buf[ ]

(không gian)? put_ubyte(*cp++, *buf++) : *buf++ = *cp++;

bù đắp++; đếm++; // inc offset, count;

duy trì--; ác--; nbyte--; // còn lại tháng 12, avil, nbyte;

nếu (nbytes==0 || tận dụng==0)

phá vỡ;

(7). mở khóa minode;

(số 8). số lần trả lại;

Thuật toán read_file() có thể được giải thích tốt nhất theo Hình 8.7. Giả sử rằng fd được mở cho READ. Phần bù trong
OFT trỏ đến vị trí byte hiện tại trong tệp mà chúng ta muốn đọc nbyte. Đối với kernel, một tệp chỉ là một chuỗi các byte
liền kề (về mặt logic), được đánh số từ 0 đến fileSize-1. Như Hình 8.7 cho thấy, vị trí byte hiện tại, offset, nằm
trong một khối logic, lbk = offset /BLKSIZE, byte để bắt đầu đọc là start = offset % BLKSIZE và số byte còn lại trong
khối logic vẫn = BLKSIZE bắt đầu. Tại thời điểm này, tệp có sẵn = fileSize offset byte vẫn có sẵn để đọc. Những
con số này được sử dụng trong thuật toán read_file. Trong EOS, kích thước khối là 4 KB và các tệp có tối đa gấp đôi
khối gián tiếp.

Thuật toán chuyển đổi số khối logic thành số khối vật lý để đọc là

/* Thuật toán chuyển khối logic thành khối vật lý */ u32 map(INODE, lbk){

// chuyển lbk sang blk

nếu (lbk < 12) // khối trực tiếp

blk = INODE.i_block[lbk];

else if (12 <= lbk < 12+256){ // khối gián tiếp


Machine Translated by Google

Hệ thống tập tin 8.12 313

Hình 8.7 Cấu trúc dữ liệu cho Read_file()

đọc INODE.i_block[12] thành u32 ibuf[256];

blk = ibuf[lbk-12];

khác{ // khối gián tiếp kép

đọc INODE.i_block[13] thành u32 dbuf[256];

lbk -= (12+256);

dblk = dbuf[lbk / 256];

đọc dblk vào dbuf[ ];

blk = dbuf[lbk % 256];

trả lại blk;

8.12.5.3 Viết tệp thông thường


Lệnh gọi hệ thống int write(int fd, char ubuf[], int nbytes) ghi nbyte từ ubuf trong không gian người dùng vào bộ
mô tả tệp đã mở và trả về số byte thực tế đã ghi. write() gọi kwrite() trong kernel, thực hiện lệnh gọi hệ thống ghi.
Thuật toán của kwrite() là

/************** Thuật toán kwrite trong kernel *************/ int kwrite(int fd, char *ubuf,

int nbytes)

(1). xác nhận fd; đảm bảo OFT được mở để ghi;

(2). nếu (oft.mode = WRITE_PIPE)

trả về write_pipe(fd, buf, nbytes);

(3). if (minode.INODE là một tệp đặc biệt)

return write_special(device,buf.nbytes);

(4). trả về write_file(fd, ubuf, nbytes);

Thuật toán write_file() có thể được giải thích tốt nhất theo Hình 8.8.
Trong hình 8.8, offset trong OFT là vị trí byte hiện tại trong tệp để ghi. Như trong read_file(), đầu tiên nó tính
số khối logic, lbk, vị trí byte bắt đầu và số byte còn lại trong khối logic. Nó chuyển đổi
Machine Translated by Google

314 8 Hệ điều hành nhúng có mục đích chung

Hình 8.8 Cấu trúc dữ liệu cho write_file()

khối logic thành khối vật lý thông qua mảng INODE.i_block của tệp. Sau đó, nó đọc khối vật lý vào bộ đệm, ghi dữ liệu
vào đó và ghi bộ đệm trở lại đĩa. Phần sau đây trình bày thuật toán write_file().

/*************** Thuật toán ghi file thông thường ****************/

int write_file(int fd, char *ubuf, int nbytes)

(1). khóa minode;

(2). đếm = 0; // số byte được ghi

(3). trong khi (nbyte){

tính toán khối logic: lbk = oftp->offset / BLOCK_SIZE;

tính toán byte bắt đầu: start = oftp->offset % BLOCK_SIZE;

(4). chuyển đổi lbk sang số khối vật lý, blk; read_block(dev, blk,

(5). kbuf); //đọc blk vào kbuf[BLKSIZE]; char *cp = kbuf + bắt đầu; còn lại = BLKSIZE - bắt đầu;

while (vẫn còn){ // sao chép byte từ kbuf[ ] sang ubuf[ ]

(6)

put_ubyte(*cp++, *ubuf++);

bù đắp++; đếm++; // inc offset, count;

duy trì --; nbyte--; // còn lại tháng 12, nbyte;

if (offset > fileSize) fileSize++; // tăng kích thước tập tin

nếu (nbyte <= 0) phá vỡ;

(7). wrtie_block(dev, blk, kbuf);

(số 8). đặt minode bẩn = 1; // đánh dấu minode bẩn cho iput()

mở khóa(minode);

số lần trả lại;

Thuật toán chuyển khối logic thành khối vật lý để ghi tương tự như thuật toán đọc, ngoại trừ điểm khác biệt sau.
Trong quá trình ghi, khối dữ liệu mong muốn có thể chưa tồn tại. Nếu khối trực tiếp không tồn tại, nó phải được cấp
phát và ghi vào INODE. Nếu khối gián tiếp không tồn tại thì phải cấp phát và khởi tạo về 0. Nếu khối dữ liệu gián tiếp
không tồn tại thì phải cấp phát và ghi vào khối gián tiếp, v.v. Người đọc có thể tham khảo file write.c để biết chi tiết .
Machine Translated by Google

Hệ thống tập tin 8.12 315

8.12.5.4 Đọc-Ghi các tệp đặc biệt


Trong kread() và kwrite(), các đường dẫn đọc/ghi và các tệp đặc biệt được xử lý khác nhau. Đường ống đọc/ghi được thực hiện trong đường ống

cơ chế trong hạt nhân EOS. Ở đây chúng tôi chỉ xem xét việc đọc/ghi các tệp đặc biệt. Mỗi tệp đặc biệt có tên tệp trong thư mục/dev

danh mục. Loại tệp trong inode của tệp đặc biệt được đánh dấu là đặc biệt, ví dụ: 0060000 = thiết bị khối, 0020000 = thiết bị char,

v.v. Vì một tệp đặc biệt không có bất kỳ khối đĩa nào, i_block[0] của INODE của nó sẽ lưu trữ số (chính, phụ) của thiết bị,

trong đó chính = loại thiết bị và phụ = số đơn vị của loại thiết bị đó. Ví dụ: /dev/sdc0 = (3,0) đại diện cho

toàn bộ SDC, /dev/sdc1 = (3,1) đại diện cho phân vùng đầu tiên của SDC, /dev/tty0 = (4,0) và /dev/ttyS1 = (5,1), v.v.

số thiết bị chính là một chỉ mục trong bảng chuyển đổi thiết bị, dev_sw[], chứa các con trỏ tới các chức năng của trình điều khiển thiết bị, như trong

cấu trúc dev_sw {

int (*dev_read)();

int (*dev_write)();

} dev_sw[];

Giả sử rằng int nocall(){} là một hàm trống và

sdc_read(), sdc_write(), console_read(), // Đọc/ghi SDC

console_write(), // đọc/ghi bảng điều khiển

serial_read(), serial_write(), // đọc/ghi cổng serail

là các chức năng điều khiển thiết bị. Bảng chuyển đổi thiết bị được thiết lập để chứa các con trỏ hàm trình điều khiển.

struct dev_sw dev_sw[ ] =

{ // đọc viết

//-------- --------

không gọi, không gọi, // 0=/dev/null

không gọi, không gọi, // 1=bộ nhớ hạt nhân

không gọi, không gọi, // 2=FD (không có FD trong EOS)

sdc_read, sdc_write, // 3=SDC

console_read, console_write, // 4=console

nối tiếp_đọc, serial_write // 5=cổng nối tiếp

};

Sau đó đọc/ghi một tập tin đặc biệt sẽ trở thành

(1). lấy số (chính, phụ) của tệp đặc biệt từ INODE.i_block[0];

(2). return (*dev_sw[major].dev_read) (phụ, tham số); //ĐỌC

HOẶC return (*dev_sw[major].dev_write)(minor, tham số); //VIẾT

(2). gọi hàm trình điều khiển thiết bị tương ứng, truyền tham số số thiết bị phụ và các thông số khác

các thông số khi cần thiết. Bảng chuyển đổi thiết bị là một kỹ thuật tiêu chuẩn được sử dụng trong tất cả các hệ thống giống Unix. Nó không chỉ làm cho

Cấu trúc hệ thống con I/O rõ ràng nhưng cũng làm giảm đáng kể kích thước mã đọc/ghi.

8.12.5.5 Opendir-Readdir
Unix coi mọi thứ là một tập tin. Do đó, chúng ta có thể mở DIR để đọc giống như một tệp thông thường. Từ một

Theo quan điểm kỹ thuật, không cần có một bộ hàm opendir() và readdir() riêng biệt. Tuy nhiên, giống Unix khác

hệ thống có thể có hệ thống tập tin khác nhau. Người dùng có thể khó diễn giải nội dung của tệp DIR. Vì lý do này,

POSIX chỉ định các hoạt động opendir và readdir, độc lập với hệ thống tệp. Hỗ trợ cho opendir là chuyện nhỏ; đó là

cùng một lệnh gọi hệ thống mở, nhưng readdir() có dạng

struct dirent ep ¼ readdir DIR ð Þ dp ;


Machine Translated by Google

316 8 Hệ điều hành nhúng có mục đích chung

trả về một con trỏ tới cấu trúc trực tiếp trên mỗi cuộc gọi. Điều này có thể được triển khai trong không gian người dùng dưới dạng chức năng I/O của thư viện.

Vì EOS chưa hỗ trợ các luồng I/O cấp người dùng theo các chức năng thư viện nên chúng tôi sẽ triển khai opendir() và readir() dưới dạng lệnh

gọi hệ thống.

int opendir(pathaname) { return

open(pathname, O_RDONLY|O_DIR); }

trong đó O_DIR là mẫu bit để mở tệp dưới dạng DIR. Trong bảng tệp đang mở, trường chế độ chứa bit O_DIR, được sử dụng để định tuyến các tòa nhà

chọc trời readdir tới hàm kreaddir().

int kreaddir(int fd, struct udir *dp) // struct udir{DIR; tên[256]};

// giống như kread() trong kernel ngoại trừ: sử dụng byte

offset hiện tại trong OFT để đọc bản ghi DIR tiếp theo; sao chép bản ghi DIR vào *udir trong không gian

Người dùng;

bù đắp trước bởi rec_len của mục nhập DIR;

Các chương trình ở chế độ người dùng phải sử dụng lệnh gọi hệ thống readdir(fd, struct udir *dir) thay vì lệnh gọi readdir(DIR *dp).

8.12.6 Thực hiện FS cấp 3

Cấp độ 3 của FS thực hiện gắn kết và umount hệ thống tệp và bảo vệ tệp.

8.12.6.1 mount-umount Lệnh mount,

mount filesys mount_point, gắn hệ thống tập tin vào thư mục mount_point. Nó cho phép hệ thống tệp bao gồm các hệ thống tệp khác như một phần của

hệ thống tệp hiện có. Cấu trúc dữ liệu được sử dụng trong mount là bảng MOUNT và minode trong bộ nhớ của thư mục mount_point. Thuật toán gắn kết

/************** Thuật toán mount ****************/ mount() // Cách sử dụng: mount

[filesys mount_point]

1. Nếu không có tham số, hiển thị hệ thống tập tin được gắn hiện tại; 2. Kiểm tra xem filesys đã

được mount chưa:

Các mục trong bảng MOUNT chứa tên hệ thống tệp (thiết bị) được gắn và các điểm gắn của chúng. Từ chối nếu thiết

bị đã được gắn.

Nếu không, hãy phân bổ một mục nhập bảng MOUNT miễn phí.

3. filesys là một tệp đặc biệt có số thiết bị dev=(major,minor).

Đọc siêu khối của filesys để xác minh đó là EXT2 FS.

4. tìm ino và sau đó là minode của mount_point:

gọi ino = get_ino(&dev, pathname); để nhận được thông tin:

gọi mip = iget(dev, ino); để tải inode của nó vào bộ nhớ;

5. Kiểm tra mount_point có phải là TRỰC TIẾP và không bận, ví dụ: không phải CWD của ai đó.

6. Ghi lại tên nhà phát triển và tập tin vào mục nhập bảng MOUNT;

Ngoài ra, hãy lưu trữ các ninode, nblocks, v.v. của nó để tham khảo nhanh.

7. Đánh dấu minode của mount_point là được gắn trên (cờ được gắn = 1) và để

nó trỏ vào mục trong bảng MOUNT, mục này trỏ trở lại mục

minode mount_point.

Hoạt động Umount filesys tách hệ thống tệp được gắn khỏi điểm gắn của nó, trong đó filesys có thể là tên tệp đặc biệt hoặc tên thư mục điểm

gắn. Thuật toán của umount là


Machine Translated by Google

Hệ thống tập tin 8.12 317

/********************* Thuật toán umount *******************/

umount(char *filesys)

1. Tìm kiếm trong bảng MOUNT để kiểm tra xem tập tin đã thực sự được gắn kết chưa.

2. Kiểm tra (bằng cách kiểm tra tất cả các minode[].dev đang hoạt động) xem có tập tin nào bị

hoạt động trong các tập tin được gắn kết; Nếu vậy, hãy từ chối;

3. Tìm inode trong bộ nhớ của mount_point, nút này sẽ nằm trong bộ nhớ khi nó được gắn vào. Đặt lại cờ

gắn của minode về 0; sau đó iput() là minode.

8.12.6.2 Ý nghĩa của mount Mặc dù việc

thực hiện mount và umount rất dễ dàng nhưng vẫn có những hàm ý. Với mount, chúng ta phải sửa đổi hàm get_ino(&dev, pathname) để
hỗ trợ vượt qua các điểm gắn kết. Giả sử rằng một hệ thống tập tin, newfs, đã được gắn vào thư mục /a/b/c/. Khi duyệt qua một tên
đường dẫn, việc vượt qua điểm gắn kết có thể xảy ra theo cả hai hướng.

(1). Truyền tải xuống: Khi duyệt qua tên đường dẫn /a/b/c/x, khi chúng ta đến minode của /a/b/c, chúng ta sẽ thấy rằng minode đã
được gắn vào (cờ được gắn = 1). Thay vì tìm kiếm x trong INODE của /a/b/c, chúng ta phải
. Đi theo con trỏ mountTable của minode để xác định mục nhập bảng gắn kết.
. Từ số dev của newfs, lấy INODE gốc (ino = 2) của nó vào bộ nhớ.
. Sau đó tiếp tục tìm kiếm x dưới INODE gốc của newfs.

(2). Truyền tải lên trên: Giả sử rằng chúng ta đang ở thư mục /a/b/c và di chuyển lên trên, ví dụ cd ../../, sẽ vượt qua điểm gắn
kết /a/b/c. Khi đến INODE gốc của hệ thống file được mount, chúng ta sẽ thấy đó là thư mục gốc (ino = 2) nhưng số dev của nó khác
với root thực nên chưa phải là root thực. Sử dụng số nhà phát triển của nó, chúng ta có thể xác định mục nhập bảng gắn kết của nó,
mục này trỏ đến minode được gắn kết của /a/b/c/. Sau đó, chúng ta chuyển sang minode của /a/b/c và tiếp tục di chuyển lên trên. Vì
vậy, việc vượt qua đỉnh núi cũng giống như con khỉ hay con sóc hy vọng từ cây này sang cây khác rồi lại quay về.

8.12.6.3 Bảo vệ tệp Trong

Unix, bảo vệ tệp bằng cách kiểm tra quyền. INODE của mỗi tệp có trường i_mode, trong đó 9 bit thấp dành cho quyền truy cập tệp. 9
bit quyền là

nhóm chủ sở hữu khác


----- ----- -----

rwx rwx rwx


------ ------ -----

trong đó 3 bit đầu tiên áp dụng cho chủ sở hữu tệp, 3 bit thứ hai áp dụng cho người dùng trong cùng nhóm với chủ sở hữu và 3 bit
cuối cùng áp dụng cho tất cả những người khác. Đối với các thư mục, bit x cho biết liệu một tiến trình có được phép đi vào thư
mục hay không. Mỗi tiến trình có một uid và một gid. Khi một tiến trình cố gắng truy cập một tệp, hệ thống tệp sẽ kiểm tra uid và
gid của tiến trình dựa trên các bit quyền của tệp để xác định xem nó có được phép truy cập tệp với chế độ hoạt động dự kiến hay
không. Nếu quá trình không có quyền phù hợp thì nó không được phép truy cập vào tệp. Để đơn giản, EOS bỏ qua gid. Nó chỉ sử dụng
uid tiến trình để kiểm tra quyền truy cập tệp.

8.12.6.4 uid thực và hiệu quả Trong

Unix, một tiến trình có uid thực và uid hiệu quả. Hệ thống tập tin kiểm tra quyền truy cập của một tiến trình bằng uid hiệu quả của nó.
Trong điều kiện bình thường, uid hiệu dụng và uid thực giống hệt nhau. Khi một quy trình thực thi chương trình setuid có bit
setuid (bit 11) trong trường i_mode của tệp được bật, uid hiệu dụng của quy trình sẽ trở thành uid của chương trình. Trong khi
thực thi một chương trình setuid, quy trình sẽ trở thành chủ sở hữu của chương trình một cách hiệu quả. Ví dụ: khi một tiến
trình thực thi chương trình thư, là chương trình setuid thuộc sở hữu của siêu người dùng, nó có thể ghi vào tệp thư của người dùng khác.
Khi một tiến trình kết thúc việc thực thi chương trình setuid, nó sẽ quay trở lại uid thực. Vì lý do đơn giản, EOS chưa hỗ trợ
uid hiệu quả. Kiểm tra quyền dựa trên uid thực.
Machine Translated by Google

318 8 Hệ điều hành nhúng có mục đích chung

8.12.6.5 Khóa tệp Khóa


tệp là một cơ chế cho phép một quá trình đặt khóa trên một tệp hoặc các phần của tệp để ngăn chặn tình trạng tương
tranh khi cập nhật tệp. Khóa tệp có thể được chia sẻ, cho phép đọc đồng thời hoặc độc quyền, thực thi ghi độc quyền.
Khóa tập tin cũng có thể là bắt buộc hoặc mang tính tư vấn. Ví dụ: Linux hỗ trợ cả khóa tệp được chia sẻ và độc quyền nhưng việc khóa tệp chỉ

mang tính chất tư vấn. Trong Linux, khóa tệp có thể được đặt bằng lệnh gọi hệ thống fcntl() và được thao tác bằng lệnh gọi hệ thống fcntl().

Trong EOS, việc khóa tệp chỉ được thực thi trong tòa nhà open() của các tệp không đặc biệt. Khi một quá trình cố gắng mở một tệp không đặc

biệt, chế độ hoạt động dự định sẽ được kiểm tra tính tương thích. Các chế độ tương thích duy nhất là READ. Nếu một tập tin đã được mở ở chế

độ cập nhật, tức là W|RW|APPEND, thì không thể mở lại được. Điều này không áp dụng cho các tập tin đặc biệt, ví dụ như thiết bị đầu cuối. Một

tiến trình có thể mở terminal của nó nhiều lần ngay cả khi các chế độ không tương thích. Điều này là do quyền truy cập vào các tệp đặc biệt

cuối cùng được kiểm soát bởi trình điều khiển thiết bị.

Các thao tác với tệp từ chế độ Người dùng đều dựa trên các lệnh gọi hệ thống. Tính đến thời điểm hiện tại, EOS chưa hỗ trợ tệp thư viện I/O
các chức năng trên luồng tập tin.

8.13 Chặn bộ đệm I/O của thiết bị

Hệ thống tệp EOS sử dụng bộ đệm I/O cho thiết bị khối (SDC) để cải thiện hiệu quả của các hoạt động I/O tệp. Bộ đệm I/O được triển khai trong

tệp buffer.c. Khi EOS khởi động, nó gọi binit() để khởi tạo 256 bộ đệm I/O. Mỗi bộ đệm I/O bao gồm một tiêu đề để quản lý bộ đệm và vùng dữ liệu

4 KB cho một khối dữ liệu SDC. Vùng dữ liệu 1 MB của bộ đệm I/O được phân bổ trong 4 MB–5 MB của bản đồ bộ nhớ hệ thống EOS. Mỗi bộ đệm (tiêu

đề) có một semaphore khóa để truy cập độc quyền vào bộ đệm. Thuật toán quản lý bộ đệm như sau.

8.14 Thuật toán quản lý bộ đệm I/O

(1). bfreelist = danh sách bộ đệm miễn phí. Ban đầu tất cả các bộ đệm đều nằm trong danh

sách bfreelist. (2). dev_tab = bảng thiết bị cho phân vùng SDC. Nó chứa số id thiết bị, số khu vực bắt đầu và kích thước theo số lượng khu

vực. Nó cũng chứa hai danh sách liên kết đệm. dev_list chứa tất cả các bộ đệm I/O được gán cho thiết bị, mỗi bộ đệm được xác định bằng số (dev,

blk) của bộ đệm. Hàng đợi I/O của thiết bị chứa các bộ đệm cho I/O đang chờ xử lý.

(3). Khi một tiến trình cần đọc dữ liệu khối SDC, nó sẽ gọi

bộ đệm cấu trúc *bread(dev, blk)

bộ đệm cấu trúc *bp = getblk(dev, blk); // lấy bp =(dev,blk) if (dữ liệu bp không hợp lệ){

đánh dấu bp cho ĐỌC

start_io(bp); // khởi động I/O trên bộ đệm

P(&bp->iodon); //chờ hoàn tất I/O

trả lại bp;

(4). Sau khi đọc dữ liệu từ bộ đệm, tiến trình sẽ giải phóng bộ đệm bằng brelse(bp). Bộ đệm được giải phóng vẫn còn trong danh sách thiết bị để

có thể tái sử dụng. Nó cũng nằm trong bfreelist nếu không được sử dụng.

(5). Khi một tiến trình ghi dữ liệu vào khối SDC, nó sẽ gọi

int bwrite(dev, blk)

bộ đệm cấu trúc *bp;

if (viết khối mới hoặc khối hoàn chỉnh)

bp = getblk(dev, blk); // lấy bộ đệm cho (dev,blk)

khác // ghi vào khối hiện có // lấy bộ đệm

bp = bánh mì(dev,blk); với dữ liệu hợp lệ


Machine Translated by Google

8.14 Thuật toán quản lý bộ đệm I/O 319

ghi dữ liệu vào bp;

đánh dấu dữ liệu bp hợp lệ và bẩn (đối với việc ghi lại bị trì hoãn)

brelse(bp); // giải phóng bp

(6). Bộ đệm bẩn chứa dữ liệu hợp lệ, có thể được đọc/ghi bởi bất kỳ quy trình nào. Một bộ đệm bẩn chỉ được ghi trở lại SDC khi
nó được gán lại cho một khối khác, tại thời điểm đó, nó được ghi ra bởi

awrite(struct buffer *bp) // để ghi ASYNC

đánh dấu bp ghi ASYNC;

start_io(bp); //đừng đợi hoàn thành

Khi thao tác ghi ASYNC hoàn tất, bộ xử lý ngắt SDC sẽ tắt ASYNC của bộ đệm và các cờ bẩn và
giải phóng bộ đệm.

int start_io(struct buf *bp) // bắt đầu I/O trên bp

int ps = int_off(); nhập bp

vào dev_tab.IOqueue; if (bp đứng đầu trong

dev_tab.IOqueue){

if(bp dành cho ĐỌC)

get_block(bp->blk, bp->buf);

khác // VIẾT

put_block(bp->blk, bp->buf);

int_on(ps);

(7). Trình xử lý ngắt SDC:

bp = dequeue(dev_tab.IOqueue);

nếu (bp==ĐỌC){

đánh dấu dữ liệu bp hợp lệ;

V(&bp->iodon); // bỏ chặn tiến trình đang chờ trên bp

khác{

tắt cờ bp ASYNC

brelse(bp);

bp = dev_tab.IOqueue;

if (bp){ // Hàng đợi I/O không trống

nếu (bp==ĐỌC)

get_block(bp->blk, bp->buf);

khác

put_block(bp->blk, bp->buf);

}
Machine Translated by Google

320 8 Hệ điều hành nhúng có mục đích chung

(số 8). getblk() và brelse() tạo thành cốt lõi của quản lý bộ đệm. Sau đây liệt kê các thuật toán của getblk() và
brelse(), sử dụng các ngữ nghĩa để đồng bộ hóa.

SEMAPHORE freebuf = NBUF; // đếm semaphore

Mỗi bộ đệm có khóa SEMAPHOREs = 1; io_done = 0;

struct buf *getblk(int dev, int blk)

struct buf *bp;

trong khi(1){

P(&freebuf); // nhận được một buf miễn phí

bp = search_dev(dev,blk);

nếu (bp){ // buf trong bộ đệm

lượt truy cập++; // số lần truy cập bộ đệm

nếu (bp->bận){ // nếu buf bận // bp

V(&freebuf); không có trong danh sách tự do, hãy từ bỏ buf miễn phí // đợi bp

P(&bp->khóa);

trả lại bp;

// bp trong bộ đệm và không bận bp->busy =

1; // đánh dấu bp bận out_freelist(bp);

P(&bp->khóa); trả // khóa bp

lại bp;

// buf không có trong bộ đệm; đã có sẵn buff miễn phí trong tay

kho a();

bp = danh sách tự do;

freelist = freelist->next_free;

mở khóa();

P(&bp->khóa); // khóa bộ đệm

nếu (bp->bẩn){ // viết chậm buf, không dùng được

viết (bp);

Tiếp tục; // tiếp tục vòng lặp while(1)

// bp là bộ đệm mới; gán lại nó cho (dev, blk)

if (bp->dev != dev){ if (bp->dev

>= 0)

out_devlist(bp);

bp->dev = dev;

enter_devlist(bp);

bp->dev = dev; bp->blk = blk; bp->hợp lệ = 0;

bp->async = 0; bp->bẩn = 0; trả lại bp;

int brelse(struct buf *bp)

if (bp->lock.value < 0){ // bp có người phục vụ

V(&bp->khóa);

trở lại;

if (freebuf.value < 0 && bp->dirty){

viết (bp);
Machine Translated by Google

8.14 Thuật toán quản lý bộ đệm I/O 321

trở lại;

enter_freelist(bp); // nhập b pint bfreeList

bp->bận = 0; // bp không còn bận nữa

V(&bp->khóa); // mở khóa bp

V(&freebuf); // V(freebuf)

Do cả hai tiến trình và trình xử lý ngắt SDC đều truy cập và thao tác danh sách bộ đệm trống và hàng đợi I/O của thiết bị,

các ngắt bị vô hiệu hóa khi các tiến trình hoạt động trên các cấu trúc dữ liệu này để ngăn chặn mọi điều kiện chạy đua.

8.14.1 Hiệu suất của bộ đệm đệm I/O

Với bộ đệm I/O, tỷ lệ trúng của bộ đệm đệm I/O là khoảng 40% khi hệ thống khởi động. Trong quá trình vận hành hệ thống, tỷ lệ trúng liên tục trên 60%.

Điều này chứng tỏ tính hiệu quả của sơ đồ đệm I/O.

8.15 Giao diện người dùng

Tất cả các lệnh của người dùng đều là các tệp thực thi ELF trong thư mục /bin (trên thiết bị gốc). Từ quan điểm của hệ thống EOS, các chương trình

chế độ người dùng quan trọng nhất là init, login và sh, những chương trình này cần thiết để khởi động hệ thống EOS. Sau đây, chúng tôi sẽ giải thích

vai trò và thuật toán của các chương trình này.

8.15.1 Chương trình INIT

Khi EOS khởi động, quy trình ban đầu P0 được thực hiện thủ công. P0 tạo một P1 con bằng cách tải tệp /bin/init dưới dạng hình ảnh Umode của nó.

Khi P1 chạy, nó thực thi chương trình init ở chế độ người dùng. Từ nay trở đi, P1 đóng vai trò tương tự như tiến trình INIT trong Unix/Linux. Một

chương trình init đơn giản, chỉ phân nhánh một quy trình đăng nhập trên bảng điều khiển hệ thống, được hiển thị bên dưới. Người đọc có thể sửa đổi

nó để phân nhánh một số quy trình đăng nhập, mỗi quy trình trên một thiết bị đầu cuối khác nhau.

/********************* tập tin init.c ****************/

#include "ucode.c"

bảng điều khiển int;

int cha() // Mã của P1

int pid, trạng thái;

trong khi(1){

printf("INIT : đợi ZOMBIE con\n");

pid = chờ(&trạng thái);

if (pid==console){ // nếu quá trình đăng nhập vào bảng điều khiển bị chết

printf("INIT: tạo thông tin đăng nhập bảng điều khiển mới\n");

bàn điều khiển = nĩa(); // rẽ nhánh một cái khác

nếu (bàn điều khiển)

Tiếp tục;

khác

exec("đăng nhập /dev/tty0"); // quá trình đăng nhập giao diện điều khiển mới

printf("INIT: Tôi vừa chôn cất một đứa trẻ mồ côi %d\n", pid);

chủ yếu()

{
Machine Translated by Google

322 8 Hệ điều hành nhúng có mục đích chung

int vào, ra; // mô tả tập tin cho thiết bị đầu cuối I/O

in = open("/dev/tty0", O_RDONLY); // bộ mô tả tập tin 0 out = open("/dev/tty0",

O_WRONLY); // để hiển thị ra console printf("INIT : phân nhánh một quá trình đăng nhập trên

console\n");

bàn điều khiển = nĩa();

if (console) // cha mẹ

cha mẹ();

khác // child: exec để đăng nhập vào tty0

exec("đăng nhập /dev/tty0");

8.15.2 Chương trình đăng nhập

Tất cả các quy trình đăng nhập đều thực hiện cùng một chương trình đăng nhập, mỗi chương trình trên một thiết bị đầu cuối khác nhau để người dùng đăng nhập. Thuật toán của

chương trình đăng nhập là

/****************** Thuật toán đăng nhập *******************/

// login.c : Khi vào, argv[0]=login, argv[1]=/dev/ttyX

#include "ucode.c"

int vào, ra, err; tên char [128], mật khẩu [128]

chính(int argc, char *argv[])

(1). đóng bộ mô tả tệp 0,1 được kế thừa từ INIT.

(2). mở argv[1] 3 lần như in(0), out(1), err(2).

(3). giải quyết(argv[1]); // đặt chuỗi tên tty trong PROC.tty (4). mở tệp /etc/passwd để

ĐỌC; trong khi(1){

(5). printf("đăng nhập:"); được (tên);

printf("mật khẩu:"); được(mật khẩu); đối với mỗi dòng

trong tệp /etc/passwd hãy làm{

mã hóa dòng tài khoản người dùng;

(6). if (người dùng có tài khoản hợp lệ){

(7). thay đổi uid, gid thành uid, gid của người dùng; // chuid() thay đổi cwd

thành home của người dùng DIR đóng đã mở /etc/ // chdir()

passwd file exec thành chương trình trong tài // đóng()

(số 8). khoản người dùng // thực thi()

printf("đăng nhập thất bại, thử lại\n");

8.15.3 Chương trình sh

Sau khi đăng nhập, quy trình người dùng thường thực thi trình thông dịch lệnh sh, trình thông dịch này nhận các dòng lệnh từ người dùng và thực thi các lệnh. Đối với mỗi dòng

lệnh, nếu lệnh không tầm thường, tức là không phải cd hoặc exit, sh sẽ phân nhánh một tiến trình con để thực thi dòng lệnh và đợi tiến trình con kết thúc. Đối với các lệnh đơn

giản, mã thông báo đầu tiên của dòng lệnh là tệp thực thi trong thư mục /bin. Một dòng lệnh có thể chứa các ký hiệu chuyển hướng I/O. Nếu vậy, con sh sẽ xử lý các chuyển hướng

I/O trước tiên. Sau đó nó sử dụng exec để thay đổi image để thực thi file lệnh. Khi tiến trình con kết thúc, nó
Machine Translated by Google

8.15 Giao diện người dùng 323

đánh thức sh cha, nhắc nhở một dòng lệnh khác. Nếu một dòng lệnh chứa ký hiệu ống, chẳng hạn như cmd1 | cmd2, con sh xử lý pipe bằng

thuật toán do_pipe sau.

/***************** Thuật toán do_pipe ****************/

int pid, pd[2];

ống(pd); // tạo một pipe: pd[0]=READ, pd[1]=WRITE

pid = ngã ba(); // yêu cầu một đứa trẻ chia sẻ tẩu thuốc

nếu (pid){ // cha mẹ: as pipe READER

đóng(pd[1]); // đóng ống VIẾT kết thúc

dup2(pd[0], 0); // chuyển hướng stdin tới đầu ống READ

thực thi (cmd2);

khác{ // con : as pipe WRITER

đóng(pd[0]); // đóng ống ĐỌC cuối

dup2(pd[1], 1); // chuyển hướng thiết bị xuất chuẩn sang ống VIẾT cuối

thực thi (cmd1);

Nhiều ống được xử lý đệ quy, từ phải sang trái.

8.16 Trình diễn EOS

8.16.1 Khởi động EOS

Hình 8.9 hiển thị màn hình khởi động của hệ thống EOS. Sau khi khởi động, đầu tiên nó sẽ khởi động màn hình LCD, định cấu hình các ngắt có

vectơ và khởi chạy trình điều khiển thiết bị. Sau đó, nó khởi tạo hạt nhân EOS để chạy quy trình ban đầu P0. P0 xây dựng các thư mục

trang, bảng trang và chuyển đổi thư mục trang để sử dụng phân trang 2 cấp động. P0 tạo quy trình INIT P1 với /bin/init dưới dạng hình ảnh

Umode. Sau đó, nó chuyển quy trình sang chạy P1 ở chế độ Người dùng. P1 phân nhánh một quy trình đăng nhập P2 trên bảng điều khiển và một

quy trình đăng nhập khác P3 trên thiết bị đầu cuối nối tiếp. Khi tạo một quy trình mới, nó hiển thị các khung trang được phân bổ động của

hình ảnh quy trình. Khi một tiến trình kết thúc, nó sẽ giải phóng các khung trang được phân bổ để sử dụng lại. Sau đó P1 thực thi trong

một vòng lặp, chờ bất kỳ ZOMBIE con nào.

Mỗi quá trình đăng nhập sẽ mở tệp đặc biệt của thiết bị đầu cuối như stdin (0), stdout (1), stderr (2) cho thiết bị đầu cuối I/O. Sau đó, mỗi

quá trình đăng nhập sẽ hiển thị lời nhắc login: trên thiết bị đầu cuối của nó và chờ người dùng đăng nhập. Khi người dùng cố gắng đăng nhập, quá

trình đăng nhập sẽ xác thực người dùng bằng cách kiểm tra tài khoản người dùng trong tệp /etc/passwd. Sau khi người dùng đăng nhập, quy trình đăng

nhập sẽ trở thành quy trình người dùng bằng cách lấy uid của nó và thay đổi thư mục thành thư mục chính của người dùng. Sau đó, tiến trình người

dùng thay đổi hình ảnh để thực thi trình thông dịch lệnh sh, trình thông dịch này sẽ nhắc lệnh người dùng và thực thi lệnh.

8.16.2 Xử lý lệnh trong EOS

Hình 8.10 cho thấy trình tự xử lý dòng lệnh "cat f1 | grep line" của quy trình EOS sh (P2).

Đối với bất kỳ dòng lệnh không tầm thường nào, sh sẽ phân nhánh một tiến trình con để thực thi lệnh và đợi tiến trình con kết thúc.

Vì dòng lệnh có ký hiệu ống dẫn, nên con sh (P8) tạo một ống dẫn và phân nhánh một ống dẫn con (P9) để chia sẻ ống dẫn. Sau đó con sh (P8)

đọc từ pipe và thực thi lệnh grep. Con sh (P9) ghi vào pipe và thực thi lệnh cat. P8 và P9 được kết nối bằng một đường ống và chạy đồng

thời. Khi pipe reader (P8) kết thúc, nó gửi tiến trình con P9 như một tiến trình mồ côi đến tiến trình INIT P1, tiến trình này thức dậy

để giải phóng tiến trình mồ côi P9.

8.16.3 Xử lý tín hiệu và ngoại lệ trong EOS

Trong EOS, các trường hợp ngoại lệ được xử lý bằng khung xử lý tín hiệu thống nhất. Chúng tôi chứng minh việc xử lý ngoại lệ và tín hiệu

trong EOS bằng các ví dụ sau.


Machine Translated by Google

324 8 Hệ điều hành nhúng có mục đích chung

Hình 8.9 Màn hình khởi động của EOS

Hình 8.10 Xử lý lệnh bằng EOS sh


Machine Translated by Google

8.16 Trình diễn EOS 325

8.16.3.1 Bộ thu tín hiệu báo động và hẹn giờ ngắt quãng

Trong thư mục USER, chương trình itimer.c hiển thị bộ đếm thời gian, tín hiệu cảnh báo và bộ thu tín hiệu cảnh báo.

/******************** tập tin itimer.c *************************/

bộ bắt khoảng trống (int sig)

{ printf("proc %d in catcher: sig=%d\n", getpid(), sig); }

chính(int argc, char *argv[])

int t = 1;

if (argc>1) t = atoi(argv[1]); // khoảng thời gian hẹn giờ

printf("cài đặt catcher? [y|n]");

nếu (getc()=='y')

tín hiệu(14, bộ bắt); // cài đặt catcher() cho SIGALRM(14)

thời gian (t); // đặt bộ đếm thời gian trong kernel

printf("Proc %d lặp lại cho đến khi SIGALRM\n", getpid());

trong khi(1); // lặp cho đến khi bị tín hiệu tắt

Trong chương trình itimer.c, trước tiên, nó cho phép người dùng cài đặt hoặc không cài đặt bộ thu tín hiệu SIGALRM(14). Sau
đó, nó đặt khoảng thời gian là t giây và thực hiện vòng lặp while(1). Khi bộ định thời ngắt quãng hết hạn, bộ xử lý ngắt bộ định
thời sẽ gửi tín hiệu SIGALRM(14) đến tiến trình. Nếu người dùng không cài đặt bộ bắt tín hiệu 14, quá trình sẽ chết do tín hiệu.
Nếu không, nó sẽ thực thi bộ bắt một lần và tiếp tục lặp. Trong trường hợp sau, tiến trình có thể bị hủy bằng các cách khác, ví
dụ bằng phím Control_C hoặc bằng lệnh kill pid từ một tiến trình khác. Người đọc có thể sửa đổi hàm catcher() để cài đặt lại
catcher. Biên dịch lại và chạy hệ thống để quan sát hiệu quả.

8.16.3.2 Xử lý ngoại lệ trong EOS Ngoài tín


hiệu hẹn giờ, chúng tôi cũng chứng minh việc xử lý tín hiệu và ngoại lệ thống nhất bằng các chương trình chế độ người dùng sau.

Mỗi chương trình có thể được chạy dưới dạng lệnh của người dùng.

Data.c: Chương trình này thể hiện việc xử lý ngoại lệ data_abort. Trong trình xử lý data_abort, trước tiên chúng tôi đọc và
hiển thị các thanh ghi địa chỉ và trạng thái lỗi của MMU để hiển thị trạng thái MMU và VA không hợp lệ gây ra ngoại lệ. Nếu ngoại
lệ xảy ra trong Kmode thì đó phải là do lỗi trong mã kernel. Trong trường hợp này, kernel không thể làm gì được. Vì vậy, nó in
ra một thông báo PANIC và dừng lại. Nếu ngoại lệ xảy ra trong Umode, hạt nhân sẽ chuyển đổi nó thành tín hiệu SIGSEG(11) cho lỗi
phân đoạn và gửi tín hiệu đến quy trình. Nếu người dùng không cài đặt bộ bắt tín hiệu 11, quá trình sẽ ngừng hoạt động do tín
hiệu đó. Nếu người dùng đã cài đặt bộ bắt tín hiệu 11, quá trình sẽ thực thi chức năng bắt trong Umode khi nhận được tín hiệu
số 11. Chức năng bắt sử dụng bước nhảy dài để bỏ qua mã bị lỗi, cho phép quá trình kết thúc bình thường.

Prefetch.c: Chương trình này thể hiện việc xử lý ngoại lệ prefetch_abort. Mã C cố gắng thực thi mã hợp ngữ nội tuyến asm(“bl
0x1000”); điều này sẽ gây ra prefetch_abort tại địa chỉ PC tiếp theo 0x1004 vì nó nằm ngoài không gian Umode VA. Trong trường
hợp này, quy trình cũng nhận được tín hiệu SIGSEG(11), tín hiệu này được xử lý theo cách tương tự như ngoại lệ data_abort.

Undef.c: Chương trình này thể hiện việc xử lý ngoại lệ không xác định. Mã C cố gắng thực thi

asm(''mcr p14,0,r1,c8,c7,0'')

điều này sẽ gây ra undef_abort vì bộ đồng xử lý p14 không tồn tại. Trong trường hợp này, tiến trình nhận được tín hiệu lệnh
không hợp lệ SIGILL(4). Nếu người dùng chưa cài đặt bộ bắt tín hiệu 4, quá trình sẽ chết do tín hiệu.
Chia cho 0: Hầu hết các bộ xử lý ARM không có hướng dẫn chia. Phép chia số nguyên trong ARM được triển khai bằng các hàm
idiv và udiv trong thư viện aeabi, giúp kiểm tra lỗi chia cho số 0. Khi phát hiện phép chia cho 0, nó sẽ phân nhánh thành hàm
__aeabi_idiv0. Người dùng có thể sử dụng thanh ghi liên kết để xác định hướng dẫn vi phạm và thực hiện các hành động khắc phục.
Trong máy ảo Versatilepb, chia cho 0 chỉ trả về giá trị số nguyên lớn nhất. Mặc dù không thể tạo ra các ngoại lệ chia cho 0 trên
máy ảo ARM Versatilepb, nhưng sơ đồ xử lý ngoại lệ của EOS có thể áp dụng được cho các bộ xử lý ARM khác.
Machine Translated by Google

326 8 Hệ điều hành nhúng có mục đích chung

8.17 Tóm tắt

Chương này trình bày một hệ điều hành nhúng có mục đích chung đầy đủ chức năng, được ký hiệu là EOS. Sau đây là bản tóm tắt ngắn gọn về tổ chức

và khả năng của hệ thống EOS.

1. Hình ảnh hệ thống: Hình ảnh hạt nhân có khả năng khởi động và các tệp thực thi ở chế độ Người dùng được tạo từ cây nguồn bằng chuỗi công cụ

ARM (của Ubuntu 15.0 Linux) và nằm trong hệ thống tệp EXT2 trên phân vùng SDC. SDC chứa bộ khởi động giai đoạn 1 và giai đoạn 2 để khởi động hình

ảnh hạt nhân từ phân vùng SDC. Sau khi khởi động, kernel sẽ gắn phân vùng SDC làm hệ thống tập tin gốc.

2. Quy trình: Hệ thống hỗ trợ NPROC=64 quy trình và NTHRED=128 luồng cho mỗi quy trình, cả hai đều có thể tăng lên nếu cần. Mỗi tiến trình (ngoại

trừ tiến trình nhàn rỗi P0) chạy ở chế độ Kernel hoặc chế độ Người dùng. Việc quản lý bộ nhớ của hình ảnh quá trình được thực hiện bằng phân

trang động 2 cấp độ. Lập kế hoạch quy trình được thực hiện theo mức độ ưu tiên động và phân chia thời gian. Nó hỗ trợ giao tiếp giữa các quá

trình bằng các đường ống và thông điệp truyền qua. Nhân EOS hỗ trợ fork, exec, vfork, thread, exit và chờ quản lý tiến trình.

3. Nó chứa trình điều khiển thiết bị cho các thiết bị I/O được sử dụng phổ biến nhất, ví dụ như màn hình LCD, bộ hẹn giờ, bàn phím, UART và SDC.

Nó triển khai hệ thống tệp EXT2 tương thích hoàn toàn với Linux với bộ đệm I/O của đọc/ghi SDC để cải thiện hiệu suất và hiệu suất.

4. Nó hỗ trợ đăng nhập nhiều người dùng vào bảng điều khiển và thiết bị đầu cuối UART. Giao diện người dùng sh hỗ trợ thực thi các lệnh đơn

giản với chuyển hướng I/O, cũng như nhiều lệnh được kết nối bằng đường ống.

5. Nó cung cấp các chức năng dịch vụ hẹn giờ và thống nhất việc xử lý ngoại lệ với xử lý tín hiệu, cho phép người dùng cài đặt bộ thu tín hiệu

để xử lý các ngoại lệ trong chế độ Người dùng.

6. Hệ thống chạy trên nhiều loại máy ảo ARM theo QEMU, chủ yếu là để thuận tiện. Nó cũng phải chạy trên bo mạch hệ thống dựa trên ARM thực sự hỗ

trợ các thiết bị I/O phù hợp. Chuyển EOS sang một số hệ thống dựa trên ARM phổ biến, ví dụ:

Raspberry PI-2 hiện đang được tiến hành. Kế hoạch là cung cấp cho người đọc tải xuống ngay khi nó sẵn sàng.

CÁC VẤN ĐỀ

1. Trong EOS, kpgdir của quy trình ban đầu P0 là 32KB. Mỗi tiến trình có pgdir riêng trong PROC.res. Với kích thước hình ảnh Umode 4 MB, các mục

2048-2051 của pgdir của mỗi PROC xác định các bảng trang Umode của quy trình. Khi chuyển đổi nhiệm vụ, chúng tôi sử dụng

switchPgdir int ð Þ ð Þrunning>res>pgdir ;

để chuyển sang pgdir của tiến trình đang chạy tiếp theo. Sửa đổi hàm lịch trình() (trong tệp kernel.c) như sau.,

int *kpgdir = (int *)0x8000; // Kmode pgdir ở mức 32KB if (đang chạy != old){

// thực sự chuyển đổi tiến trình

cho (i=0; i<npgdir; i++) // sao chép pgtables Umode sang kpgdir

kpgdir[2048+i] = đang chạy->res->pgdir[2048+i];

switchPgdir((int)kpgdir); // sử dụng cùng kpgdir với dung lượng 32KB

(1). Xác minh rằng bộ lập lịch đã sửa đổi() vẫn hoạt động và giải thích TẠI SAO?

(2). Mở rộng sơ đồ này để chỉ sử dụng một kpgdir cho TẤT CẢ các quy trình.

(3). Thảo luận những ưu điểm và nhược điểm của việc sử dụng một pgdir cho mỗi quy trình so với một pgdir cho tất cả các quy trình.

2. Các hoạt động gửi/recv trong EOS sử dụng giao thức đồng bộ, giao thức này đang bị chặn và có thể dẫn đến bế tắc do, ví dụ: không có bộ đệm tin

nhắn trống. Thiết kế lại các hoạt động gửi/recv để tránh bế tắc.

3. Sửa đổi thuật toán Delete_dir_entry như sau. Nếu mục nhập đã xóa là mục nhập duy nhất trong khối dữ liệu, hãy giải phóng khối dữ liệu và thu

gọn mảng khối dữ liệu của DIR INODE. Sửa đổi thuật toán Insert_dir_entry cho phù hợp và triển khai các thuật toán mới trong hệ thống tệp EOS.

4. Trong thuật toán read_file của Sec. 8.12.5.2, dữ liệu có thể được đọc từ một tệp vào không gian người dùng hoặc không gian kernel.

(1). Giải thích tại sao cần đọc dữ liệu vào không gian kernel.
Machine Translated by Google

8.17 Tóm tắt 327

(2). Trong vòng lặp bên trong của thuật toán read_file, dữ liệu được truyền từng byte một để đảm bảo rõ ràng. Tối ưu hóa vòng lặp bên trong bằng

cách truyền các khối dữ liệu cùng một lúc (GỢI Ý: dữ liệu tối thiểu còn lại trong khối và dữ liệu có sẵn trong tệp).

5. Sửa đổi thuật toán ghi tệp trong Phần. 8.12.5.3 cho phép (1). Ghi dữ

liệu vào không gian kernel và (2). Tối ưu

hóa việc truyền dữ liệu bằng cách sao chép các khối dữ liệu.

6. Giả sử: dir1 và dir2 là các thư mục. cpd2d dir1 dir2 sao chép đệ quy dir1 vào dir2.

(1). Viết mã C cho chương trình cpd2d.

(2). Điều gì sẽ xảy ra nếu dir1 chứa dir2, ví dụ cpd2d /a/b /a/b/c/d?

(3). Làm cách nào để xác định xem dir1 có chứa dir2 không?

7. Hiện tại, EOS chưa hỗ trợ truyền phát tệp. Triển khai các hàm I/O của thư viện để hỗ trợ các luồng tệp trong không gian người dùng.

Người giới thiệu

Android: https://en.wikipedia.org/wiki/Android_operating_system, 2016.


ARM Versatilepb: ARM 926EJ-S, 2016: Ván chân tường ứng dụng đa năng dành cho Hướng dẫn sử dụng ARM926EJ-S, Trung tâm thông tin Arm, 2016.
Cao, M., Bhattacharya, S, Tso, T., “Ext4: Thế hệ tiếp theo của hệ thống tệp Ext2/3”, Trung tâm Công nghệ Linux của IBM, 2007.
Card, R., Theodore Ts'o,T., Stephen Tweedie,S., “Thiết kế và triển khai Hệ thống tệp mở rộng thứ hai”, web.mit. edu/tytso/www/linux/ext2intro.html,
1995.
EXT2: www.nongnu.org/ext2-doc/ext2.html, 2001.
EXT3: jamesthornton.com/hotlist/linux-filesystems/ext3-journal, 2015.
FreeBSD: Dự án FreeBSD/ARM, https://www.freebsd.org/platforms/arm.html, 2016.
Raspberry_Pi: https://www.raspberrypi.org/products/raspberry-pi-2-model-b, 2016.
Sevy, J., “Chuyển NetBSD sang SoC ARM mới”, http://www.netbsd.org/docs/kernel/porting_netbsd_arm_soc.html, 2016.
Wang, KC, “Thiết kế và triển khai hệ điều hành MTX”, Nhà xuất bản Quốc tế Springer AG, 2015.
Machine Translated by Google

Đa xử lý trong hệ thống nhúng


9

9.1 Đa xử lý

Một hệ thống đa bộ xử lý bao gồm nhiều bộ xử lý, bao gồm cả bộ xử lý đa lõi, dùng chung bộ nhớ chính và các thiết bị I/O. Nếu bộ
nhớ chính dùng chung là bộ nhớ duy nhất trong hệ thống thì nó được gọi là hệ thống Truy cập bộ nhớ thống nhất (UMA). Nếu, ngoài
bộ nhớ dùng chung, mỗi bộ xử lý còn có bộ nhớ cục bộ riêng thì nó được gọi là hệ thống Truy cập bộ nhớ không đồng nhất (NUMA).
Nếu vai trò của các bộ xử lý không giống nhau, ví dụ: chỉ một số bộ xử lý có thể thực thi mã hạt nhân trong khi những bộ xử lý
khác thì không, thì nó được gọi là hệ thống MP bất đối xứng (ASMP). Nếu tất cả các bộ xử lý đều có chức năng giống hệt nhau thì
nó được gọi là hệ thống MP đối xứng (SMP). Với công nghệ xử lý đa lõi hiện tại, SMP gần như đồng nghĩa với MP. Trong chương
này, chúng ta sẽ thảo luận về hệ điều hành SMP trên hệ thống đa bộ xử lý dựa trên ARM.

9.2 Yêu cầu hệ thống SMP

Hệ thống SMP yêu cầu nhiều thứ hơn là chỉ có nhiều bộ xử lý hoặc lõi bộ xử lý. Để hỗ trợ SMP, kiến trúc hệ thống phải có các khả
năng bổ sung. SMP không phải là mới. Trong thế giới PC, nó bắt đầu vào đầu những năm 90 bởi Intel trên bộ xử lý đa lõi dựa trên
x86 của họ. Thông số kỹ thuật đa bộ xử lý của Intel (Intel 1997) định nghĩa các hệ thống tuân thủ SMP là hệ thống tương thích
với PC/AT với các khả năng sau.

(1). Kết hợp bộ đệm: Trong hệ thống tuân thủ SMP, nhiều CPU hoặc lõi chia sẻ bộ nhớ. Để tăng tốc độ truy cập bộ nhớ, hệ thống
thường sử dụng một số cấp độ bộ nhớ đệm, ví dụ: bộ đệm L1 bên trong mỗi CPU và bộ đệm L2 ở giữa CPU và bộ nhớ chính, v.v. Hệ
thống con bộ nhớ phải triển khai giao thức kết hợp bộ đệm để đảm bảo tính nhất quán của bộ nhớ đệm.

(2). Hỗ trợ ngắt định tuyến và ngắt liên bộ xử lý. Trong hệ thống tuân thủ SMP, các ngắt từ thiết bị I/O có thể được định tuyến
đến các bộ xử lý khác nhau để cân bằng tải xử lý ngắt. Các bộ xử lý có thể ngắt lẫn nhau bằng Ngắt liên bộ xử lý (IPI) để liên lạc
và đồng bộ hóa. Trong hệ thống tuân thủ SMP, chúng được cung cấp bởi một bộ Bộ điều khiển ngắt lập trình nâng cao (APIC). Một hệ
thống tuân thủ SMP thường có IOAPIC trên toàn hệ thống và một bộ APIC cục bộ của các bộ xử lý riêng lẻ. Cùng với nhau, các APIC
triển khai giao thức liên lạc giữa các bộ xử lý, hỗ trợ định tuyến ngắt và IPI.

(3). Một BIOS mở rộng có chức năng phát hiện cấu hình hệ thống và xây dựng cấu trúc dữ liệu SMP cho hệ điều hành để
sử dụng.

(4). Khi hệ thống tuân thủ SMP khởi động, một trong các bộ xử lý được chỉ định là Bộ xử lý khởi động (BSP), thực thi mã khởi động
để khởi động hệ thống. Tất cả các bộ xử lý khác được gọi là Bộ xử lý ứng dụng (AP), ban đầu được giữ ở trạng thái không hoạt
động nhưng có thể nhận IPI từ BSP để khởi động. Sau khi khởi động, tất cả các bộ xử lý đều có chức năng giống hệt nhau.

So sánh, SMP trên các hệ thống dựa trên ARM tương đối mới và vẫn đang phát triển. Có thể sẽ sáng suốt hơn khi so sánh cách
tiếp cận SMP của ARM với cách tiếp cận của Intel.

© Springer International Publishing AG 2017 329


KC Wang, Hệ điều hành nhúng và thời gian thực, DOI
10.1007/978-3-319-51517-5_9
Machine Translated by Google

330 9 Đa xử lý trong hệ thống nhúng

(1). Kết hợp bộ đệm: tất cả các hệ thống dựa trên ARM MPcore đều bao gồm Bộ điều khiển Snoop (SCU), thực hiện giao thức kết hợp
bộ nhớ đệm để đảm bảo tính nhất quán của bộ nhớ đệm. Do các đường dẫn bên trong của CPU ARM, ARM đã đưa ra một số loại rào cản
để đồng bộ hóa cả việc truy cập bộ nhớ và thực thi lệnh.
(2). Định tuyến ngắt: Tương tự như APIC của Intel, tất cả các hệ thống ARM MPcore đều sử dụng Bộ điều khiển ngắt chung (GIC)
(ARM GIC 2013) để định tuyến ngắt. GIC bao gồm hai phần; một bộ phân phối ngắt và một giao diện tới CPU. Bộ phân phối ngắt của
GIC nhận các yêu cầu ngắt bên ngoài và gửi chúng đến CPU. Mỗi CPU có một giao diện CPU, có thể được lập trình để cho phép hoặc
không cho phép gửi các ngắt tới CPU. Mỗi ngắt có một số ID ngắt, trong đó số thấp hơn có mức độ ưu tiên cao hơn. Việc định
tuyến ngắt có thể được kiểm soát thêm bởi thanh ghi mặt nạ ưu tiên ngắt của CPU. Đối với các thiết bị I/O, số ID ngắt tương
ứng gần đúng với số vectơ truyền thống của chúng.
(3). Ngắt giữa các bộ xử lý: Mười sáu ngắt GIC đặc biệt có số ID từ 0 đến 15 được dành riêng cho Ngắt do phần mềm tạo (SGI),
tương ứng với IPI trong kiến trúc Intel SMP. Trong hệ thống dựa trên ARM MPcore, CPU có thể phát hành SGI để đánh thức các CPU
khác khỏi trạng thái WFI, khiến chúng thực hiện các hành động, ví dụ: thực thi trình xử lý ngắt, như một phương tiện liên lạc
giữa các bộ xử lý. Trong ARM Cortex-A9 MPcore, thanh ghi SGI (GICD_SGIR) nằm ở độ lệch 0x1F00 so với Địa chỉ cơ sở ngoại vi.
Nội dung đăng ký SGI là

---------------- Nội dung đăng ký SGI -------------------

bit 25-24 : targetListfilter: 00 = tới CPU được chỉ định

01 = cho tất cả các CPU khác

10 = tới CPU yêu cầu

bit 23-16: CPUtargetList; mỗi bit cho giao diện CPU

chút 15 : chỉ dành cho CPU có phần mở rộng bảo mật

bit 3-0 : ID ngắt (0-15)


-------------------------------------------------- -------

Để phát hành SGI, chỉ cần viết một giá trị thích hợp vào sổ đăng ký SGI. Ví dụ: đoạn mã sau gửi SGI của ID ngắt tới CPU mục
tiêu cụ thể.

int send_sgi(int intID, int targetCPU, int filter)

int *sgi_reg = (int *)(CGI_BASE + 0x1F00);

*sgi_reg = (filter<<24)|((1<<targetCPU)<<16)|(intID);

send_sgi(0x00, CPUID, 0x00); // intID=0x00, CPUID = 0 đến 3, filter=0x00

Để gửi SGI tới tất cả các CPU khác, hãy thay đổi giá trị bộ lọc thành 0x01. Trong trường hợp này, danh sách CPU mục tiêu
phải được đặt thành 0x0F (đối với 4 CPU). Khi nhận được ngắt SGI, CPU mục tiêu có thể tiếp tục từ trạng thái WFI hoặc thực thi
trình xử lý ngắt cho số ID ngắt để thực hiện một tác vụ được quy định.
Một cách sử dụng đặc biệt của SGI là trong quá trình khởi động hệ thống SMP. Khi hệ thống ARM SMP khởi động, bộ khởi động
ban đầu thường chọn CPU0 làm bộ xử lý khởi động, bộ xử lý này thực thi mã khởi động để khởi tạo hệ thống. Trong khi đó, tất cả
các CPU phụ khác được giữ trong vòng lặp WFI, chờ SGI từ CPU0 khởi động. Sau khi khởi tạo hệ thống, CPU0 ghi địa chỉ bắt đầu
cho các CPU phụ vào vùng giao tiếp, vùng này có thể là một vị trí bộ nhớ cố định hoặc một thanh ghi toàn hệ thống được tất cả
các CPU có thể truy cập. Sau đó, nó phát SGI tới các CPU phụ. Khi thức dậy sau ngắt SGI, mỗi CPU phụ sẽ kiểm tra nội dung của
vùng liên lạc. Nếu nội dung bằng 0, nó sẽ lặp lại vòng lặp WFI. Ngược lại, nó thực thi từ địa chỉ bắt đầu được gửi bởi CPU0.
Vùng giao tiếp và địa chỉ bắt đầu cho các CPU thứ cấp được chọn bởi bộ khởi động ban đầu và mã khởi động kernel. Ví dụ: khi
khởi động nhân SMP Linux trên ARM bằng Uboot, CPU0 sẽ ghi địa chỉ bắt đầu cho các CPU phụ vào thanh ghi SYS_FLAGSSET (Khởi động
ARM Linux SMP trên MPcore 2010). Trong máy ảo real-view-pbx-a9 được mô phỏng trong QEMU, nó cũng sử dụng bộ điều chỉnh
SYS_FLAGSSET làm vùng liên lạc giữa CPU0 và CPU phụ.
Machine Translated by Google

9.3 Bộ xử lý ARM MPcore 331

9.3 Bộ xử lý ARM MPcore

ARM Cortex-A là một nhóm lõi xử lý 32 bit và 64 bit triển khai kiến trúc ARMv7. Nhóm này bao gồm các bộ xử lý ARM MPcore 32 bit có
nhãn Cortex-A5, A8, A9, A12, cũng như một số mẫu 64 bit, chẳng hạn như Cortex-A15 và A17. Đây không phải là những bộ vi điều khiển
đơn giản mà là những bộ xử lý đa lõi (MPcore) dành cho các ứng dụng MP thông thường. Sau đây, chúng tôi sẽ giới hạn các cuộc thảo
luận của mình ở bộ xử lý Cortex-A9 MPcore 32-bit, dành cho các mục sau
lý do.

(1). Bộ xử lý ARM Cortex-A9 MPcore được ghi rõ trong [Hướng dẫn tham khảo kỹ thuật ARM Cortex-A9 MPcore].
Một loạt ván chân tường ARM RealView đã được triển khai dựa trên Cortex-A9 MPcore, cũng được ghi lại đầy đủ trong [Bảng nền tảng
nền tảng ARM RealView cho Cortex-A9].

(2). Bộ xử lý Cortex-A9 hỗ trợ các hướng dẫn đồng bộ hóa CPU và quản lý bộ nhớ, những điều cần thiết cho hệ điều hành đa nhiệm.

(3). Ván chân tường ARM Realview hỗ trợ các thiết bị ngoại vi ARM tiêu chuẩn, chẳng hạn như UART, LCD, bàn phím và giao diện Đa
phương tiện cho thẻ SDC.

(4). Khá dễ dàng để nói về các nguyên tắc chung của SMP. Tuy nhiên, nếu không thực hành lập trình, những kiến thức đó sẽ chỉ là hời
hợt. Vì hầu hết người đọc có thể không có quyền truy cập vào hệ thống phần cứng dựa trên ARM MPcore thực sự, điều này đặt ra một
thách thức trong việc tìm hiểu cả khía cạnh lý thuyết và thực tiễn của SMP. Để giải quyết vấn đề này, chúng ta lại chuyển sang máy
ảo. Tính đến thời điểm hiện tại, QEMU hỗ trợ một số phiên bản máy ảo ARM MPcore dựa trên ARM Cortex-A9 MPcore. Ví dụ: nó hỗ trợ các
bo mạch ARM realview-pb-a8, realview-pbx-a9 và vexpress-a9 với tối đa 4 lõi. Ngoài ra, nó còn hỗ trợ vexpress-a15, dựa trên bộ xử lý
ARM Cortex-A15 MPcore với tối đa 8 lõi.
Các máy ảo ARM MPcore mô phỏng QEMU cung cấp cho chúng tôi một môi trường thuận tiện để phát triển và chạy các hệ điều hành SMP OS.

Mặc dù chúng tôi đã thử nghiệm tất cả các ví dụ lập trình trên một số phiên bản của máy ảo ARM MPcore, nhưng sau đây chúng tôi
sẽ tập trung chủ yếu vào máy ảo Realview-pbx-a9 trong QEMU.

Bộ xử lý lõi 9.4 ARM Cortex-A9

Các tính năng chính của bộ xử lý ARM Cortex-A9 MPcore hỗ trợ SMP có thể được tóm tắt như sau.

9.4.1 Lõi bộ xử lý

1 đến 4 lõi. Trạng thái SMP của mỗi lõi được điều khiển bởi bit 6 của Thanh ghi điều khiển phụ trợ (ACTL) của bộ đồng xử lý CP15.
Mỗi lõi CPU có thể được lập trình để tham gia SMP hoặc không. Cụ thể, mỗi lõi CPU có thể sử dụng các đoạn mã sau để tham gia hoặc
tách khỏi hoạt động SMP.

tham gia_smp: // CPU tham gia hoạt động SMP

MRC p15, 0, r0, c1,c0, 1 // đọc ACTLR

ORR r0, r0, #0x040 // đặt bit 6

MCR p15, r0, c1, c0, 1 // ghi vào ACTLR

BX lr

ngắt kết nối_smp: // CPU ngắt hoạt động SMP

MRC 0, r0, c1, c0, 1 // đọc ACTLR

BIC r0, #0x040 // xóa bit 6

MCR p15, 0, r0, c0, 1 // ghi vào ACTLR

BX lr

Nếu một số lõi không tham gia SMP, chúng có thể được chỉ định để chạy các tác vụ chuyên dụng hoặc thậm chí các hệ điều hành
riêng biệt, bên ngoài môi trường SMP, dẫn đến hệ thống MP bất đối xứng (ASMP). Vì hệ thống MP nên thử
Machine Translated by Google

332 9 Đa xử lý trong hệ thống nhúng

để tận dụng tối đa khả năng của tất cả các lõi, chúng ta sẽ giả định một môi trường SMP mặc định, trong đó tất cả các lõi đều tham gia

vào các hoạt động SMP.

9.4.2 Bộ điều khiển rình mò (SCU)

SCU đảm bảo sự kết hợp bộ đệm L1 giữa các CPU.

9.4.3 Bộ điều khiển ngắt chung (GIC)

Bộ phân phối ngắt của GIC có thể được lập trình để định tuyến các ngắt đến các CPU cụ thể. Mỗi CPU có Giao diện CPU riêng, có thể được

lập trình để cho phép hoặc không cho phép các ngắt với mức độ ưu tiên khác nhau. Trên bo mạch ARM Realview-PBX-A9, địa chỉ cơ sở ngoại

vi là 0x1F000000. Nó cũng có thể được đọc vào thanh ghi chung Rn từ bộ đồng xử lý p15 bằng cách

MRC p15, 4, Rn, c15, c0, 0

Từ địa chỉ cơ sở ngoại vi, Giao diện CPU ở địa chỉ offset 0x100 và Bộ phân phối ngắt ở địa chỉ offset 0x1000. Các thanh ghi khác là

độ lệch 32 bit từ các địa chỉ cơ sở này. Phần sau đây mô tả các thanh ghi GIC.

Thanh ghi nhà phân phối ngắt: Đây là các thanh ghi 32 bit để điều khiển SCU. Các offset và chức năng của chúng trong các thanh ghi

này là

0x000: kiểm soát nhà phân phối; bit-0 = bật/tắt 0x100:

3 (32-bit) đặt các thanh ghi kích hoạt; mỗi bit cho phép một ID ngắt tương ứng 0x180: 3 (32-

bit) thanh ghi cho phép xóa; mỗi bit vô hiệu hóa một ID ngắt tương ứng. 0x800: Thanh ghi mục

tiêu CPU; gửi ID ngắt tới CPU mục tiêu 0xC00: Thanh ghi cấu hình; xác

định mô hình xử lý ngắt 1-to-N hoặc N-to-N 0xF00: Thanh ghi ngắt tạo mềm (SGI); gửi SGI đến CPU

mục tiêu Thanh ghi giao diện CPU: Đây là các thanh ghi giao diện CPU 32-bit trong

SCU. Địa chỉ và chức năng offset của chúng là 0x00: thanh ghi điều khiển; bit-0 = bật/tắt 0x04: mặt nạ ưu tiên; một ngắt chỉ được gửi

đến CPU này nếu mức độ ưu tiên của nó cao hơn

mặt nạ. Giá trị thấp hơn có nghĩa là mức độ ưu tiên cao hơn. 0x08: điểm nhị phân: chỉ định xem có cho phép ngắt ưu tiên hay không

0x0C:

Thanh ghi ACK ngắt: chứa số ID ngắt 0x10: Kết thúc thanh ghi ngắt: ghi ID ngắt

vào thanh ghi này để báo hiệu EOI.

9.5 Ví dụ lập trình GIC

Vì GIC là một phần thiết yếu của hệ thống ARM MPcore nên việc hiểu chức năng và cách sử dụng của nó là cần thiết và quan trọng. Trước

khi thảo luận về ARM SMP, trước tiên chúng tôi minh họa lập trình GIC bằng một ví dụ.

9.5.1 Định cấu hình GIC để định tuyến các ngắt

Như thường lệ, chương trình ví dụ C9.1 bao gồm tệp ts.s trong hợp ngữ và tệp tc trong C. Để bắt đầu, chúng ta sẽ chỉ sử dụng một CPU

duy nhất để hỗ trợ ba loại ngắt: ngắt đầu vào từ hai UART, ngắt bàn phím và đồng hồ hẹn giờ. Bo mạch ARM Realview-pbx-a9 hỗ trợ cùng

loại thiết bị I/O như trong bo mạch ARM Versatilepb (ARM926EJ-S 2010; ARM Hẹn giờ 2004), nhưng địa chỉ cơ sở và số IRQ của chúng khác

nhau. Phần sau đây liệt kê các địa chỉ cơ sở và số ngắt GIC của các thiết bị I/O dự định của bo mạch ARM Realview-pbx-a9.
Machine Translated by Google

9.5 Ví dụ lập trình GIC 333

Địa chỉ cơ sở thiết bị I/O Số IRQ GIC


-------- ---------------- --------------

Bộ hẹn giờ0 0x10011000 36

UART0: 0x10009000 44

UART1: 0x1000A000 45

Bàn phím 0x10006000 52

LCD 0x10120000 -

----------------------------------------

Khi VM ARM realview-pbx-a9 khởi động, CPU0 sẽ thực thi trình xử lý đặt lại ở chế độ SVC. Nó đặt SVC và IRQ

chế độ con trỏ ngăn xếp và sao chép bảng vectơ tới địa chỉ 0. Sau đó, nó cho phép ngắt IRQ và gọi hàm main() trong C. Vì

Mục đích ở đây là hiển thị cấu hình và lập trình GIC, tất cả các trình điều khiển thiết bị đều là phiên bản đơn giản của trình điều khiển

được phát triển trong các chương trước cho bo mạch ARM Versatilepb. Mỗi trình điều khiển đơn giản chỉ bao gồm một hàm init(),

khởi tạo thiết bị để tạo ra các ngắt và trình xử lý ngắt để đáp ứng các ngắt. Để cho

Kiểm tra đầu đọc chạy chương trình trực tiếp, chúng tôi hiển thị mã chương trình hoàn chỉnh của C9.1.

(1). tập tin ts.s của C9.1

/*************** tệp ts.s của C9.1 ****************/

.chữ

.code 32

.global reset_handler, vectơ_start, vectơ_end

.global Enable_scu, get_cpu_id

reset_handler:

LDR sp, =svc_stack_top // đặt ngăn xếp SVC

// chuyển sang chế độ IRQ để đặt ngăn xếp IRQ

MSR cpsr, #0x92

LDR sp, =irq_stack_top // đặt ngăn xếp IRQ

// quay lại chế độ SVC

MSR cpsr, #0x93

Bản sao BL_vector // sao chép vectơ vào địa chỉ 0

MSR cpsr, #0x13 // kích hoạt ngắt IRQ

BL chính // CPU0 gọi main() trong C

B .

irq_handler:

phụ lr, lr, #4

stmfd sp!, {r0-r3, r12, lr}

bl irq_chandler // gọi irq_chandler() trong C

ldmfd sp!, {r0-r3, r12, pc}^

// các trình xử lý ngoại lệ giả không được sử dụng

undef_handler:

swi_handler:

tìm nạp trước_abort_handler:

dữ liệu_abort_handler:

fiq_handler:

B .

// Các hàm tiện ích SMP:

kích hoạt_scu: // void Enable_scu(void)

MRC p15, 4, r0, c15, c0, 0 // Đọc địa chỉ cơ sở ngoại vi

LDR r1, [r0, #0x0] // Đọc thanh ghi điều khiển SCU

ORR r1, r1, #0x1 // Đặt bit 0 (Bit Kích hoạt)

STR r1, [r0, #0x0] // Viết lại giá trị đã sửa đổi

BX lr
Machine Translated by Google

334 9 Đa xử lý trong hệ thống nhúng

// int get_cpu_id(): trả về ID (0 đến 3) của CPU thực thi

get_cpu_id:

MRC p15, 0, r0, c0, c0, 5 // Đọc thanh ghi ID CPU

VÀ r0, r0, #0x03 // Mặt nạ ở mức 2 bit thấp = ID CPU

BX lr

vectơ_start:

PC LDR, reset_handler_addr

Máy tính LDR, undef_handler_addr

Máy tính LDR, swi_handler_addr

PC LDR, tìm nạp trước_abort_handler_addr

PC LDR, dữ liệu_abort_handler_addr

B .

Máy tính LDR, irq_handler_addr

PC LDR, fiq_handler_addr

đặt lại_handler_addr: .word reset_handler

undef_handler_addr: .word undef_handler

swi_handler_addr: .word swi_handler

prefetch_abort_handler_addr: .word prefetch_abort_handler

dữ liệu_abort_handler_addr: .word data_abort_handler

irq_handler_addr: .word irq_handler

fiq_handler_addr: .word fiq_handler

vectơ_end:

(2). tập tin uart.c của C9.1

/*************** tập tin uart.c của C9.1 *************/

#define UART0_BASE 0x10009000

typedef cấu trúc uart{

u32DR; // thanh ghi dữ liệu

u32 DSR;

u32 pad1[4]; // 8+16=24 byte vào thanh ghi FR

u32 FR; // đăng ký cờ ở 0x18

u32 pad2[7];

u32 imsc; // đăng ký imsc ở offset 0x38

}UART;

UART *upp[4]; // 4 con trỏ UART tới cấu trúc UART

int uputc(UART *up, char c)

int i = up->FR;

while((up->FR & 0x20));

(up->DR) = (int)c;

void uart_handler(int ID)

UART *lên;

ký tự c;

int cpuid = get_cpu_id();

màu = (ID==0)? VÀNG : TÍM;

lên = upp[ID];

c = lên->DR;

uputc(lên, c);

printf("UART%d ngắt trên CPU%dc=%c\n", ID, cpuid, c);


Machine Translated by Google

9.5 Ví dụ lập trình GIC 335

nếu (c=='\r')

uputc(lên, '\n');

màu=ĐỎ;

int uart_init()

int tôi;

cho (i=0; i<4; i++){ // uart0 đến uart2 liền kề

upp[i] = (UART *)(UART0_BASE + i*0x1000); upp[i]->imsc |=

(1<<4); // kích hoạt ngắt UART RXIM

(3). tập tin kbd.c của C9.1

/*************** tệp kbd.c của C9.1 *************/

#include "sơ đồ bàn phím"

int bên ngoài kputc(char); // trong vid.c của trình điều khiển LCD

typedef cấu trúc kbd{ // cơ sở = 0x10006000

điều khiển u32; // 7- 6- 5(0=AT) 4=RxIntEn 3=TxIntEn 2 1 0

trạng thái u32; // 7- 6=TxE 5=TxBusy 4=RXFull 3=RxBusy 2 1 0

dữ liệu u32;

đồng hồ u32;

trạng thái u32;

// các lĩnh vực khác;

}KBD;

KBD *kbd;

khoảng trống kbd_handler()

mã char không dấu, c;

int cpuid = get_cpu_id();

màu = ĐỎ;

scode = kbd->data;

nếu (mã & 0x80) // bỏ qua việc phát hành khóa

đi ra ngoài;

c = unsh[scode];

printf("ngắt kbd trên CPU%d: c=%x %c\n", cpuid, c, c);

ngoài:

kbd->trạng thái = 0xFF;

int kbd_init()

kbd = (KBD *)0x10006000; // địa chỉ cơ sở

kbd->điều khiển = 0x14; // 0001 0100

kbd->đồng hồ = 8;

(4). tập tin time.c của C9.1

/************ tập tin time.c của C9.1 *************/

#xác định CTL_ENABLE ( 0x00000080 )

#define CTL_MODE ( 0x00000040 )

#xác định CTL_INTR ( 0x00000020 )


Machine Translated by Google

336 9 Đa xử lý trong hệ thống nhúng

#xác định CTL_PRESCALE_1 ( 0x00000008 )

#xác định CTL_PRESCALE_2 #xác định ( 0x00000004 )

CTL_CTRLEN #xác định ( 0x00000002 )

CTL_ONESHOT ( 0x00000001 )

#xác định SỐ 64

bộ đếm thời gian cấu trúc typedef {

u32 TẢI; // Tải thanh ghi, hẹn giờXLoad 0x00

GIÁ TRỊ u32; // Thanh ghi giá trị hiện tại, TimeXValue 0x04

u32 ĐIỀU KHIỂN;// Thanh ghi điều khiển, Hẹn giờXControl 0x08

u32 INTCLR; // Ngắt xóa thanh ghi, TimeXIntClr 0x0C

u32 RIS; // Thanh ghi trạng thái ngắt thô, Hẹn giờXRIS 0x10

u32 MIS; // Đăng ký trạng thái ngắt được che giấu,TimerXMIS 0x14

u32 BGLOAD; // Đăng ký tải nền, Hẹn giờXBGLoad 0x18

u32 *cơ sở;

}Đồng hồ hẹn giờ;

THỜI GIAN *tp[4]; // 4 bộ định thời; 2 bộ tính giờ cho mỗi đơn vị; ở 0x00 và 0x20

int kprintf(char *fmt, …);

hàng int bên ngoài, col;

int kpchar(char, int, int);

int unkpchar(char, int, int);

đồng hồ char[16];
"
char *khoảng trống = : : ";

int hh, mm, ss;

u32 đánh dấu = 0;

bộ đếm thời gian trống0_handler()

int tôi;

đánh dấu++;

if (đánh dấu >= CHIA){

đánh dấu = 0; ss++;

nếu (ss==60){

ss = 0; mm++;

nếu (mm==60){

mm = 0; hh++;

if (tick==0){ // mỗi giây: hiển thị đồng hồ treo tường

màu = XANH;

cho (i=0; i<8; i++){

unkpchar(đồng hồ[i], 0, 60+i);

đồng hồ[7]='0'+(ss%10); đồng hồ[6]='0'+(ss/10);

đồng hồ[4]='0'+(mm%10); đồng hồ[3]='0'+(mm/10);

đồng hồ[1]='0'+(hh%10); đồng hồ[0]='0'+(hh/10);

cho (i=0; i<8; i++){

kpchar(đồng hồ[i], 0, 60+i);

hẹn giờ_clearInterrupt(0); // xóa ngắt hẹn giờ

}
Machine Translated by Google

9.5 Ví dụ lập trình GIC 337

làm trống bộ đếm thời gian_init()

int tôi;

printf("timer_init()\n"); // đặt địa

chỉ cơ sở hẹn giờ của bảng linh hoạtpb-A9

tp[0] = (TIMER *)0x10011000;

tp[1] = (TIMER *)0x10012000;

tp[2] = (TIMER *)0x10018000;

tp[3] = (TIMER *)0x10019000;

// đặt reg bộ đếm điều khiển thành mặc định

cho (i=0; i<4; i++){

tp[i]->LOAD = 0x0; tp[i]- // cài lại

>VALUE= 0xFFFFFFFF;

tp[i]->RIS = 0x0;

tp[i]->MIS = 0x0;

tp[i]->LOAD = 0x100;

// 0x62=|011- 0010=|NOTEn|Pe|IntE|-|scal=00|1=32-bit|0=wrap|

tp[i]->KIỂM SOÁT = 0x62;

tp[i]->BGLOAD = 0xF0000/DIVISOR;

strcpy(đồng hồ, "00:00:00");

hh = mm = ss = 0;

void time_start(int n) // time_start(0), 1, v.v.

THỜI GIAN *tpr;

printf("timer_start\n");

tpr = tp[n];

tpr-> KIỂM SOÁT |= 0x80; // đặt bit kích hoạt 7

int time_clearInterrupt(int n) // time_start(0), 1, v.v.

THỜI GIAN *tpr = tp[n]; tpr-

>INTCLR = 0xFFFFFFFF;

(5). vid.c của C9.1: giống như các chương trước ngoại trừ LCD base=0x10120000 (6).
tập tin tc của C9.1

/*************** file tc của C9.1: ConfigGIC *************/

#define GIC_BASE 0x1F000000

#include "uart.c"

#include "kbd.c"

#include "timer.c"

#include "vid.c"

int copy_vectors(){ // giống như trước }

int config_int(int intID, int targetCPU)

int reg_offset, chỉ mục, địa chỉ; mức độ ưu

tiên char = 0x80;

// đặt intID BIT trong thanh ghi int ID

reg_offset = (intID>>3) & 0xFFFFFFFC;

chỉ mục = intID & 0x1F;


Machine Translated by Google

338 9 Đa xử lý trong hệ thống nhúng

địa chỉ = (GIC_BASE + 0x1100) + reg_offset;

*(int *)address |= (1 << chỉ mục); // đặt intID

BYTE trong mục tiêu bộ xử lý register reg_offset = (intID & 0xFFFFFFFC);

chỉ mục = intID & 0x3; địa chỉ = (GIC_BASE + 0x1400)

+ reg_offset + chỉ mục; // đặt

BYTE ưu tiên trong thanh ghi ưu tiên *(char *)address = (char)priority; địa chỉ =

(GIC_BASE + 0x1800) + reg_offset + chỉ mục; *(char *)address =

(char)(1 << targetCPU);

int config_gic()

printf("config ngắt 36, 44, 45, 52\n"); config_int(36, 0); // Time0

config_int(44, 0); // UART0 config_int(45,

0); // Cấu hình UART1_int(52, 0); // KBD //

thiết lập thanh ghi mặt nạ ưu tiên int

*(int *)(GIC_BASE + 0x104) = 0xFF;

// thiết lập thanh ghi điều khiển giao diện CPU: cho phép định tuyến ngắt

*(int *)(GIC_BASE + 0x100) = 1;

// thiết lập thanh ghi điều khiển phân phối: gửi các ngắt đang chờ xử lý tới CPU

*(int *)(GIC_BASE + 0x1000) = 1;

int irq_chandler()

// đọc ICCIAR của giao diện CPU trong GIC

int intID = *(int *)(GIC_BASE + 0x10C);

chuyển đổi (intID) {

trường hợp 36: time0_handler(); phá vỡ; // ngắt hẹn giờ

trường hợp 44: uart_handler(0); phá vỡ; // ngắt UART0

trường hợp 45: uart_handler(1); phá vỡ; // Ngắt UART1

trường hợp 52: kbd_handler(); phá vỡ; // ngắt KBD

*(int *)(GIC_BASE + 0x110) = intID; // viết EOI

int chính()

fbuf_init(); // khởi tạo trình điều khiển LCD

printf("***** Ví dụ về cấu hình ARM GIC ******\n");

Enable_scu(); // kích hoạt SCU

config_gic(); // cấu hình GID

kbd_init(); // khởi tạo trình điều khiển KKB

uart_init(); // khởi tạo trình điều khiển UART

bộ đếm thời gian_init(); // khởi tạo trình điều khiển hẹn giờ

bộ đếm thời gian_start(0); // bắt đầu hẹn giờ

printf("nhập key từ KBD hoặc UARTs :\n");

trong khi(1); // lặp nhưng có thể đáp ứng các ngắt

}
Machine Translated by Google

9.5 Ví dụ lập trình GIC 339

(7). Biên dịch liên kết ts.s và tc thành t.bin: giống như trong các Chương trước

(số 8). Chạy t.bin trên máy ảo realview-pbx-a9 với 2 cổng UART:

qemu-system-arm -M realview-pbx-a9 -kernel t.bin \

-serial mon:stdio –serial /dev/pts/1

9.5.2 Giải thích về Mã cấu hình GIC

Trong config_gic(), các dòng thiết lập các thanh ghi kích hoạt Giao diện CPU và Nhà phân phối (cả ở bit0) đều hiển nhiên. CPU của
thanh ghi mặt nạ ngắt được đặt ở mức ưu tiên thấp nhất là 0xFF, do đó CPU sẽ nhận được các ngắt có giá trị ưu tiên <0xFF. TRONG
Nhà phân phối ngắt (tại cơ sở GIC + 0x1000), các thanh ghi khác nhau nằm ở độ lệch

0x100: Các thanh ghi cho phép cài đặt ngắt: mỗi bit = bật một intID

0x400: Thanh ghi ưu tiên ngắt: 0x800: Thanh ghi CPU 4 bit cao của mỗi byte = mức độ ưu tiên của intID

đích: mỗi byte= CPU đích của intID

Có 3 thanh ghi cho phép thiết lập ngắt, được ký hiệu là Set-enable0 đến Set-enable2, nằm ở độ lệch 0x100 đến 0x108.
Các thanh ghi này có thể được coi là một danh sách tuyến tính các bit, trong đó mỗi bit cho phép một ID ngắt, như sơ đồ sau
trình diễn

BIT: |0 1 . . . 31|32 33 . . . 63|64 65 . . . 95|


-----------------------------------------------

REG: | Đặt-kích hoạt0 | Đặt-kích hoạt1 | Đặt-kích hoạt2 |


-----------------------------------------------

Với số ID ngắt, intID, chúng ta phải xác định vị trí bit cần thiết lập trong các thanh ghi cho phép cài đặt ngắt. Các
tính toán dựa trên các phép chia và modulo, được gọi là thuật toán của Người đưa thư trong (Wang 2015). Đầu tiên, chúng tôi
tính toán độ lệch thanh ghi và bit của intID như

reg_offset = 4*(intID / 32); // giống như (intID >> 5) << 2

mục lục = intID% 32; // giống intID & 0x1F

Sau đó, các dòng mã sau

địa chỉ = (GIC_BASE + 0x1100) + reg_offset;

*(int *)address |= (1 << chỉ mục);

đặt bit cho phép ID ngắt thành 1.


Tương tự, chúng ta có thể đặt mức độ ưu tiên và byte CPU mục tiêu của ID ngắt bằng cùng một thuật toán. Có 24 CPU mục tiêu
sổ đăng ký. Mỗi thanh ghi chứa dữ liệu CPU của 4 ID ngắt, tức là mỗi byte chỉ định CPU mục tiêu của ID ngắt.
Tương tự, có 24 thanh ghi ưu tiên ngắt. Mỗi thanh ghi giữ mức độ ưu tiên của 4 ID ngắt, tức là mỗi byte
giữ mức độ ưu tiên của ID ngắt. Bố cục (tuyến tính) của các thanh ghi này tương tự như sơ đồ trên ngoại trừ mỗi thanh ghi
byte chỉ định danh sách CPU mục tiêu hoặc mức độ ưu tiên ngắt. Với một ID ngắt, chúng tôi tính toán thanh ghi và byte
bù đắp như

reg_offset = intID/4; // giống intID >> 2

mục lục = intID% 4; // giống intID & 0x3

Sau đó, các dòng mã sau


Machine Translated by Google

340 9 Đa xử lý trong hệ thống nhúng

Hình 9.1 Trình diễn lập trình GIC

địa chỉ = (GIC_BASE + 0x1800) + reg_offset + chỉ mục;

*(char *)address = (char)(1 << targetCPU);

đặt byte CPU trong thanh ghi CPU đích. Có thể sử dụng chính xác thuật toán tương tự để đặt byte mặt nạ ưu tiên ngắt

trong thanh ghi mặt nạ ưu tiên.

Vì chúng tôi chưa kích hoạt các CPU khác nên hiện tại tất cả các ngắt đều được chuyển đến CPU0. Chúng tôi sẽ trình bày cách định

tuyến các ngắt đến các CPU khác nhau sau. Nói chung, mỗi ID ngắt phải được định tuyến đến một CPU duy nhất. ID ngắt cũng có thể được

định tuyến tới nhiều CPU. Trong trường hợp đó, người dùng phải đảm bảo rằng quá trình xử lý ngắt sử dụng mô hình 1-to-N, trong đó chỉ

có một CPU thực sự xử lý ngắt.

9.5.3 Ưu tiên ngắt và mặt nạ ngắt

Mỗi ID ngắt có thể được đặt thành giá trị ưu tiên từ 0 đến 15, trong đó giá trị thấp có nghĩa là mức độ ưu tiên cao. Để gửi một ngắt

tới CPU, mức ưu tiên ngắt phải cao hơn (về giá trị thấp hơn) so với giá trị mặt nạ ưu tiên của CPU. Để đơn giản, trong ví dụ, tất

cả các ngắt được đặt thành cùng mức ưu tiên 8. Thanh ghi mặt nạ của CPU được đặt thành 0xF, do đó nó sẽ chấp nhận mọi ngắt có mức

ưu tiên > 0xF. Bạn đọc có thể thử làm thí nghiệm sau. Nếu chúng ta đặt thanh ghi mặt nạ ưu tiên của CPU thành giá trị <= 8 thì sẽ

không có ngắt nào xảy ra do không có ngắt nào được gửi đến CPU.

9.5.4 Trình diễn lập trình GIC

Hình 9.1 cho thấy kết quả đầu ra mẫu khi chạy chương trình ví dụ C9.1. Đối với các ngắt hẹn giờ, bộ xử lý ngắt hẹn giờ sẽ hiển thị

đồng hồ treo tường. Đối với các ngắt bàn phím và UART, nó hiển thị các ngắt và các phím nhập. Đối với mỗi ngắt UART, nó cũng lặp lại

phím đầu vào tới cổng UART, được hiển thị ở đầu Hình 9.1.

9.6 Trình tự khởi động của ARM MPcores

Trình tự khởi động của hệ thống SMP dựa trên Intel x86 được xác định rõ ràng (Intel 1997). Tất cả các hệ thống SMP dựa trên Intel x86

đều chứa BIOS tiêu chuẩn trong ROM, chạy khi hệ thống được bật nguồn hoặc sau khi đặt lại. Khi hệ thống SMP dựa trên Intel x86 khởi

động, trước tiên BIOS sẽ cấu hình hệ thống để vận hành SMP. Nó chỉ định một trong các CPU, thường là CPU0, làm Bộ xử lý khởi động

(BSP), thực thi mã khởi động để khởi động hệ thống. Tất cả các CPU khác được gọi là Bộ xử lý ứng dụng (AP), được giữ ở trạng thái

không hoạt động, chờ Ngắt liên bộ xử lý (IPI) từ BSP khởi động. Để biết thêm thông tin về trình tự khởi động của hệ thống SMP dựa trên

Intel x86, người đọc có thể tham khảo (Intel 1997; Wang 2015).
Machine Translated by Google

9.6 Trình tự khởi động của ARM MPcores 341

Ngược lại, trình tự khởi động của hệ thống ARM SMP hơi mơ hồ và trong nhiều trường hợp mang tính đặc biệt. Lý do chính cho
việc thiếu tiêu chuẩn này là do các hệ thống dựa trên ARM không có BIOS tiêu chuẩn. Hầu hết các hệ thống ARM đều có bộ khởi động
tích hợp được triển khai trong phần sụn. Trình tự khởi động của hệ thống ARM SMP chủ yếu dựa vào bộ khởi động tích hợp, bộ khởi
động này rất khác nhau, tùy thuộc vào bo mạch hoặc nhà cung cấp cụ thể. Sau khi khảo sát tài liệu, chúng tôi có thể phân loại trình
tự khởi động của hệ thống ARM SMP thành ba loại.

9.6.1 Trình tự khởi động thô

Khi hệ thống SMP dựa trên ARM khởi động, tất cả các CPU đều thực thi từ địa chỉ vectơ 0 (giả sử không có chuyển vị trí vectơ trong
khi khởi động). Mỗi CPU có thể lấy số CPUID từ bộ đồng xử lý p15. Tùy thuộc vào số CPUID, chỉ một trong số các CPU, điển hình là
CPU0, được chọn làm BSP, BSP sẽ tự khởi tạo và thực thi mã khởi tạo hệ thống. Tất cả các CPU thứ cấp (AP) khác đều tự đặt mình vào
vòng chờ bận hoặc trạng thái WFI tiết kiệm điện, chờ SGI (Ngắt tạo phần mềm) từ BSP thực sự khởi động. Sau khi khởi tạo hệ thống,
BSP ghi địa chỉ bắt đầu của các AP vào vùng giao tiếp, có thể là một vị trí cố định trong bộ nhớ hoặc một thanh ghi toàn hệ thống
mà tất cả các CPU có thể truy cập được. Hầu hết các hệ thống ARM SMP sử dụng thanh ghi SYS_FLAGSSET trên toàn hệ thống làm vùng
liên lạc. Sau đó BSP kích hoạt các AP bằng cách gửi SGI cho chúng. Giống như trong hệ thống Intel SMP, ARM SGI có thể được gửi
đến từng AP riêng lẻ (CPU filterList=00) hoặc tới tất cả các AP bằng cách phát sóng (CPU targetList=0xF, filterList=01). Khi thức
dậy từ trạng thái WFI, mỗi AP sẽ kiểm tra nội dung của vùng liên lạc. Nếu nội dung bằng 0, nó sẽ lặp lại vòng lặp WFI. Ngược lại,
nó bắt đầu thực thi từ địa chỉ bắt đầu được BSP ký gửi.

9.6.2 Trình tự khởi động được hỗ trợ khởi động

Khi hệ thống ARM SMP khởi động, bộ khởi động tích hợp sẽ chọn CPU0 làm BSP và tạm dừng các CPU phụ khác cho đến khi chúng được BSP
kích hoạt. Trong nhiều trường hợp, bộ khởi động tích hợp quá thô sơ để khởi động một hệ điều hành thực sự. Vì vậy, nó tải bộ khởi
động giai đoạn 2 từ thiết bị lưu trữ và chuyển quyền điều khiển sang bộ khởi động giai đoạn 2. Bộ khởi động giai đoạn 2 được thiết
kế để khởi động hạt nhân SMP cụ thể. Nó chỉ kích hoạt các AP sau khi BSP đã khởi tạo kernel. Khi làm như vậy, nó có thể gửi thông
tin khởi động của các AP vào một khu vực liên lạc khác. Lược đồ này được hầu hết ARM SMP Linux sử dụng bằng cách sử dụng Das Uboot
làm bộ khởi động giai đoạn 2 (Khởi động ARM Linux SMP trên MPcore 2010).

9.6.3 Khởi động SMP trên máy ảo

Nhiều máy ảo ARM (VM) hỗ trợ ARM MPcores cho SMP. Để cụ thể hơn, chúng ta sẽ xem xét máy ảo ARM realview-pbx-a9, dựa trên ARM
Cortex-A9 MPcore với tối đa 4 lõi. Khi một VM mô phỏng QEMU khởi động, QEMU sẽ tải một hình ảnh thực thi, bằng tùy chọn –kernel
IMAGE, về 0x10000 và bắt đầu thực thi HÌNH ẢNH. Hình ảnh được tải có thể là nhân hệ điều hành hoặc bộ khởi động giai đoạn 2. Do
đó, đối với các máy ảo được mô phỏng, QEMU hoạt động giống như một bộ khởi động tích hợp hoặc giai đoạn 1. Nó đặt các AP ở trạng
thái WFI, chờ SGI từ BSP. Trong trường hợp này, CPU sử dụng thanh ghi SYS_FLAGSSET làm vùng liên lạc, nằm ở địa chỉ được ánh xạ
bộ nhớ 0x10000030 trên bo mạch realview-pbx-a9. Để kích hoạt các AP, BSP ghi địa chỉ bắt đầu vào thanh ghi SYS_FLAGSSET, sau đó
phát SGI tới tất cả các AP.

9.7 Ví dụ khởi động ARM SMP

Mặc dù việc khởi động hệ thống SMP khá dễ dàng nhưng hoạt động của hệ thống SMP phức tạp hơn nhiều so với hệ thống đơn bộ xử lý.
Để giúp người đọc hiểu rõ hơn về hoạt động của SMP, trước tiên chúng tôi minh họa trình tự khởi động của hệ thống ARM SMP bằng
một loạt ví dụ. Để cụ thể hơn, chúng tôi sẽ sử dụng máy ảo realview-pbx-a9 được mô phỏng trong QEMU làm nền tảng triển khai.
Machine Translated by Google

342 9 Đa xử lý trong hệ thống nhúng

9.7.1 Ví dụ khởi động ARM SMP 1

Trong ví dụ đầu tiên, được ký hiệu là C9.2, chúng tôi hiển thị số lượng mã tối thiểu cần thiết để khởi động SMP dựa trên ARM
hệ thống. Để đơn giản, chúng tôi sẽ chỉ hỗ trợ các ngắt đầu vào từ UART và các ngắt của bộ định thời. Để hiển thị
đầu ra, chúng tôi cũng triển khai hàm uprintf() để in được định dạng sang cổng UART.
(1). tập tin ts.s:

/****************** ts.s tệp của C9.2 **********************\

.chữ

.code 32

.global reset_handler, vectơ_start, vectơ_end

.global kích hoạt_SCU, get_cpuid

reset_handler: // TẤT CẢ CPU bắt đầu thực thi tại đây

// lấy ID CPU và giữ nó trong R11

MRC p15, 0, r11, c0, c0, 5 // đọc thanh ghi CPU ID vào R11

VÀ r11, r11, #0x03 // chỉ che dấu CPUID

// đặt ngăn xếp SVC

LDR r0, =svc_stack // r0->svc_stack (diện tích 16KB trong t.ld)

chuyển động r1, r11 // r1 = cpuid

thêm r1, r1, #1 // cpuid++

lsl r2, r1, #12 // (cpuid+1)* 4096

thêm r0, r0, r2

mov sp, r0 // SVC sp=svc_stack[cpuid] cao cấp

// chuyển sang chế độ IRQ với các ngắt TẮT

MSR cpsr, #0x92

// đặt ngăn xếp IRQ

LDR r0, =irq_stack // r0->irq_stack (diện tích 16KB trong t.ld)

chuyển động r1, r11

thêm r1, r1, #1

lsl r2, r1, #12 // (cpuid+1) * 4096

thêm r0, r0, r2

mov sp, r0 // IRQ sp=irq_stack[cpuid] cao cấp

// quay lại chế độ SVC với IRQ ON

MSR cpsr, #0x13

cmp r11, #0

AP tốt // chỉ CPU0 sao chép vectơ, gọi main()

Bản sao BL_vector // sao chép vectơ vào địa chỉ 0

BL chính // CPU0 gọi main() trong C

B .

AP: // mỗi AP gọi APstart() trong C

adr r0, APaddr

máy tính ldr, [r0]

APaddr: .word APstart

irq_handler:

phụ lr, lr, #4

stmfd sp!, {r0-r3, r12, lr}

bl irq_chandler // gọi irq_chandler() trong C

ldmfd sp!, {r0-r3, r12, pc}^

vectơ_start:

PC LDR, reset_handler_addr

Máy tính LDR, undef_handler_addr

Máy tính LDR, swi_handler_addr

PC LDR, tìm nạp trước_abort_handler_addr


Machine Translated by Google

9.7 Ví dụ khởi động ARM SMP 343

PC LDR, dữ liệu_abort_handler_addr

B .

Máy tính LDR, irq_handler_addr

PC LDR, fiq_handler_addr

đặt lại_handler_addr: .word reset_handler

undef_handler_addr: .word undef_handler

swi_handler_addr: .word swi_handler

prefetch_abort_handler_addr: .word prefetch_abort_handler

dữ liệu_abort_handler_addr: .word data_abort_handler

irq_handler_addr: .word irq_handler

fiq_handler_addr: .word fiq_handler

vectơ_end:

// các trình xử lý ngoại lệ giả không được sử dụng

undef_handler:

swi_handler:

tìm nạp trước_abort_handler:

dữ liệu_abort_handler:

fiq_handler: B .

kích hoạt_scu: // kích hoạt SCU

MRC p15, 4, r0, c15, c0, 0 // Đọc địa chỉ cơ sở ngoại vi

LDR r1, [r0] // đọc thanh ghi điều khiển SCU

ORR r1, r1, #0x1 // đặt bit0 (Bật bit) thành 1

STR r1, [r0] // ghi lại giá trị đã sửa đổi

BX lr

get_cpuid:

MRC p15, 0, r0, c0, c0, 5 // đọc thanh ghi ID CPU

VÀ r0, r0, #0x03 // mặt nạ trong trường CPUID

Máy tính MOV, LR

// ----------------- cuối tập tin ts.s -------------------

Giải thích về tệp ts.s: Hệ thống được liên kết biên dịch với hình ảnh thực thi t.bin có địa chỉ bắt đầu 0x10000. TRONG

tệp tập lệnh liên kết, t.ld, nó chỉ định svc_stack làm địa chỉ bắt đầu của vùng 16 KB, đây sẽ là ngăn xếp chế độ SVC

của 4 CPU. Tương tự, CPU sẽ sử dụng vùng 16KB tại irq_stack làm ngăn xếp chế độ IRQ của chúng. Vì chúng tôi không có ý định

xử lý bất kỳ loại ngoại lệ nào khác, ngăn xếp chế độ ABT và UND, cũng như các trình xử lý ngoại lệ đều bị bỏ qua. Bức hình

được điều hành theo QEMU như

qemu-system-arm –m realview-pbx-a9 –smp 4 –m 512M –kernel t.bin –serial mon:stdio

Để rõ ràng, loại máy (–m realview-pbx-a9) và số lượng CPU (-smp 4) được in đậm. Khi mà

hệ thống khởi động, hình ảnh thực thi được QEMU tải tới 0x10000 và chạy từ đó. Khi CPU0 bắt đầu thực thi

reset_handler, QEMU cũng đã khởi động các CPU khác (CPU1 đến CPU3) nhưng chúng được giữ ở trạng thái WFI. Vì vậy, chỉ có CPU0

thực thi reset_handler lúc đầu. Nó thiết lập ngăn xếp chế độ SVC và IRQ, sao chép bảng vectơ tới địa chỉ 0 và sau đó gọi

main() trong C. CPU0 lần đầu tiên khởi tạo hệ thống. Sau đó, nó ghi 0x10000 làm địa chỉ bắt đầu của các AP vào SYS_FLAGSSET

đăng ký (tại 0x10000030) và cấp SGI cho tất cả các AP, khiến chúng thực thi từ cùng một mã reset_handler tại

0x10000. Tuy nhiên, dựa trên số ID CPU của chúng, mỗi AP chỉ thiết lập các ngăn xếp và lệnh gọi chế độ SVC và IRQ của riêng nó.

APstart() trong C, bỏ qua mã khởi tạo hệ thống, chẳng hạn như sao chép vectơ, đã được CPU0 thực hiện.

(2). tc: trong main(), CPU0 kích hoạt SCU và định cấu hình GIC để định tuyến các ngắt. Để đơn giản, hệ thống chỉ

hỗ trợ UART0 (0x10009000) và hẹn giờ0 (0x10011000). Trong config_gic(), các ngắt hẹn giờ được định tuyến tới CPU0 và UART

các ngắt được định tuyến đến CPU1, khá tùy ý. Nếu muốn, người đọc có thể xác minh rằng chúng có thể được định tuyến đến bất kỳ CPU nào.
Sau đó CPU0 khởi chạy trình điều khiển thiết bị và khởi động bộ hẹn giờ. Sau đó, nó ghi địa chỉ bắt đầu AP vào 0x10000030 và phát hành

SGI để kích hoạt các AP. Sau đó, tất cả các CPU thực thi theo vòng lặp WFI, nhưng CPU0 và CPU1 có thể phản hồi và xử lý bộ đếm thời gian

và ngắt UART.
Machine Translated by Google

344 9 Đa xử lý trong hệ thống nhúng

/********************* tệp tc của C9.2 ********************** ****/

#include "type.h"

#define GIC_BASE 0x1F000000

int *apAddr = (int *)0x10000030; // đăng ký SYS_FLAGSSET

#include "uart.c"

#include "timer.c"

int copy_vectors(){ // giống như trước }

int APstart() // Mã khởi động AP

int cpuid = get_cpuid(); uprintf("CPU%d

start: ", cpuid); uprintf("CPU%d vào trạng thái

WFI\n", cpuid);

trong khi(1){

asm("WFI");

int config_int(int intID, int targetCPU)

int reg_offset, chỉ mục, địa chỉ;

mức độ ưu tiên char = 0x80;

reg_offset = (intID>>3) & 0xFFFFFFFC;

chỉ mục = intID & 0x1F;

địa chỉ = (GIC_BASE + 0x1100) + reg_offset;

*(int *)address = (1 << chỉ mục);

// đặt mức độ ưu tiên của ID ngắt

reg_offset = (intID & 0xFFFFFFFC);

chỉ mục = intID & 0x3;

địa chỉ = (GIC_BASE + 0x1400) + reg_offset + chỉ mục;

*(char *)address = (char)ưu tiên;

// đặt CPU mục tiêu

địa chỉ = (GIC_BASE + 0x1800) + reg_offset + chỉ mục;

*(char *)address = (char)(1 << targetCPU);

int config_gic()

// thiết lập thanh ghi mặt nạ ưu tiên int

*(int *)(GIC_BASE + 0x104) = 0xFF;

// Kích hoạt thanh ghi điều khiển giao diện CPU để báo hiệu các ngắt

*(int *)(GIC_BASE + 0x100) = 1;

// Cho phép thanh ghi điều khiển nhà phân phối gửi các ngắt tới CPU

*(int *)(GIC_BASE + 0x1000) = 1;

config_int(36, 0); // ID bộ định thời = 36 tới CPU0

config_int(44, 1); // UART0 ID=44 tới CPU1

int irq_chandler()

// đọc ICCIAR của giao diện CPU trong GIC

int intID = *(int *)(GIC_BASE + 0x10C);

nếu (intID == 36)

hẹn giờ_handler(); // ngắt hẹn giờ0

nếu (intID == 44)

uart_handler(0); // ngắt UART0

*(int *)(GIC_BASE + 0x110) = intID; // phát hành EOI

}
Machine Translated by Google

9.7 Ví dụ khởi động ARM SMP 345

int chính()

Enable_scu(); // kích hoạt SCU

uart_init(); // khởi tạo UART

uprintf("CPU0 khởi động\n"); bộ đếm thời

gian_init(); // khởi tạo bộ đếm thời gian

bộ đếm thời gian_start(0); // bắt đầu hẹn giờ

config_gic(); // gửi // cấu hình GIC

SGI tới các AP đánh thức send_sgi(0x00,

0x0F, 0x01); // intID=0,CPU=0xF,filter=0x01

apAddr = (int *)0x10000030; // đăng ký SYS_FLAGSSET

*apAddr = (int)0x10000; // tất cả các AP thực thi từ 0x10000

uprintf("CPU0 nhập vòng lặp WFI: nhập khóa từ UART\n");

trong khi(1)

asm("WFI");

(3). Tệp hẹn giờ.c: tệp này triển khai trình điều khiển hẹn giờ0. Trên mỗi giây, nó hiển thị một dòng tới cổng UART0.

//****************** tập tin time.c của C9.2 *******************

#xác định SỐ 64

bộ đếm thời gian cấu trúc typedef {

u32 TẢI; // Tải thanh ghi

GIÁ TRỊ u32; // Đăng ký giá trị hiện tại

u32 KIỂM SOÁT; // Thanh ghi điều khiển u32 INTCLR; // Ngắt

Xóa thanh ghi

u32 RIS; // Đăng ký trạng thái ngắt thô

u32 MIS; // Đăng ký trạng thái ngắt được che giấu

u32 BGLOAD; // Đăng ký tải nền

}Đồng hồ hẹn giờ;

THỜI GIAN *tp;

UART bên ngoài *lên;

u32 tích tắc = 0, ss = 0;

int time_handler()

int cpuid = get_cpuid();

đánh dấu++;

if (đánh dấu >= CHIA){

đánh dấu = 0; ss++;

nếu (ss==60)

ss = 0;

if (tick==0){ // mỗi giây: hiển thị một dòng

uprintf("TIMER ngắt trên CPU%d : time = %d\r", cpuid, ss);

hẹn giờ_clearInterrupt(0); // xóa ngắt hẹn giờ

int hẹn giờ_init()

tp = (TIMER *)0x10011000; // đặt địa chỉ cơ sở của bộ định thời // đặt các thanh ghi

điều khiển và bộ đếm

tp->TẢI = 0x0;

tp->VALUE= 0xFFFFFFFF; tp->RIS =

0x0; tp->MIS = 0x0;


Machine Translated by Google

346 9 Đa xử lý trong hệ thống nhúng

tp->TẢI = 0x100;

tp-> ĐIỀU KHIỂN = 0x62; // |En|Per|Int|-|Sca|00|32B|Wrap|=01100010

tp->BGLOAD = 0xF0000/DIVISOR;

int hẹn giờ_start()

THỜI GIAN *tpr = tp; tpr-

> KIỂM SOÁT |= 0x80; // đặt bit kích hoạt 7

int time_clearInterrupt()

THỜI GIAN *tpr = tp; tpr-

>INTCLR = 0xFFFFFFFF; // ghi vào thanh ghi INTCLR

(4). Tệp uart.c: đây là trình điều khiển UART. Trình điều khiển đã sẵn sàng cho 4 UART nhưng nó chỉ sử dụng UART0. Để cho ngắn

gọn, mã uprintf() không được hiển thị.

/************ tập tin uart.c của C9.2 *************/

#define UART0_BASE 0x10009000

typedef struct uart{u32 DR; //

đăng ký dữ liệu

u32 DSR;

u32 pad1[4]; // 8+16=24 byte vào thanh ghi FR

u32 FR; // cờ reg ở 0x18

u32 pad2[7];

u32 IMSC; // tại offset 0x38

}UART;

UART *upp[4]; // 4 con trỏ UART

UART *lên; // con trỏ UART hoạt động

int uprintf(char *fmt, …){ // giống như trước }

int uart_handler(int ID)

ký tự c;

int cpuid = get_cpuid();

lên = upp[ID];

c = lên->DR;

uprintf("UART%d ngắt trên CPU%d : c=%c\n", ID, cpuid, c);

int uart_init()

int tôi;

for (i=0; i<4; i++){ // địa chỉ cơ sở của UART

upp[i] = (UART *)(0x10009000 + i*0x1000); upp[i]->IMSC |=

(1<<4); // kích hoạt ngắt UART RXIM

9.7.2 Trình diễn Ví dụ khởi động SMP 1

Hình 9.2 thể hiện các kết quả đầu ra của chương trình Ví dụ khởi động SMP C9.2. Hệ thống có 4 CPU, được xác định bởi CPU0 đến CPU3.

Trong config_gic(), các ngắt hẹn giờ (36) được định tuyến đến CPU0 và các ngắt UART (44) được định tuyến đến CPU1. Tại
Machine Translated by Google

9.7 Ví dụ khởi động ARM SMP 347

Hình 9.2 Ví dụ khởi động ARM SMP 1

mỗi giây, bộ hẹn giờ hiển thị một dòng hiển thị thời gian đã trôi qua tính bằng giây. Tất cả các CPU thực thi theo vòng lặp WFI,
nhưng CPU0 và CPU1 có thể phản hồi và xử lý các ngắt. Như một bài tập, người đọc có thể sửa đổi mã config_gic() để định tuyến
các ngắt đến các CPU khác nhau nhằm quan sát hiệu ứng.

9.7.3 Ví dụ khởi động ARM SMP 2

Trong chương trình ví dụ khởi động ARM SMP thứ hai, ký hiệu là C9.3, chúng tôi thêm trình điều khiển LCD và bàn phím vào chương
trình để có giao diện và hiển thị người dùng tốt hơn. Đây là các trình điều khiển bàn phím và LCD tương tự được phát triển
trong các chương trước cho bo mạch ARM Versatilepb, ngoại trừ những khác biệt nhỏ sau đây. Trên bo mạch ARM realview-pbx-a9,
các thông số thời gian hiển thị LCD vẫn giống như trước nhưng địa chỉ cơ sở của nó là 0x10120000. Đối với bàn phím, địa chỉ
cơ sở (tại 0x1000600) vẫn giống như trước nhưng nó sử dụng số ngắt GIC 52 do bảng realview-pbx-a9 không có bộ điều khiển ngắt
VIC và SIC. Khi các AP khởi động, chúng tôi cho phép mỗi AP in một vài dòng lên màn hình LCD. Để cho ngắn gọn, chúng tôi chỉ
hiển thị tệp tc đã sửa đổi.

/********* tc file của C9.3: Ví dụ khởi động SMP 2 ********/ #include "type.h"

extern int uprintf(char


*fmt, …); extern int printf(char *fmt, …);

#define GIC_BASE 0x1F000000

int *apAddr = (int *)0x10000030;

#include "uart.c"
#include "timer.c"

#include "kbd.c" // thư mục KBD


#include "vid.c" // Trình điều khiển LCD

int copy_vectors(){// giống như trước }

int sen_sgi(int filter,int targetCPU, int intID){//giống như trước}

int APstart()
{

int i, cpuid = get_cpuid();


printf("CPU%d start\n", cpuid);
cho (i=0; i<2; i++){

printf("CPU%d trước trạng thái WFI i=%d\n",cpuid, i);


}

printf("CPU%d vào trạng thái WFI\n", cpuid);


trong khi(1){

asm("WFI");
}
}
Machine Translated by Google

348 9 Đa xử lý trong hệ thống nhúng

int config_gic()

// đặt thanh ghi mặt nạ ưu tiên int *(int *)

(GIC_BASE + 0x104) = 0xFF; // thiết lập thanh ghi

điều khiển giao diện CPU: cho phép ngắt tín hiệu *(int *)(GIC_BASE + 0x100) = 1; // thanh ghi điều khiển

phân phối để gửi các ngắt đang chờ xử lý tới CPU

*(int *)(GIC_BASE + 0x1000) = 1;

config_int(36, 0); // bộ đếm thời gian ngắt t0 CPU0

config_int(44, 1); // UART0 ngắt tới CPU1

config_int(52, 2); // KBD ngắt tới CPU2

int config_int(int intID, int targetCPU){// GIỐNG NHƯ trong ví dụ 1}

int irq_chandler()

// đọc ICCIAR của giao diện CPU trong GIC

int intID = *(int *)(GIC_BASE + 0x10C);

nếu (intID == 36)

hẹn giờ_handler();

nếu (intID == 44)

uart_handler(0);

nếu (intID == 52) // Trình xử lý ngắt KBD

kbd_handler();

*(int *)(GIC_BASE + 0x110) = intID; // phát hành EOI

int chính()

Enable_scu();

fbuf_init(); // khởi tạo màn hình LCD

printf("********* Ví dụ khởi động ARM SMP 2 *************\n"); kbd_init(); uart_init();

printf("CPU0

khởi động\n"); bộ

đếm thời gian_init();

bộ đếm thời gian_start(0);

config_gic();

send_sgi(0x00, 0x0F, 0x01); // intID=0,CPUs=0xF,filter=b01

apAddr = (int *)0x10000030;

*apAddr = (int)0x10000;

printf("Vòng lặp CPU0 nhập while(1). Nhập khóa từ KBD:\n");

uprintf("Nhập key từ thiết bị đầu cuối UART:\n");

trong khi(1)

asm("WFI");

9.7.4 Trình diễn ví dụ khởi động ARM SMP 2

Hình 9.3 cho thấy kết quả đầu ra khi chạy chương trình ví dụ C9.3. Như hình minh họa, đầu ra của các AP được xen kẽ.
Ví dụ: trước khi CPU2 in xong, CPU3 cũng bắt đầu in, dẫn đến kết quả đầu ra hỗn hợp. Nếu chúng ta để AP in nhiều dòng
hơn, một số dòng có thể bị cắt xén. Điều này là do CPU có thể ghi vào cùng một vị trí trong bộ nhớ màn hình LCD. Đây là
hiện tượng điển hình khi các CPU khác nhau thực thi song song. CPU có thể truy cập và
Machine Translated by Google

9.7 Ví dụ khởi động ARM SMP 349

Hình 9.3 Ví dụ khởi động ARM SMP 2

sửa đổi vị trí bộ nhớ chia sẻ theo bất kỳ thứ tự nào. Khi nhiều CPU cố gắng sửa đổi cùng một vị trí bộ nhớ, nếu kết quả phụ thuộc vào thứ tự

thực hiện thì đó được gọi là tình trạng dồn đuổi. Trong hệ thống SMP, các điều kiện tương tranh không được tồn tại vì chúng có thể làm hỏng

các đối tượng dữ liệu dùng chung, dẫn đến kết quả không nhất quán và khiến hệ thống gặp sự cố. Các ví dụ trên nhằm mục đích chỉ ra rằng mặc dù

rất dễ dàng khởi động nhiều CPU trong hệ thống SMP nhưng chúng ta phải kiểm soát việc thực thi của chúng để đảm bảo tính toàn vẹn của các đối

tượng dữ liệu được chia sẻ. Điều này khiến chúng ta phải xem xét lại vấn đề đồng bộ hóa quy trình, điều cần thiết cho mọi hệ thống SMP.

9.8 Vùng quan trọng trong SMP

Trong hệ thống SMP, mỗi CPU thực thi một quy trình, quy trình này có thể truy cập cùng các đối tượng dữ liệu trong bộ nhớ dùng chung. Vùng

quan trọng (CR) (Silberschatz và cộng sự 2009; Stallings 2011) là một chuỗi các lần thực thi trên các đối tượng dữ liệu được chia sẻ mà chỉ

có thể được thực hiện bởi một quy trình tại một thời điểm. Các vùng quan trọng thực hiện nguyên tắc loại trừ lẫn nhau của quá trình, đây là cơ

sở của việc đồng bộ hóa quy trình. Vì vậy vấn đề cơ bản là làm thế nào để triển khai các vùng quan trọng trong hệ thống SMP.

9.8.1 Triển khai các vùng quan trọng trong SMP

Giả sử rằng x là một vị trí bộ nhớ có thể định địa chỉ, ví dụ một byte hoặc một từ. Trong mọi hệ thống máy tính, read(x) và write(x) là các

phép toán nguyên tử. Cho dù có bao nhiêu CPU cố gắng đọc hoặc ghi cùng một x, thậm chí cùng một lúc, bộ điều khiển bộ nhớ chỉ cho phép một CPU

truy cập x tại một thời điểm. Khi CPU bắt đầu đọc hoặc ghi x, nó sẽ hoàn thành thao tác trước khi bất kỳ CPU nào khác được phép truy cập vào

cùng x. Tuy nhiên, tính nguyên tử riêng lẻ của read(x) và write(x) không đảm bảo rằng x có thể được cập nhật chính xác. Điều này là do việc

cập nhật x yêu cầu đọc(x) và sau đó viết(x) theo hai bước. Giữa hai bước, các CPU khác có thể xen vào, đọc giá trị cũ chưa được cập nhật của

x hoặc ghi các giá trị vào x sẽ bị ghi đè. Để khắc phục sự cố này, các CPU được thiết kế cho đa xử lý thường hỗ trợ lệnh Kiểm tra và Thiết

lập (TS) hoặc lệnh tương đương, hoạt động như sau. Giả sử một lần nữa rằng x là một vị trí bộ nhớ có thể định địa chỉ và ban đầu x=0. Lệnh

TS(x) thực hiện chuỗi hành động sau đây trên x dưới dạng một thao tác không thể phân chia (nguyên tử).

TS(x) = {đọc x từ bộ nhớ; kiểm tra x cho 0 hoặc 1; viết 1 vào x}


Machine Translated by Google

350 9 Đa xử lý trong hệ thống nhúng

Cho dù có bao nhiêu CPU cố gắng thực hiện TS(x), thậm chí cùng lúc, chỉ một CPU có thể đọc x là 0, tất cả các CPU khác sẽ
đọc x là 1. Trong CPU Intel x86, lệnh tương đương là XCHG, trao đổi một thanh ghi CPU có vị trí bộ nhớ trong một hoạt động
không thể phân chia. Trong các phiên bản trước của CPU ARM (trước ARMv6), lệnh tương đương là SWAP, hoán đổi thanh ghi CPU
với một vị trí bộ nhớ trong một thao tác không thể phân chia duy nhất. Với TS hoặc các hướng dẫn tương đương, chúng ta có
thể triển khai CR được liên kết với x như sau.

Byte x = 0;
(1). int SR = int_off();
(2). trong khi(TS(x));
-------------------

(3). | Khu vực quan trọng |


-------------------

(4). x = 0;
(5). int_on(SR);

Trên mọi CPU, quá trình chuyển đổi thường được kích hoạt bởi các ngắt. Bước (1) vô hiệu hóa các ngắt CPU để ngăn chặn
việc chuyển đổi quy trình. Điều này đảm bảo rằng mỗi tiến trình tiếp tục chạy trên CPU mà không bị tắt. Ở Bước (2), quy trình
lặp lại cho đến khi TS(x) nhận giá trị 0. Trong số các CPU đang cố gắng thực thi TS(x), chỉ một CPU có thể nhận được 0 để
nhập CR ở Bước (3). Tất cả các CPU khác sẽ nhận giá trị 1 và tiếp tục thực hiện vòng lặp while. Do đó, chỉ có một tiến trình
có thể ở bên trong CR bất cứ lúc nào. Khi quá trình kết thúc CR, nó sẽ xóa x thành 0, cho phép một quá trình khác đi qua Bước
(2) để nhập CR. Ở Bước (5), quy trình khôi phục thanh ghi trạng thái CPU ban đầu, có thể cho phép các ngắt để cho phép chuyển
đổi quy trình trên CPU, nhưng quy trình đã thoát khỏi CR.

9.8.2 Những thiếu sót trong hoạt động XCHG/SWAP

Mặc dù các hướng dẫn giống TS có thể và đã được sử dụng để triển khai CR trong hệ thống SMP (Wang 2015), nhưng chúng cũng có
một số thiếu sót.

(1). Khi CPU bắt đầu thực hiện lệnh giống TS, toàn bộ bus bộ nhớ có thể bị khóa cho đến khi lệnh hoàn thành. Trong khi đó,
không có CPU nào khác có thể truy cập vào bộ nhớ, điều này làm giảm khả năng xử lý đồng thời.
(2). Khi CPU bắt đầu thực thi một lệnh giống TS, nó phải hoàn thành lệnh trước khi có thể nhận bất kỳ lệnh ngắt nào, điều này
làm tăng độ trễ xử lý ngắt.
(3). Giả sử rằng CPU đã đặt vị trí bộ nhớ thành 1 và đang thực thi bên trong CR. Trước khi CPU đặt lại vị trí bộ nhớ về 0,
tất cả các CPU khác cố gắng đặt nó về 1 phải liên tục thực hiện lệnh giống TS. Điều này làm tăng mức tiêu thụ điện năng, điều
không mong muốn trong các hệ thống nhúng và di động.

9.8.3 Hướng dẫn đồng bộ hóa ARM cho SMP

Để khắc phục những thiếu sót của các lệnh giống TS, ARM đã giới thiệu một số lệnh mới trong bộ xử lý MPcore của họ để đồng bộ
hóa quy trình trong SMP.

9.8.3.1 Hướng dẫn LDREX/STREX ARM

LDREX: Lệnh LDREX tải một từ từ bộ nhớ, được đánh dấu để truy cập độc quyền. Nó cũng khởi tạo trạng thái của các đơn vị
phần cứng liên quan, được gọi là màn hình độc quyền, để theo dõi các hoạt động cập nhật trên các vị trí bộ nhớ đó.

STREX: Lệnh STREX cố gắng lưu trữ một từ vào một vị trí bộ nhớ được đánh dấu là quyền truy cập độc quyền. Nếu màn hình độc
quyền cho phép lưu trữ, nó sẽ cập nhật vị trí bộ nhớ và trả về 0, cho biết thao tác đã thành công. Nếu màn hình độc quyền
không cho phép lưu trữ, nó sẽ không cập nhật vị trí bộ nhớ và trả về 1, cho biết thao tác không thành công. Trong trường hợp
sau, CPU có thể thử lại strex sau hoặc thực hiện các hành động thay thế.
Machine Translated by Google

9.8 Vùng quan trọng trong SMP 351

LDREX-STREX chia hoạt động giống TS cổ điển một cách hiệu quả thành hai bước riêng biệt. Với sự trợ giúp của các màn hình độc

quyền, chúng hỗ trợ cập nhật bộ nhớ nguyên tử trong bộ xử lý ARM MPcore. Bất kể có bao nhiêu CPU cố gắng cập nhật cùng một vị trí bộ

nhớ, chỉ một CPU có thể thành công. Vì CPU thực thi LDREX-STREX theo hai bước nên nó không phải liên tục thực hiện chuỗi lệnh cho đến

khi thành công. Sau LDREX, nếu CPU nhận thấy vị trí bộ nhớ đã là 1, thay vì thử lại nhiều lần, CPU có thể thực hiện các hành động thay

thế. Cách tốt hơn là đặt CPU ở chế độ tiết kiệm năng lượng cho đến khi có tín hiệu lên, lúc đó nó có thể cố gắng thiết lập lại vị trí

bộ nhớ. Với mục đích này, ARM đã giới thiệu các hướng dẫn WFI, WFE và SEV, có thể được sử dụng như sau.

9.8.3.2 Hướng dẫn ARM WFI, WFE, SEV


WFI (Chờ ngắt): CPU chuyển sang chế độ tiết kiệm năng lượng, chờ bất kỳ ngắt nào để đánh thức nó.

WFE (Wait-for-Event): CPU chuyển sang chế độ tiết kiệm năng lượng, chờ đợi bất kỳ sự kiện nào, trong đó bao gồm các ngắt và sự kiện

do CPU khác gây ra, để đánh thức nó.

SEV (Gửi sự kiện); Gửi một sự kiện để đánh thức các CPU khác ở chế độ WFE.

Các hướng dẫn trên sẽ hoạt động nếu CPU và trình biên dịch (tối ưu hóa) không thay đổi thứ tự thực hiện lệnh của chương trình.

Tuy nhiên, mã được tạo bởi chuỗi công cụ ARM và bản thân CPU ARM có thể sắp xếp lại thứ tự thực thi lệnh, điều này có thể dẫn đến

việc truy cập bộ nhớ không theo thứ tự khác với trình tự lệnh trong chương trình. Trong hệ thống SMP, việc truy cập bộ nhớ không theo

thứ tự có thể tạo ra kết quả không nhất quán. Để đảm bảo chế độ xem nhất quán về nội dung bộ nhớ từ các thực thể thực thi khác nhau,

ARM đã đưa ra các rào cản bộ nhớ.

9.8.3.3 Rào cản bộ nhớ ARM Rào cản bộ

nhớ là một loại lệnh khiến CPU thực thi ràng buộc thứ tự đối với các hoạt động bộ nhớ được đưa ra trước và sau lệnh rào cản. Điều

này thường có nghĩa là các hoạt động được thực hiện trước rào cản được đảm bảo thực hiện trước các hoạt động sau rào cản. Rào cản

bộ nhớ ARM bao gồm những điều sau đây.

DMB (Rào cản bộ nhớ dữ liệu): DMB hoạt động như một rào cản bộ nhớ. Nó đảm bảo rằng tất cả các lần truyền bộ nhớ dữ liệu rõ ràng

trước DMB được hoàn thành trước bất kỳ giao dịch bộ nhớ dữ liệu rõ ràng nào tiếp theo sau khi DMB khởi động. Điều này đảm bảo thứ tự

chính xác giữa hai lần truy cập bộ nhớ.

DSB (Rào cản đồng bộ hóa dữ liệu): DSB hoạt động như một loại rào cản bộ nhớ đặc biệt. Lệnh DSB đảm bảo tất cả việc truyền dữ liệu

rõ ràng trước DSB được hoàn thành trước bất kỳ lệnh nào sau khi DSB được thực thi. Lệnh hoàn thành khi tất cả các truy cập bộ nhớ

rõ ràng trước khi lệnh này hoàn thành và tất cả các hoạt động bảo trì Cache, Branch và TLB trước khi lệnh này hoàn thành.

ISB (Rào cản đồng bộ hóa lệnh): ISB xóa đường dẫn trong bộ xử lý để tất cả các lệnh theo ISB được tìm nạp từ bộ đệm hoặc bộ nhớ

sau khi lệnh được hoàn thành. Nó đảm bảo rằng tác động của các hoạt động thay đổi ngữ cảnh, chẳng hạn như thay đổi ASID hoặc các hoạt

động bảo trì TLB đã hoàn thành hoặc các hoạt động bảo trì bộ dự đoán nhánh, cũng như tất cả các thay đổi đối với các thanh ghi CP15,

được thực hiện trước lệnh ISB đều hiển thị với các lệnh được tìm nạp sau đó. ISB. Ngoài ra, lệnh ISB đảm bảo rằng mọi nhánh xuất

hiện theo thứ tự chương trình sau lệnh ISB luôn được ghi vào logic dự đoán nhánh với ngữ cảnh hiển thị sau lệnh ISB. Điều này đảm

bảo thực hiện đúng luồng lệnh.

9.9 Nguyên tắc đồng bộ hóa trong SMP

Nguyên thủy đồng bộ hóa là các công cụ phần mềm để đồng bộ hóa quy trình. Có nhiều loại nguyên thủy đồng bộ hóa khác nhau, từ các khóa

xoay đơn giản đến các cấu trúc đồng bộ hóa cấp cao rất phức tạp, chẳng hạn như Biến điều kiện và Màn hình, v.v. (Wang 2015). Sau đây,

chúng ta sẽ chỉ thảo luận về các nguyên tắc đồng bộ hóa cơ bản được sử dụng phổ biến nhất trong các hệ điều hành SMP.

9.9.1 Khóa xoay

Loại nguyên thủy đồng bộ hóa đơn giản nhất là spinlock, là khóa khái niệm được sử dụng để bảo vệ các Vùng quan trọng (CR) trong thời

gian ngắn. Để truy cập CR, trước tiên một tiến trình phải có được spinlock liên kết với CR. Nó liên tục cố gắng lấy được spinlock

cho đến khi thành công, do đó có tên là spinlock. Sau khi kết thúc với CR, tiến trình sẽ giải phóng
Machine Translated by Google

352 9 Đa xử lý trong hệ thống nhúng

spinlock, cho phép một tiến trình khác lấy được spinlock để vào CR. Hoạt động Spinlock bao gồm hai hoạt động chính
chức năng.

slock(int *spin) : thu được một spinlock được trỏ bởi spin

sunlock(int *spin): giải phóng một spinlock được trỏ bởi spin

Chúng được gọi lần lượt là slock(&spin) và sunlock(&spin), trong đó spin biểu thị một spinlock được khởi tạo cho

Trạng thái MỞ KHÓA. Các đoạn mã sau đây cho thấy việc triển khai các hàm spinlock trong tập hợp ARM.

MỞ KHÓA = 0

BỊ KHÓA = 1

int quay = 0; // spinlock, 0=MỞ KHÓA, 1=ĐÃ KHÓA

slock: // slock(int *spin): lấy spinlock

ldrex r1, [r0] // đọc giá trị spinlock

cmp r1, #0x0 // so sánh với 0

WENE // không phải 0 nghĩa là đã bị khóa: làm WFE

bn slock // thử lại sau khi bị đánh thức bởi sự kiện

mov r1, #1 // đặt r1=1

strex r2, r1, [r0] // thử lưu 1 vào [r0]; r2=giá trị trả về

cmp r2, #0x0 // kiểm tra giá trị trả về trong r2

bn slock // không phải 0 nghĩa là thất bại; thử khóa lại lần nữa

DMB // rào cản bộ nhớ TRƯỚC KHI truy cập CR

bx lr // chỉ trả về nếu đã có được spinlock

nắng: // sunlock(int *spin)

mov r1, #0x0 // đặt r1=0

DMB // rào cản bộ nhớ TRƯỚC KHI giải phóng CR

str r1, [r0] // lưu 0 vào [r0]

DSB // đảm bảo cập nhật hoàn tất trước SEV

SEV // báo hiệu sự kiện để đánh thức CPU ở chế độ WFE

bx lr // trở lại

9.9.2 Ví dụ về khóa spin

Trong các chương trình ví dụ khởi động SMP, CPU0 là BSP, khởi tạo hệ thống trước tiên. Sau đó, nó đánh thức các AP khác bằng cách

gửi cho họ SGI. Khi thức dậy, tất cả các AP đều thực thi cùng một mã APstart(). Như Hình 9.3 cho thấy, kết quả đầu ra của họ trên

Màn hình LCD rất có thể được trộn lẫn với nhau. Ví dụ: lời nhắc đầu vào của CPU0 xuất hiện ngay cả trước khi tất cả các AP được đánh thức

hướng lên. Trong thực tế, BSP nên đợi cho đến khi tất cả các AP sẵn sàng trước khi tiếp tục. Để đạt được điều này, chúng ta có thể khởi tạo một

(toàn cầu) biến ncpu=1 và yêu cầu mỗi AP tăng ncpu thêm 1 khi nó hoạt động và sẵn sàng. Sau khi gửi SGI để đánh thức

Các AP, BSP thực hiện vòng lặp chờ bận while(ncpu < 4); trước khi hiển thị lời nhắc nhập liệu. Tuy nhiên, nếu không có sự thích hợp

đồng bộ hóa, sơ đồ này có thể không hoạt động vì các AP có thể cập nhật ncpu theo thứ tự tùy ý do điều kiện xung đột. Các

giá trị cuối cùng của ncpu có thể không bằng 4, khiến BSP bị kẹt trong vòng lặp while. Tất cả những vấn đề này có thể được loại bỏ bằng cách thêm một

spinlock để đảm bảo rằng các AP thực thi APstart() lần lượt. Để làm điều này, chúng tôi xác định các biến toàn cục, spin như một spinlock,

ncpu làm bộ đếm và sửa đổi mã APstart() và tc như sau.

int quay = 0; // khóa quay

biến động int ncpu = 1; // số lượng CPU sẵn sàng

int APstart() // mã để AP thực thi

{
int cpuid = get_cpuid();

slock(&spin);

printf("CPU%d trong APstart\n", cpuid);


Machine Translated by Google

9.9 Nguyên tắc đồng bộ hóa trong SMP 353

ncpu++;

printf("CPU%d vào vòng lặp WFI ncpu=%d\n", cpuid, ncpu); khóa nắng(&spin);

trong khi(1)

asm("WFI");

chủ yếu() // được thực thi bởi CPU0

// cùng mã như trước

while(ncpu < 4); // đợi cho đến khi tất cả các AP sẵn sàng printf("nhập khóa

từ KBD hoặc UART: ");

while(1) asm("WFI");

9.9.3 Trình diễn khởi động SMP với Spinlock

Sau đó, chúng tôi biên dịch lại chương trình đã sửa đổi, ký hiệu là C9.4 và chạy lại chương trình đó. Hình 9.4 cho thấy kết quả đầu ra

mẫu của chương trình C9.4 đã sửa đổi. Như hình minh họa, tất cả các đầu ra hiện theo thứ tự CPU.

9.9.4 Mutex trong SMP

Mutex là một công cụ đồng bộ hóa phần mềm được sử dụng để khóa/mở khóa các vùng quan trọng trong thời gian dài. Một mutex chứa trường
khóa, cho biết mutex đang ở trạng thái khóa hay mở khóa và trường chủ sở hữu, xác định thực thể thực thi, ví dụ: một tiến trình hoặc ID

CPU, hiện đang giữ khóa mutex. Một mutex đã mở khóa không có chủ sở hữu. Nếu một thực thể thực thi đã khóa thành công một mutex thì nó sẽ

trở thành chủ sở hữu. Chỉ chủ sở hữu mới có thể mở khóa một mutex bị khóa. Dạng đơn giản nhất của mutex chỉ là một biến số nguyên (toàn

cục) hiển thị cho tất cả các thực thể thực thi. Giả sử rằng tất cả các ID thực thể thực thi đều >= 0. Một mutex có ID=-1 không hợp lệ biểu

thị trạng thái được mở khóa và ID không âm biểu thị trạng thái bị khóa cũng như chủ sở hữu của mutex. Để bắt đầu, chúng ta sẽ giả sử một

dạng mutex đơn giản như vậy với CPU là các thực thể thực thi.

cấu trúc typedef mutex{

khóa int;

}MUTEX; MUTEX m; // m là một mutex

Các đoạn mã sau đây trong tập hợp ARM hiển thị việc triển khai các hoạt động mutex, bao gồm ba
các hàm, mutex_init, mutex_lock và mutex_unlock.

Hình 9.4 Chương trình khởi động SMP với spinlock


Machine Translated by Google

354 9 Đa xử lý trong hệ thống nhúng

MỞ KHÓA=0xFFFFFFFF // giống như -1

mutex_init: // init_mutex(MUTEX *m)

MOV r1, #MỞ KHÓA // Đánh dấu là đã mở khóa

STR r1, [r0]

BX lr

mutex_lock: // int mutex_lock(MUTEX *m)

LDREX r1, [r0] // Đọc trường khóa

CMP r1, #MỞ KHÓA // So sánh với UNLOCKED

WENE // Nếu đã bị khóa, WFE

BNE mutex_lock // Khi thức dậy: thử lại lần nữa

// Cố gắng khóa mutex bằng ID CPU

MRC p15, 0, r1, c0, c0, 5 // Đọc thanh ghi ID CPU

VÀ r1, r1, #0x03 // Mặt nạ trong trường ID CPU.

STREX r2, r1, [r0] // Cố gắng ghi ID CPU vào m->lock

CMP r2, #0x0 // Kiểm tra giá trị trả về: 0=OK, 1=failed

BNE mutex_lock // Nếu lưu trữ thất bại, hãy thử lại

DMB

BX lr // trở lại

mutex_unlock: // int mutex_unlock(MUTEX *m)

MRC p15, 0, r1, c0, c0, 5 // Đọc thanh ghi ID CPU tới r1

VÀ r1, r1, #0x03 // Mặt nạ trong trường ID CPU r1


LDR r2, [r0] // Đọc trường khóa của mutex

CMP r1, r2 // So sánh CPU ID với chủ sở hữu mutex

MOVNE r0, #0x1 // Nếu không phải chủ sở hữu: trả về 1 nếu thất bại

BXNE lr

DMB // Đảm bảo tất cả quyền truy cập vào tài nguyên được chia sẻ đã hoàn thành

MOV r1, #đã mở khóa // Viết "đã mở khóa" vào trường khóa

STR r1, [r0]

DSB // Đảm bảo cập nhật hoàn tất trước khi đánh thức các CPU khác

SEV // Gửi sự kiện tới các CPU khác đang chờ trong WFE

MOV r0, #0x0 // Trả về 0 nếu thành công

BX lr

Có thể thấy, việc triển khai các thao tác mutex tương tự như thao tác spinlock. Cả hai đều dựa vào cập nhật nguyên tử và

hoạt động rào cản bộ nhớ. Sự khác biệt duy nhất là một mutex bị khóa có chủ sở hữu, chỉ có thể được mở khóa bởi

Người chủ hiện tại. Ngược lại, spinlock không có chủ sở hữu, do đó bất kỳ thực thể thực thi nào cũng có thể mở khóa spinlock ngay cả khi nó

không có khóa xoay, điều này có thể dẫn đến việc sử dụng sai khóa xoay.

9.9.5 Triển khai Mutex bằng Spinlock

Ngoài ra, một mutex có thể được coi là một cấu trúc bao gồm các trường sau.

cấu trúc typedef mutex{

khóa int; // spinlock để truy cập mutex này

trạng thái int; // 0=ĐÃ MỞ KHÓA, 1=ĐÃ KHÓA

chủ sở hữu int; // ID chủ sở hữu; -1=CHỦ SỞ HỮU

}MUTEX; MUTEX m;

Trường khóa là một spinlock, đảm bảo rằng mọi thao tác trên mutex chỉ có thể được thực hiện bên trong CR của

spinlock của mutex, trường trạng thái biểu thị trạng thái hiện tại của mutex, ví dụ 0=UNLOCKED, 1=LOCKED và chủ sở hữu

trường xác định thực thể thực thi hiện tại chứa mutex. Với trường spinlock, các thao tác mutex có thể được thực hiện
Machine Translated by Google

9.9 Nguyên tắc đồng bộ hóa trong SMP 355

dựa trên slock()/sunlock() trên spinlock. Các đoạn mã sau đây hiển thị cách triển khai các thao tác mutex bằng ngôn ngữ cấp
cao C.

#define NO_OWNWR -1

int mutex_init(MUTEX *m)

{ m->khóa = 0; m->trạng thái = MỞ KHÓA; m->chủ sở hữu = NO_OWNER; }

int mutex_lock(MUTEX *m)

trong khi(1){

slock(&m->lock); // thu được spinlock

if (m->status == MỞ KHÓA)

phá vỡ;

khóa nắng(&m->lock); // giải phóng spinlock

asm("WFE"); // đợi sự kiện rồi thử lại

// hiện đang giữ spinlock, cập nhật mutex trong CR

m->trạng thái = KHÓA;

m->chủ sở hữu = get_cpuid();

khóa nắng(&m->lock); // giải phóng spinlock

int mutex_unlock(MUTEX *m)

slock(&m->lock); // thu được spinlock

if (m->owner != get_cpuid()){

khóa nắng(&m->lock); // nếu không phải chủ sở hữu: giải phóng spinlock

trả về -1; // trả về -1 nếu THẤT BẠI

m->trạng thái = MỞ KHÓA; // đánh dấu mutex là UNLOCKED

m->chủ sở hữu = NO_OWNER; // và không có chủ sở hữu

khóa nắng(&m->lock); // giải phóng spinlock

trả về 0; // trả về 0 cho THÀNH CÔNG

Việc triển khai các thao tác mutex ở trên trong C có thể kém hiệu quả hơn một chút so với trong hợp ngữ, nhưng nó có
hai ưu điểm. Đầu tiên, nó dễ hiểu hơn và do đó ít có khả năng xảy ra lỗi hơn vì mã C luôn dễ đọc hơn mã hợp ngữ. Thứ hai
và quan trọng hơn, nó cho thấy mối quan hệ phân cấp giữa các loại nguyên thủy đồng bộ hóa khác nhau. Khi chúng tôi có khóa
spin để bảo vệ CR trong thời gian ngắn, chúng tôi có thể sử dụng chúng làm cơ sở để triển khai các mutex, về cơ bản là CR
có thời lượng dài hơn. Tương tự như vậy, chúng tôi cũng có thể sử dụng spinlocks để triển khai các công cụ đồng bộ hóa
mạnh mẽ hơn khác. Trong phương pháp phân cấp, chúng tôi không phải lặp lại trình tự ldrex-strex và rào cản bộ nhớ cấp thấp
trong các công cụ đồng bộ hóa mới.

9.9.6 Trình diễn Khởi động SMP bằng Khóa Mutex

Trong chương trình ví dụ C9.5, chúng ta sẽ sử dụng mutex để đồng bộ hóa việc thực thi các AP. Hiệu ứng này cũng giống như
việc sử dụng spinlocks, cả hai đều thực thi từng AP thực thi APstart() một lần.

/****************** tệp tc của C9.5 *******************/

#include "mutex.c"

MUTEX m;

biến động int ncpu = 1;

int APstart()
Machine Translated by Google

356 9 Đa xử lý trong hệ thống nhúng

int tôi;

int cpuid = get_cpuid();

mutex_lock(&m);

printf("CPU%d start\n", cpuid); for (i=0; i<4;

i++){ // mỗi AP in các dòng bên trong CR

printf("CPU%d trước trạng thái WFI i=%d\n", cpuid, i);

printf("CPU%d vào trạng thái WFI\n", cpuid, cpuid);

ncpu++; // mỗi AP tăng ncpu thêm 1 bên trong CR

mutex_unlock(&m);

trong khi(1)

asm("WFI");

int chính()

// mã tương tự như trong ví dụ 2

send_sgi(0x0, 0x0F, 0x01); // intID=0,targetList=0xF,filter=0x01

apAddr = (int *)0x10000030;

*apAddr = (int)0x10000;

while(ncpu < 4); // đợi cho đến khi tất cả các AP sẵn sàng

printf("CPU0 nhập vòng lặp while(1). Nhập khóa từ KBD:\n");

uprintf("Nhập key từ thiết bị đầu cuối UART:\n");

trong khi(1);

Hình 9.5 cho thấy các kết quả đầu ra của Chương trình khởi động ARM SMP sử dụng mutex để đồng bộ hóa CPU. Như có thể
thấy trong hình, trong khi giữ khóa mutex, mỗi CPU có thể in hoặc làm bất cứ điều gì cho vấn đề đó mà không bị các CPU
khác can thiệp.

Hình 9.5 Ví dụ khởi động ARM SMP bằng Mutex


Machine Translated by Google

9.10 Bộ hẹn giờ toàn cầu và cục bộ 357

9.10 Bộ hẹn giờ toàn cầu và cục bộ

Bảng Realview-pbx hỗ trợ một số loại bộ hẹn giờ khác nhau. Bao gồm các

Hẹn giờ toàn cầu : bộ đếm thời gian toàn cầu 64 bit chung cho tất cả các CPU

Bộ tính giờ địa phương : mỗi CPU có bộ định thời cục bộ 32 bit

Bộ hẹn giờ ngoại vi: 4 bộ hẹn giờ ngoại vi

Bộ định thời toàn cầu là bộ đếm thời gian đếm ngược 64 bit, có thể được sử dụng làm nguồn định thời duy nhất cho tất cả
các CPU trong hệ thống ARM MPcore. Mỗi CPU có một thanh ghi so sánh 64-bit riêng. Khi số lượng bộ đếm thời gian toàn cầu đạt
đến giá trị so sánh cục bộ, nó sẽ tạo ra một ngắt có ID 27. Cho đến nay, chúng ta chỉ sử dụng bộ định thời ngoại vi. Sau đây,
chúng ta sẽ sử dụng bộ định thời cục bộ của CPU cho các hoạt động SMP. Chương trình ví dụ tiếp theo, C9.6, trình bày bộ định
thời cục bộ của CPU. Bộ đếm thời gian cục bộ của mỗi CPU nằm ở Địa chỉ cơ sở ngoại vi + 0x600. Nó ngắt với ID 29. Mỗi CPU phải
cấu hình GIC để định tuyến ngắt 29 đến chính nó. Đối với mỗi CPU, chúng ta sẽ hiển thị đồng hồ treo tường dựa trên bộ đếm thời
gian cục bộ của nó ở góc trên bên phải của màn hình LCD. Phần sau đây hiển thị các đoạn mã triển khai bộ tính giờ cục bộ.

/*************** tệp ptimer.c của C9.6 *******************/

#xác định SỐ 64

#define THỜI GIAN 0x6000000/PHÂN

int plock = 0;

ptimer cấu trúc typedef {

tải u32; // Tải thanh ghi 0x00

số u32; // Số hiện tại Đăng ký 0x04

điều khiển u32; // Thanh ghi điều khiển u32 intclr; // 0x08

Ngắt xóa thanh ghi 0x0C

}PTIMER;

PTIMER *ptp;

// hẹn giờ riêng tại BASE+0x600 ngắtID=29 int printf(char *fmt, …);

đồng hồ char[4][16];
"
char *khoảng trống = : : ";

// 01234567

cấu trúc tt{

int hh, mm, ss, tích tắc;

}tt[4];

int ptimer_handler() // trình xử lý ngắt hẹn giờ cục bộ

int tôi, id;

cấu trúc tt *tp;

int cpuid = id = get_cpuid(); tp = &tt[cpuid];

slock(&plock);

tp->đánh dấu++;

if (tp->tick >= DIVISOR){

tp->tick = 0; tp->ss++;

nếu (tp->ss==60){

tp->ss = 0; tp->mm++;

nếu (tp->mm==60){

tp->mm = 0; tp->hh++;

}
Machine Translated by Google

358 9 Đa xử lý trong hệ thống nhúng

if (tp->tick==0){ // mỗi giây: hiển thị đồng hồ treo tường

màu = XANH+cpuid;

cho (i=0; i<8; i++){

unkpchar(đồng hồ[cpuid][i], 0+cpuid, 70+i);

đồng hồ[id][0]='0'+(tp->hh/10); đồng hồ[id][1]='0'+(tp->hh%10); đồng hồ[id][3]='0'+(tp->mm/10);

đồng hồ[id][4]='0'+(tp->mm%10); đồng hồ[id][6]='0'+(tp->ss/10); đồng hồ[id][7]='0'+(tp->ss%10);

cho (i=0; i<8; i++){

kpchar(đồng hồ[id][i], 0+cpuid, 70+i);

khóa nắng(&plock);

ptimer_clearInterrupt(); // xóa ngắt hẹn giờ

int ptimer_init()

int tôi;

printf("ptimer_init() ");

// đặt địa chỉ cơ sở hẹn giờ

ptp = (PTIMER *)0x1F000600;

// đặt reg bộ đếm điều khiển thành mặc định

ptp->tải = THỜI GIAN;

ptp->điều khiển = 0x06; // Bit IAE = 110, chưa được mã hóa for (i=0; i<4; i++)

{ tt[i].tick = tt[i].hh =

tt[i].mm = tt[i].ss = 0; strcpy(đồng hồ[i], "00:00:00");

int ptimer_start() // khởi động bộ đếm thời gian cục bộ

PTIMER *tpr;

printf("ptimer_start\n");

tpr = ptp; tpr-

>điều khiển |= 0x01; // đặt bit kích hoạt 0

int ptimer_stop() // dừng bộ đếm thời gian cục bộ

PTIMER *tptr = ptp;

tptr->điều khiển &= 0xFE; // xóa bit kích hoạt 0

int ptimer_clearInterrupt() // xóa ngắt

PTIMER *tpr = ptp;

ptp->intclr = 0x01;

int APstart() // Mã khởi động AP

int cpuid = get_cpuid();

mutex_lock(&m);

printf("CPU%d start: ", cpuid);

config_int(29, cpuid); // cần cái này cho mỗi CPU

ptimer_init();

ptimer_start();
Machine Translated by Google

9.10 Bộ hẹn giờ toàn cầu và cục bộ 359

Hình 9.6 Trình diễn bộ định thời cục bộ trong SMP

ncpu++;

printf("CPU%d vào trạng thái WFI\n", cpuid, cpuid);

mutex_unlock(&m);

trong khi(1){

asm("WFI");

int irq_chandler()

int int_ID = *(int *)(GIC_BASE + 0x10C);

nếu (int_ID == 29){

ptimer_handler();

*(int *)(GIC_BASE + 0x110) = int_ID;

9.10.1 Trình diễn bộ định thời cục bộ trong SMP

Hình 9.6 cho thấy kết quả đầu ra khi chạy chương trình ví dụ C9.6, hiển thị trình tự khởi động ARM SMP với mutex để
đồng bộ hóa CPU và bộ định thời cục bộ.

9.11 Semaphores trong SMP

Một semaphore (đếm) là một cấu trúc

cấu trúc typedef sem{

khóa int; // khóa quay

giá trị int; // giá trị


Machine Translated by Google

360 9 Đa xử lý trong hệ thống nhúng

struct proc * hàng đợi; // hàng đợi FIFO PROC

}SEMAPHORE;

SEMAPHORE s = GIÁ TRỊ; // s.lock=0; s.value=VALUE; s.queue=0;

Trong cấu trúc đèn hiệu, trường khóa là một khóa xoay, đảm bảo rằng mọi thao tác trên đèn hiệu phải được thực hiện

được thực hiện bên trong CR của spinlock của semaphore, trường giá trị là giá trị ban đầu của semaphore, đại diện cho

số lượng tài nguyên có sẵn được bảo vệ bởi semaphore và hàng đợi là hàng đợi FIFO của các tiến trình bị chặn đang chờ

nguồn lực sẵn có. Các phép toán semaphore thường được sử dụng nhất là P và V, được định nghĩa như sau.

// đang chạy là một con trỏ tới tiến trình thực thi hiện tại
-------------------------------------------------- ------------------

P(SEMAPHORE *s) V(Semaphore *s)

{ | |{

int sr = int_off(); |
int sr = int_off();

slock(&s->lock); |
slock(&s->lock);

nếu (--s->giá trị < 0) |


nếu (++s->giá trị <= 0)

(các) KHỐI; |
TÍN HIỆU;

khác |

khóa nắng(&s->lock); khóa nắng(&s->lock);

int_on(sr); int_on(sr);

} | | |}
-------------------------------------------------- ------------------

int BLOCK(SEMAPHORE *s) int TÍN HIỆU(SEMAPHORE *s)

{ | |{

đang chạy->trạng thái = KHỐI; PROC *p = dequeue(&s->queue);

| enqueue(&s->queue, đang chạy); | | p->trạng thái = SẴN SÀNG;

sunlok(s->lock); enqueue(&readyQueue, p);

tswitch(); // quá trình chuyển đổi |


} |}
-------------------------------------------------- ------------------

Trong hệ thống SMP, chuyển đổi quy trình thường được kích hoạt bởi các ngắt. Trong cả hai chức năng P và V, trước tiên quy trình sẽ vô hiệu hóa

ngắt để ngăn quá trình chuyển đổi trên CPU. Sau đó, nó thu được spinlock của semaphore, điều này ngăn cản các CPU khác

nhập cùng một CR. Do đó, bất kể có bao nhiêu CPU, chỉ một tiến trình có thể thực thi bất kỳ chức năng nào hoạt động

trên cùng một ngữ nghĩa. Trong P(s), tiến trình giảm giá trị semaphore đi 1. Nếu giá trị không âm, tiến trình sẽ giảm

giải phóng spinlock, cho phép ngắt và trả về. Trong trường hợp này, tiến trình hoàn thành thao tác P mà không bị

bị chặn. Nếu giá trị trở thành âm, quá trình sẽ tự chặn nó trong hàng đợi semaphore (FIFO), giải phóng spinlock và

quá trình chuyển mạch. Trong V(s), tiến trình vô hiệu hóa các ngắt, thu được khóa xoay của đèn hiệu và tăng giá trị của đèn hiệu

giá trị bằng 1. Nếu giá trị không dương, nghĩa là có các tiến trình bị chặn trong hàng đợi semaphore, nó sẽ bỏ chặn một

xử lý khỏi hàng đợi semaphore và làm cho quy trình sẵn sàng chạy lại. Khi một tiến trình bị chặn tiếp tục chạy trong P

(s), nó cho phép ngắt và trả về. Trong trường hợp này, quy trình hoàn tất thao tác P sau khi bị chặn. Nó được ghi nhận một lần nữa

rằng các đèn hiệu được định nghĩa trong cuốn sách này là các đèn hiệu đếm, chúng có tính tổng quát hơn các đèn hiệu nhị phân truyền thống. Là một

semaphore đếm, giá trị có thể âm. Tại bất kỳ thời điểm nào, các bất biến sau đây vẫn giữ nguyên.

if (s.value >= 0): value = số lượng tài nguyên có sẵn

khác : |giá trị| = số tiến trình bị chặn trong hàng đợi semaphore

9.11.1 Ứng dụng của Semaphores trong SMP

Không giống như mutexes chỉ có thể được sử dụng làm khóa, semaphores linh hoạt hơn vì chúng có thể được sử dụng làm khóa cũng như cho

quá trình hợp tác. Để biết danh sách các ứng dụng semaphore, người đọc có thể tham khảo Chap. 6 của (Wang 2015). Trong cuốn sách này, chúng tôi

sẽ sử dụng các ẩn dụ làm công cụ đồng bộ hóa trong quản lý tài nguyên, trình điều khiển thiết bị và hệ thống tệp.
Machine Translated by Google

9.12 Khóa có điều kiện 361

9.12 Khóa có điều kiện

Spinlocks, mutexes và semaphores đều sử dụng giao thức khóa, chặn một quá trình cho đến khi nó thành công. Bất kỳ khóa nào

giao thức có thể dẫn đến bế tắc (Silberschatz và cộng sự 2009; Stallings 2011; Wang 2015). Bế tắc là tình trạng trong đó một tập hợp

trong số các tiến trình chờ đợi lẫn nhau để không có tiến trình nào có thể tiếp tục. Một cách đơn giản để ngăn ngừa bế tắc là đảm bảo

rằng thứ tự lấy các ổ khóa khác nhau luôn là một chiều, do đó việc khóa chéo hoặc khóa tròn không bao giờ có thể xảy ra.

Tuy nhiên, điều này có thể không thực hiện được trong tất cả các chương trình đồng thời. Một cách tiếp cận thực tế để ngăn chặn bế tắc là sử dụng

khóa và lùi có điều kiện. Trong sơ đồ này, nếu một tiến trình đã nắm giữ một số khóa, cố gắng lấy một khóa khác

lock, nó sẽ cố gắng lấy khóa tiếp theo một cách có điều kiện. Nếu nỗ lực khóa thành công, nó sẽ diễn ra như bình thường. Nếu khóa

lần thử không thành công, nó sẽ thực hiện các hành động khắc phục, thường liên quan đến việc giải phóng một số khóa mà nó đã giữ và thử lại

thuật toán. Để thực hiện sơ đồ này, chúng tôi giới thiệu các hoạt động khóa có điều kiện sau đây.

9.12.1 Spinlock có điều kiện

Khi một tiến trình cố gắng giành được một spinlock thông thường, nó sẽ không quay trở lại cho đến khi có được spinlock đó. Điều này có thể dẫn tới

bế tắc do các nỗ lực khóa chéo của các quy trình khác nhau. Giả sử rằng một quá trình Pi đã có được một spinlock spin1,

và nó cố gắng thu được một spinlock spin2 khác. Một quá trình khác Pj đã thu được spin1 nhưng cố gắng thu được spinlock spin2.

Sau đó Pi và Pj sẽ đợi nhau mãi mãi nên cuối cùng họ rơi vào bế tắc do những nỗ lực khóa chéo. ĐẾN

ngăn chặn những bế tắc như vậy, một trong các quy trình có thể sử dụng khóa spin có điều kiện, được xác định như sau.

// int cslock(int *spin): có điều kiện để có được một spinlock

// trả về 0 nếu khóa không thành công; trả về 1 nếu khóa thành công

cslock:

ldrex r1, [r0] // đọc giá trị spinlock

cmp r1, #UNLOCKED // so sánh với UNLOCKED(0)

khóa thử beq

chuyển r0, #0 // trả về 0 nếu thất bại

bx lr

thử khóa: // cố gắng khóa

mov r1, #1 // đặt r1=1

strex r2, r1, [r0] // thử lưu 1 vào [r0]; r2=giá trị trả về

cmp r2, #0x0 // kiểm tra giá trị trả về strex trong r2

chúc mừng cslock // strex thất bại; thử khóa lại lần nữa

DMB // rào cản bộ nhớ TRƯỚC KHI truy cập CR

Mov r0, #1

bx lr // trả về 1 cho THÀNH CÔNG

cslock() có điều kiện trả về 0 (đối với FAIL) nếu khóa xoay đã bị khóa. Nó trả về 1 (đối với THÀNH CÔNG) nếu có

đã có được spinlock. Trong trường hợp trước, quy trình có thể giải phóng một số khóa mà nó đã giữ và thử lại thuật toán,

do đó ngăn ngừa mọi bế tắc có thể xảy ra. Tương tự, chúng ta cũng có thể triển khai các loại cơ chế khóa có điều kiện khác.

9.12.2 Mutex có điều kiện

int mutex_clock(MUTEX *m)

slock(&m->lock); // thu được spinlock

if (m->status == ĐÃ KHÓA || m->chủ sở hữu != đang chạy->pid){

khóa nắng(&m->lock); // giải phóng spinlock

trả về 0; // trả về 0 nếu thất bại

}
Machine Translated by Google

362 9 Đa xử lý trong hệ thống nhúng

// hiện đang giữ spinlock, cập nhật mutex trong CR

m->trạng thái = KHÓA;

m->chủ sở hữu = get_cpuid();


khóa nắng(&m->lock); // giải phóng spinlock

trả về 1; // trả về 1 cho THÀNH CÔNG

9.12.3 Hoạt động Semaphore có điều kiện

-------------------------------------------------- -----------

int CP(SEMAPHORE *s) | int CV(Semaphore *s)

{ |{

int sr = int_off(); int sr = int_off();

slock(&s->lock); slock(&s->lock);

nếu (s->giá trị <= 0){ nếu (s->giá trị >= 0){

khóa nắng(&s->lock) khóa nắng(&s->lock);

trả về 0; trả về 0;

} | | | | | |}

nếu (--s->giá trị < 0) nếu (++s->giá trị <= 0)

(các) KHỐI; TÍN HIỆU;

int_on(sr); int_on(sr);

trả về 1; trả về 1;

} | | | | |}
-------------------------------------------------- -----------

CP có điều kiện hoạt động chính xác như P và trả về 1 cho THÀNH CÔNG nếu giá trị semaphore > 0. Ngược lại, nó

trả về 0 cho FAIL mà không thay đổi giá trị semaphore. CV có điều kiện hoạt động chính xác như V và trả về 1 nếu

giá trị semaphore là < 0. Ngược lại, nó trả về 0 mà không thay đổi giá trị semaphore hoặc bỏ chặn bất kỳ quá trình nào từ

hàng đợi ngữ nghĩa. Chúng tôi sẽ trình bày cách sử dụng các thao tác khóa có điều kiện này sau trong nhân hệ điều hành SMP.

9.13 Quản lý bộ nhớ trong SMP

Đơn vị quản lý bộ nhớ (MMU) trong Cortex-A9 (Hướng dẫn tham khảo kỹ thuật ARM Cortex-A9 MPcore r4p1 2012)

và Cortex-A11 (Bộ xử lý lõi ARM11, r2p0 2008) có các khả năng bổ sung sau để hỗ trợ SMP.

(1). Bộ mô tả cấp 1: Bộ mô tả bảng trang cấp 1 có các bit bổ sung: NS(19), NG(17), S(16), APX(15), TEX

(14-12) và P(9). Các bit khác trong AP(11-10), DOMain(8-5), CB(3-2), ID(1-0) không thay đổi. Trong số các bit được thêm vào, S

bit xác định xem vùng bộ nhớ có được chia sẻ hay không. Các bit TEX (Type Extension) được sử dụng để phân loại vùng bộ nhớ

dưới dạng chia sẻ, không chia sẻ hoặc loại thiết bị. Các vùng bộ nhớ dùng chung được sắp xếp mạnh mẽ các vùng bộ nhớ toàn cầu có thể truy cập được bởi tất cả

CPU. Vùng bộ nhớ không chia sẻ là riêng tư đối với các CPU cụ thể. Vùng bộ nhớ thiết bị là các thiết bị ngoại vi được ánh xạ bộ nhớ

có thể truy cập được bởi tất cả các CPU. Ví dụ: khi sử dụng phân trang, các thuộc tính vùng bộ nhớ trong các mục trong bảng trang Cấp 1
nên được đặt thành

Đã chia sẻ 0x14C06

0x00C1E không chia sẻ

Thiết bị 0x00C06

thay vì các giá trị 0x412 hoặc 0x41E. Nếu không, MMU sẽ cấm truy cập bộ nhớ trừ khi AP truy cập miền

các bit được đặt thành 0x3 cho chế độ người quản lý, chế độ này hoàn toàn không thực thi việc kiểm tra quyền. Khi sử dụng phân trang 2 cấp,

kích thước trang có thể là 4KB, 64KB, 1MB hoặc siêu trang 16MB. Các bit khả năng truy cập của tên miền và các trang nhỏ 4KB vẫn còn

không thay đổi.


Machine Translated by Google

9.13 Quản lý bộ nhớ trong SMP 363

(2). Các tính năng TLB khác: Trong Cortex-A9 đến Cortex-A11, TLB bao gồm các TLB vi mô cho cả lệnh và dữ liệu, đồng thời cũng là

một TLB chính thống nhất. Về cơ bản, chúng chia TLB thành bộ đệm hai cấp, cho phép giải quyết các mục TLB nhanh hơn.
Ngoài việc khóa mục nhập TLB, các mục nhập TLB chính có thể được liên kết với các quy trình hoặc ứng dụng cụ thể bằng cách sử dụng
Mã định danh không gian địa chỉ (ASID), cho phép các mục nhập TLB đó vẫn tồn tại trong quá trình chuyển đổi ngữ cảnh, tránh nhu cầu
tải lại chúng.
(3). Dự đoán luồng chương trình: CPU ARM thường dự đoán các lệnh nhánh. Trong Cortex-A9, tính năng dự đoán luồng chương trình
có thể bị tắt và bật một cách rõ ràng. Các tính năng này, cùng với Rào cản đồng bộ hóa dữ liệu (DSB), có thể được sử dụng để đảm
bảo duy trì nhất quán các mục nhập TLB.

9.13.1 Mô hình quản lý bộ nhớ trong SMP

Khi định cấu hình ARM MMU cho SMP, không gian Địa chỉ ảo (VA) của CPU có thể được định cấu hình là không gian VA đồng nhất hoặc
không đồng nhất. Trong mô hình không gian VA thống nhất, ánh xạ VA tới PA giống hệt nhau đối với tất cả các CPU. Trong mô hình
không gian VA không đồng nhất, mỗi CPU có thể ánh xạ cùng một phạm vi VA tới các vùng PA khác nhau để sử dụng riêng. Chúng tôi
chứng minh các mô hình ánh xạ bộ nhớ này bằng các ví dụ sau.

9.13.2 Không gian VA thống nhất

Trong ví dụ này, ký hiệu là C9.7, chúng tôi giả sử có 4 CPU, CPU0 đến CPU3. Khi hệ thống khởi động, CPU0 bắt đầu thực thi
reset_handler, được QEMU tải về 0x10000. Nó khởi tạo ngăn xếp chế độ SVC và IRQ, sao chép bảng vectơ sang địa chỉ 0 và tạo bảng
trang Cấp 1 sử dụng các phần 1 MB để ánh xạ nhận dạng 512 MB VA sang PA. Tất cả các vùng bộ nhớ được đánh dấu là CHIA SẺ (0x14C06)
ngoại trừ không gian I/O 1 MB ở mức 256 MB, được đánh dấu là THIẾT BỊ (0x00C06). Tất cả các vùng bộ nhớ đều nằm trong miền 0, có
các bit AP được đặt ở Chế độ máy khách (b01) để thực thi việc kiểm tra quyền. Sau đó, nó kích hoạt MMU để dịch VA sang PA và gọi
main() trong C. Sau khi khởi tạo hệ thống, CPU0 phát hành SGI để đánh thức các CPU phụ.

Tất cả các CPU hoặc AP phụ bắt đầu thực thi cùng một mã reset_handler ở 0x10000. Mỗi AP thiết lập các ngăn xếp chế độ SVC và
IRQ của riêng mình, nhưng không sao chép bảng vectơ và tạo lại bảng trang. Mỗi AP sử dụng cùng một bảng trang được tạo bởi CPU0
ở 0x4000 để bật MMU. Do đó, tất cả các CPU chia sẻ cùng một không gian VA vì bảng trang của chúng giống hệt nhau.
Sau đó, mỗi AP gọi APstart() trong C.

/******************** ts.s tệp của C9.7 ********************** */

.chữ

.code 32

.global reset_handler, vectơ_start, vectơ_end

.global apStart, slock, sunlock

// TẤT CẢ CPU thực thi reset_handler ở 0x10000

reset_handler:

// lấy ID CPU và giữ nó trong R11

MRC p15, 0, r11, c0, c0, 5 // Đọc thanh ghi ID CPU

VÀ r11, r11, #0x03 // Tắt mặt nạ, để lại trường ID CPU

// đặt ngăn xếp CPU SVC

LDR r0, =svc_stack // r0->16KB svc_stack trong t.ld // r1 =

chuyển động r1, r11 cpuid // cpuid++ //

thêm r1, r1, #1 (cpuid+1) * 4096

lsl r2, r1, #12

thêm r0, r0, r2

mov sp, r0 // // SVC sp=svc_stack[cpuid] cao cấp

chuyển sang chế độ IRQ

MSR cpsr, #0x12

// đặt ngăn xếp IRQ của CPU


Machine Translated by Google

364 9 Đa xử lý trong hệ thống nhúng

LDR r0, =irq_stack // r0->16KB irq_stack trong t.ld

chuyển động r1, r11

thêm r1, r1, #1

lsl r2, r1, #12 // (cpuid+1) * 4096

thêm r0, r0, r2

mov sp, r0 // IRQ sp=irq_stack[cpuid] cao cấp

// quay lại chế độ SVC

MSR cpsr, #0x13

cmp r11, #0

bỏ qua nhé // AP bỏ qua

Bản sao BL_vector // chỉ CPU0 sao chép vectơ vào địa chỉ 0

BL mkPtable // chỉ CPU0 tạo pgdir và pgtable trong C

nhảy:

ldr r0, Mtable // tất cả CPU đều kích hoạt MMU

mcr p15, 0, r0, c2, c0, 0 // đặt TTBase

mcr p15, 0, r0, c8, c7, 0 // tuôn ra TLB

// đặt miền0 AP thành 01=client (kiểm tra quyền)

chuyển r0, #0x1 // AP=b01 cho KHÁCH HÀNG

mcr p15, 0, r0, c3, c0, 0

// kích hoạt MMU

mrc p15, 0, r0, c1, c0, 0

hoặc r0, r0, #0x00000001 // đặt bit0

mcr p15, 0, r0, c1, c0, 0 // ghi vào c1

không

không

không

mrc p15, 0, r2, c2, c0

chuyển động r2, r2

cmp r11, #0

AP tốt

BL chính // CPU0 gọi main() trong C

B .

AP: // mỗi AP gọi APstart() trong C

adr r0, APaddr

máy tính ldr, [r0]

APaddr: .word APstart

Mtable: .word 0x4000

/********************* tệp tc của C9.7 *******************/

int *apAddr = (int *)0x10000030; // đăng ký SYS_FLAGSSET

int aplock = 0; biến // spinlock cho AP

động int ncpu = 1; // số lượng CPU sẵn sàng

#include "uart.c" // trình điều khiển UART

#include "kbd.c" // trình điều khiển KBD

#include "ptimer.c" // trình điều khiển hẹn giờ cục bộ

#include "vid.c" // Trình điều khiển LCD

int copy_vectors() { // GIỐNG NHƯ TRƯỚC }

int config_gic(int cpuid){ // CÙNG như trước }

int irq_chandler() ( // Giống như trước )

int send_sgi() { // Giống như trước }

// Bộ mô tả mẫu của các vùng bộ nhớ khác nhau

#define CHIA SẺ 0x14C06

#define KHÔNG CHIA SẺ 0x00C1E

#xác định THIẾT BỊ 0x00C06

int mkPtable() // tạo pgdir bằng CPU0


Machine Translated by Google

9.13 Quản lý bộ nhớ trong SMP 365

int tôi;

u32 *ut = (u32 *)0x4000; // Mtable ở mức 16KB

for (i=0; i<4096; i++) // xóa các mục pgdir về 0

ut[i] = 0;

mục nhập u32 = CHIA SẺ; // bắt đầu với mặc định=SHARED

for (i=0; i<512; i++){ // 512 mục: Ánh xạ ID 512MB VA tới PA ut[i] = entry;

mục += 0x100000;

// đánh dấu không gian I/O 1 MB ở mức 256 MB là THIẾT BỊ

ut[256] = (256*0x100000) | THIẾT BỊ;

int APstart() // Mã khởi động AP

slock(&aplock);

int cpuid = get_cpuid();

printf("CPU%d trong APstart()\n", cpuid);

config_int(29, cpuid); // cấu hình bộ đếm thời gian cục bộ cho mỗi CPU

ptimer_init(); ptimer_start();

ncpu++;

khóa nắng(&aplock);

printf("CPU%d vào vòng lặp WFI ncpu=%d\n", cpuid, ncpu);

trong khi(1){

asm("WFI");

int chính()

Enable_scu();

fbuf_init();

printf("********* Ví dụ khởi động ARM SMP 5 *************\n");

kbd_init();

uart_init();

ptimer_init(); ptimer_start();

config_gic();

*apAddr = (int)0x10000; // AP bắt đầu thực thi ở 0x10000

send_sgi(0x01, 0x0F, 0x00); // AP đánh thức

printf("CPU0: đợi AP sẵn sàng\n");

while(ncpu < 4);

printf("CPU0: continue ncpu=%d\n", ncpu);

printf("nhập key từ KBD hoặc UARTs : ");

while(1) asm("WFI");

9.13.2.1 Trình diễn ánh xạ không gian VA thống nhất Hình 9.7 cho
thấy đầu ra của hệ thống SMP với ánh xạ không gian VA thống nhất.

9.13.3 Không gian VA không đồng nhất

Trong mô hình không gian VA không đồng nhất, CPU có thể ánh xạ cùng một phạm vi VA tới các phạm vi PA khác nhau để tạo các
vùng bộ nhớ riêng chứa thông tin cụ thể của CPU. Trong trường hợp này, mỗi CPU có bảng trang riêng, bảng này có thể được tạo
bởi CPU0 hoặc bởi chính mỗi CPU trong quá trình khởi động hệ thống. Chúng tôi minh họa kỹ thuật này bằng ví dụ sau.
Machine Translated by Google

366 9 Đa xử lý trong hệ thống nhúng

Hình 9.7 Trình diễn ánh xạ không gian VA thống nhất

Trong ví dụ C9.8 này, chúng tôi giả sử rằng hệ thống thực thi ở mức 1 MB bộ nhớ vật lý thấp nhất. Khi hệ thống khởi
động, chúng ta sẽ để CPU0 tạo 4 bảng trang cấp 1, một bảng cho mỗi CPU. Trong các bảng trang, chúng tôi ánh xạ cùng
VA=1MB tới các PA khác nhau dựa trên ID CPU. Do hệ thống được QEMU tải ở 0x10000 (64KB) nên không có đủ dung lượng để
tạo bốn bảng trang (16KB) ở 0x4000 (16KB). Chúng tôi sẽ sử dụng không gian 64KB dưới 1 MB (tại 0xF0000) làm bảng trang
Cấp 1 (pgdirs) của CPU. Khi hệ thống khởi động, CPU0 tạo bốn pgdir, một pgdir cho mỗi CPU, ở 0xF0000 + cpuid*16KB để ánh
xạ ID 512 MB VA tới PA. Tuy nhiên, đối với mỗi CPU, không gian VA ở mức 1 MB được ánh xạ tới vùng 1 MB khác trong bộ
nhớ vật lý. Cụ thể, VA=1MB của cpuid được ánh xạ tới PA=(cpuid+1)*1MB. Trước khi kích hoạt MMU, CPU0 có thể truy cập
tất cả các PA. Nó ghi các chuỗi khác nhau vào PA ở mức 1M đến 4 MB, mỗi chuỗi cho một CPU khác nhau. Sau đó CPU0 khởi
tạo hệ thống và phát hành SGI để đánh thức các AP.
Khi các AP bắt đầu thực thi reset_handler, chúng chỉ khởi tạo ngăn xếp chế độ SVC và IRQ. Mỗi AP nhận được cpuid và
sử dụng pgdir ở 0xF0000+cpuid*16K để kích hoạt MMU. Trong APstart(), mỗi AP in chuỗi có cùng VA=1MB.
Mã lắp ráp của hệ thống vẫn giống như trước, ngoại trừ những sửa đổi nhỏ đã đề cập ở trên. Vì vậy, chúng tôi chỉ hiển
thị mã C đã sửa đổi.

/********************* tệp tc của C9.8 *******************/

int *apAddr = (int *)0x10000030; int aplock =

0; biến động int ncpu

= 1;

#include "uart.c"

#include "kbd.c"

#include "ptimer.c"

#include "vid.c"

int copy_vectors() { // CÙNG như trước } int config_gic()

{ // CÙNG như trước }

int irq_chandler() { // CÙNG như trước }

int seng_sgi() { // Giống như trước }

#define CHIA SẺ 0x00014C06

#define KHÔNG CHIA SẺ 0x00000C1E

#xác định THIẾT BỊ 0x00000C06

int mkPtable() // CPU0 tạo 4 pgdir

int i, j, *ut;

cho (i=0; i<4; i++){ // tạo 4 pgdir

ut = (u32 *)(0xF0000 + i*0x4000); // cách nhau 16KB


Machine Translated by Google

9.13 Quản lý bộ nhớ trong SMP 367

mục nhập u32 = CHIA SẺ;

vì (j=0; j<4096; j++) ut[j] = 0; // xóa các mục pgdir

for (j=0;

j<512; j++){ ut[j] = mục nhập; // Ánh xạ ID 512MB VA tới PA

mục += 0x100000;

ut[256] = 256*0x100000 | THIẾT BỊ; // đánh dấu không gian I/O là THIẾT BỊ

nếu tôi){ // cho CPU1 tới CPU3:

ut[1] = (i+1)*0x100000 | KHÔNG CHIA SẺ;

ut[2] = ut[3] = ut[4] = 0; // mục không còn hợp lệ

// TRƯỚC KHI kích hoạt MMU: ghi các chuỗi khác nhau vào PA=1MB đến 4MB

char *cp = (char *)0x100000; // PA=1MB

strcpy(cp, "chuỗi ban đầu của CPU0");

cp += 0x100000; // PA=2MB

strcpy(cp, "Chuỗi BAN ĐẦU của CPU1");

cp += 0x100000; // PA=3MB

strcpy(cp, "CHUINGI ban đầu của CPU2");

cp += 0x100000; // PA=4MB

strcpy(cp, "CHUỖI BAN ĐẦU của CPU3");

int APstart()

char *cp;

slock(&aplock);

int cpuid = get_cpuid();

cp = (char *)0x100000; // CÙNG VA=1MB

printf("CPU%d in APstart(): chuỗi tại VA 1MB = %s\n", cpuid, cp); config_int(29, cpuid);

ptimer_init(); ptimer_start(); // cấu hình bộ đếm thời gian cục bộ cho mỗi CPU

ncpu++;

khóa nắng(&aplock);

printf("CPU%d vào vòng lặp WFI ncpu=%d\n", cpuid, ncpu);

trong khi(1){

asm("WFI");

int chính()

Enable_scu();

fbuf_init();

printf("********* Ví dụ khởi động ARM SMP 6 *************\n");

kbd_init();

uart_init();

ptimer_init(); ptimer_start(); config_gic();

apAddr = (int

*)0x10000030; *apAddr = (int)0x10000;

send_sgi(0x01, 0x0F, 0x01);

printf("CPU0: ncpu=%d\n", ncpu);

while(ncpu < 4); printf("CPU0: ncpu=%d\n",

ncpu); char *cp = (char

*)0x100000;

// tại VA=1MB
Machine Translated by Google

368 9 Đa xử lý trong hệ thống nhúng

Hình 9.8 Trình diễn ánh xạ không gian VA không đồng nhất

printf("CPU0: ncpu=%d string at VA 1MB = %s\n", ncpu, cp);


printf("nhập key từ KBD hoặc UARTs : "); trong khi(1)

asm("WFI");
}

9.13.3.1 Trình diễn ánh xạ không gian VA không đồng nhất Hình 9.8 cho thấy đầu

ra của hệ thống SMP với ánh xạ không gian VA không đồng nhất. Như hình minh họa, mỗi AP hiển thị một chuỗi khác nhau trong vùng bộ nhớ

riêng của nó với giá trị VA=1MB.

9.13.4 Tính toán song song trong không gian VA không đồng nhất

Chúng tôi trình diễn một hệ thống tính toán song song sử dụng mô hình bộ nhớ không gian VA không đồng nhất. Trong hệ thống ví dụ này,

chúng tôi cho phép CPU thực hiện các phép tính song song như sau. Trước khi thiết lập bảng trang và kích hoạt MMU, CPU0 sẽ gửi một chuỗi

N số nguyên vào vùng bộ nhớ riêng của mỗi CPU. Sau khi kích hoạt MMU cho dịch VA sang PA, mỗi CPU có thể truy cập vùng bộ nhớ riêng của

mình bằng cùng một VA. Sau đó, mỗi CPU tính tổng N số nguyên trong vùng bộ nhớ riêng của nó. Khi CPU đã tính tổng một phần (cục bộ), nó

sẽ cộng tổng một phần vào biến tổng (toàn cục), được khởi tạo bằng 0. Khi tất cả các CPU đã cập nhật tổng tổng, CPU0 sẽ in kết quả cuối

cùng. Chương trình ví dụ nhằm mục đích hiển thị các nguyên tắc quan trọng sau đây của xử lý song song trong SMP.

(1). Mỗi CPU có thể thực thi một tiến trình trong vùng bộ nhớ riêng của nó.

(2). Khi CPU có ý định sửa đổi bất kỳ biến toàn cục được chia sẻ nào, nó phải thực hiện cập nhật trong Vùng quan trọng (CR) được bảo vệ

bằng khóa spinlock hoặc khóa mutex.

Sau đây liệt kê các đoạn mã chính của một hệ thống tính toán song song như vậy.

(1). Tệp ts.s: tệp ts.s giống hệt tệp của chương trình ví dụ 9.8. (2) tập tin tc:
Machine Translated by Google

9.13 Quản lý bộ nhớ trong SMP 369

/*************** file tc của Chương trình 9.9 ****************/

int aplock = 0; // spinlock cho AP

MUTEX m; // mutex để khóa

tổng số int dễ bay hơi = 0;

biến động int ncpu = 1;

int *apAddr = (int *)0x10000030;

#include "uart.c" // Trình điều khiển UART

#include "kbd.c" // trình điều khiển KBD

#include "ptimer.c" // trình điều khiển hẹn giờ cục bộ

#include "vid.c" // Trình điều khiển LCD

#include "mutex.c" // các hàm mutex

int copy_vectors(){ // giống như trước }

int config_gic() { // giống như trước }

int irq_chandler(){ // giống như trước }

int send_sgi() { // giống như trước }

#define CHIA SẺ 0x14C06

#define KHÔNG CHIA SẺ 0x00C1E

#xác định THIẾT BỊ 0x00C06

int mkPtable() // CPU0 tạo 4 pgdir

int i, j, *ut;

cho (i=0; i<4; i++){ // tạo 4 pgdir

ut = (u32 *)(0xF0000 + i*0x4000); // cách nhau 16KB

mục nhập u32 = CHIA SẺ;

vì (j=0; j<4096; j++) ut[j] = 0; // xóa các mục pgdir

cho (j=0; j<512; j++){ // Ánh xạ ID 512MB VA tới PA

ut[j] = mục nhập;

mục += 0x100000;

ut[256] = 256*0x100000 | THIẾT BỊ; // đánh dấu không gian I/O là THIẾT BỊ

nếu tôi){ // cho CPU1 tới CPU3:

ut[1] = (i+1)*0x100000 | KHÔNG CHIA SẺ;

ut[2] = ut[3] = ut[4] = 0; // mục không còn hợp lệ

int fill_values() // CPU0: điền số vào 1MB đến 4MB

int i, j, k = 1;

int *ip = (int *)0x100000; // 1MB PA

cho (i=1; i<5; i++){ // 4 vùng dữ liệu

cho (j=0; j<8; j++){ // 8 số cho mỗi khu vực

ip[j] = k++; // giá trị

ip += 0x100000/4; // ip trỏ tới 1MB tiếp theo

}
Machine Translated by Google

370 9 Đa xử lý trong hệ thống nhúng

// TẤT CẢ CPU gọi điện toán() để tính tổng cục bộ và cập nhật tổng

tính toán int(int cpuid)

int i, *ip, sum = 0;

printf("CPU%d tính tổng một phần\n", cpuid); ip = (int *)0x100000;

// VA ở mức 1MB

cho (i=0; i<8; i++){

printf("%d", ip[i]); // hiển thị dữ liệu cục bộ dưới dạng VA=1MB sum +=

ip[i]; // tính tổng cục bộ

printf("CPU%d: sum=%d\n", cpuid, sum);

mutex_lock(&m); // khóa mutex m

tổng += tổng; // cập nhật tổng toàn cầu

mutex_unlock(&m); // mở khóa mutex m

int APstart()

char *cp; tổng số nguyên;

slock(&aplock); int

cpuid = get_cpuid(); config_int(29,

cpuid); // cấu hình bộ đếm thời gian cục bộ cho mỗi CPU

ptimer_init(); ptimer_start();

tính toán (cpuid); // gọi tính toán()

printf("CPU%d vào vòng lặp WFI ncpu=%d\n", cpuid, ncpu);

ncpu++;

khóa nắng(&aplock);

while(1) asm("WFI");

int chính()

Enable_scu();

fbuf_init();

printf("********* Ví dụ khởi động ARM SMP 6 *************\n");

kbd_init();

uart_init();

ptimer_init();

ptimer_start();

config_gic();

mutex_init(&m); // khởi tạo mutex m

apAddr = (int *)0x10000030;

*apAddr = (int)0x10000;

send_sgi(0x01, 0x0F, 0x00); // AP đánh thức

printf("CPU0: ncpu=%d\n", ncpu); while(ncpu <

4); // đợi cho đến khi tất cả các AP tính toán xong printf("CPU0: ncpu=%d\n", ncpu);

tính toán(0);

printf("tổng = %d\n", tổng); printf("nhập key

từ KBD hoặc UARTs : ");

trong khi(1);

Hình 9.9 thể hiện kết quả đầu ra khi chạy hệ thống tính toán song song C9.9.
Machine Translated by Google

9.14 Đa nhiệm trong SMP 371

Hình 9.9 Đầu ra của hệ thống tính toán song song

9.14 Đa nhiệm trong SMP

Trong ví dụ tính toán song song đơn giản, mỗi CPU chỉ thực hiện một tác vụ duy nhất và không tận dụng hết khả năng của nó.

của các CPU. Một cách tốt hơn để sử dụng CPU hiệu quả hơn là để mỗi CPU thực thi nhiều tiến trình bằng cách đa nhiệm.

Mỗi tiến trình có thể thực thi ở hai chế độ khác nhau, chế độ kernel và chế độ người dùng. Khi ở chế độ kernel, tất cả các tiến trình đều thực thi

trong cùng một không gian địa chỉ của kernel. Khi ở chế độ người dùng, mỗi tiến trình có một vùng bộ nhớ riêng được MMU bảo vệ.

Trong một hệ thống CPU, mỗi lần chỉ có một tiến trình có thể thực thi ở chế độ kernel. Kết quả là không cần phải bảo vệ kernel

cấu trúc dữ liệu từ việc thực thi đồng thời của các tiến trình. Trong SMP, nhiều tiến trình có thể thực thi song song trên các CPU khác nhau.

Tất cả các cấu trúc dữ liệu trong hạt nhân SMP phải được bảo vệ để ngăn chặn tình trạng hỏng hóc do các điều kiện tương tranh. Trong hệ thống đơn bộ xử lý,

kernel thường sử dụng một con trỏ đang chạy để trỏ đến tiến trình hiện đang thực thi. Trong SMP, một con trỏ đang chạy

không còn phù hợp vì nhiều tiến trình có thể thực thi trên các CPU khác nhau cùng một lúc. Chúng ta phải nghĩ ra cách để

xác định các PROC hiện đang thực thi trên CPU. Có hai cách tiếp cận có thể cho vấn đề này. đầu tiên

một là sử dụng bộ nhớ ảo. Xác định cấu trúc CPU, như trong

cấu trúc CPU{

cấu trúc cpu *cpu; // con trỏ tới cấu trúc CPU này

PROC *proc; // con trỏ tới PROC trên CPU này

int cpuid; // ID CPU

int *pgdir; // pgdir của CPU này

PROC *readyQueue; // hàng đợi sẵn sàng của CPU này

// các lĩnh vực khác

}CPU;

Mỗi CPU có cấu trúc CPU trong một vùng bộ nhớ riêng, được ánh xạ tới cùng một VA, ví dụ: ở mức 1 MB cho tất cả các CPU.

Xác định các ký hiệu


Machine Translated by Google

372 9 Đa xử lý trong hệ thống nhúng

#define cpu (struct cpu *)0x100000

#define đang chạy cpu->proc

Sau đó, chúng ta có thể sử dụng cùng một biểu tượng đang chạy để truy cập PROC hiện đang thực thi trên mỗi CPU. Hạn chế của sơ đồ này là nó sẽ

làm cho các tiến trình điều phối chạy trên các CPU khác nhau trở nên khá phức tạp. Điều này là do bảng trang cấp 1 (pgdirs) và bảng trang cấp 2 (nếu

sử dụng phân trang 2 cấp) của CPU đều khác nhau. Để gửi một tiến trình tới CPU, chúng ta phải thay đổi pgdir tiến trình thành tiến trình của CPU

đích, việc này có thể yêu cầu xóa hoặc vô hiệu hóa nhiều mục trong TLB của CPU. Vì vậy, sơ đồ này chỉ phù hợp với mỗi CPU chạy một bộ quy trình cố

định không di chuyển sang các CPU khác.

Lược đồ thứ hai là sử dụng nhiều con trỏ PROC, mỗi con trỏ trỏ tới tiến trình hiện đang thực thi trên CPU. ĐẾN

làm điều này, chúng tôi xác định các ký hiệu sau.

#define PROC *chạy[NCPU]

#define đang chạy run[get_cpuid()]

Sau đó, chúng ta có thể sử dụng cùng một ký hiệu đang chạy trong tất cả mã C để chỉ PROC hiện đang thực thi trên CPU. Trong trường hợp này, tất

cả các CPU có thể sử dụng mô hình bộ nhớ không gian VA thống nhất. Các không gian VA của chế độ kernel của tất cả các tiến trình đều giống hệt nhau,

do đó chúng có thể chia sẻ cùng một bảng pgdir và trang trong chế độ kernel. Chỉ các mục trong bảng trang Chế độ người dùng của họ có thể khác

nhau. Trong sơ đồ này, việc gửi một tiến trình tới CPU dễ dàng hơn nhiều. Mặc dù chúng ta vẫn phải thay đổi tiến trình pgdir cho phù hợp với CPU

mục tiêu nhưng nó chỉ liên quan đến một vài thay đổi của các mục User mode trong TLB của CPU. Do đó, mô hình bộ nhớ VA thống nhất phù hợp hơn với

các quy trình đang chạy trên các CPU khác nhau bằng cách lập lịch quy trình động.

Hạt nhân 9.15 SMP để quản lý quy trình

Trong chương. 7, chúng tôi đã phát triển một hạt nhân đơn giản cho các hệ thống bộ xử lý đơn (UP). Kernel đơn giản hỗ trợ các tiến trình động ở

cả chế độ kernel và user. Là một hệ thống UP, nó chỉ cho phép một tiến trình thực thi trong kernel tại một thời điểm. Trong phần này, chúng tôi sẽ

trình bày cách mở rộng kernel UP cho SMP. Sau đây là danh sách các nhà tù theo kế hoạch và các sửa đổi của hệ thống SMP kết quả.

(1). Hệ thống sẽ khởi động từ hệ thống tệp EXT2/3 trong phân vùng SDC. Khi chạy trên máy ảo ARM realview-pbx-a9 trong QEMU, hình ảnh hạt nhân được

QEMU tải về 0x10000 và chạy từ đó.

(2). Khi chạy trên VM ARM realview-pbx-a9, hệ thống hỗ trợ 4 CPU.

(3). Hệ thống sử dụng mô hình bộ nhớ không gian VA thống nhất với phân trang động 2 cấp độ. Tất cả các tiến trình đều chia sẻ cùng một không gian

VA trong kernel, được ID ánh xạ tới PA thấp 512 MB. Mỗi CPUi bắt đầu bằng cách chạy một quy trình ban đầu, iproc[i], chỉ chạy ở chế độ kernel với

mức ưu tiên thấp nhất là 0. Quy trình ban đầu của mỗi CPU cũng là quy trình nhàn rỗi của CPU, chạy bất cứ khi nào không có quy trình nào khác có

thể chạy được .

(4). Hệ thống duy trì một hàng đợi lập lịch quy trình duy nhất, được sắp xếp theo mức độ ưu tiên của quy trình. Tất cả các CPU đều cố gắng chạy

các tiến trình từ cùng một hàng đợi sẵn sàng. Nếu hàng đợi trống, mỗi CPU sẽ chạy một quy trình không hoạt động, quy trình này sẽ đặt CPU ở trạng

thái tiết kiệm năng lượng WFI, chờ ngắt.

(5). Tất cả các quy trình khác thường chạy ở chế độ Người dùng trong phạm vi VA là [2GB đến 2GB + kích thước hình ảnh]. Hình ảnh chế độ Người

dùng có thể thực thi nằm trong thư mục /bin của hệ thống tệp trên SDC. Để đơn giản, chúng tôi giả sử kích thước hình ảnh trong thời gian chạy cố

định từ 1 đến 4 MB. Hình ảnh chế độ Người dùng của một quy trình bao gồm các khung trang được phân bổ động. Khi một tiến trình kết thúc, nó sẽ

giải phóng các khung trang trở lại danh sách trang trống để sử dụng lại.

(6). Các quy trình ở chế độ người dùng sử dụng các lệnh gọi hệ thống (thông qua SWI) để vào kernel nhằm thực thi các chức năng của kernel. Mỗi lệnh

gọi hệ thống sẽ trả về một giá trị cho chế độ Người dùng, ngoại trừ exit() không bao giờ trả về và exec() sẽ trả về một hình ảnh khác nếu thao tác
thành công.

(7). CPU0 sử dụng bảng điều khiển (bàn phím + LCD) cho I/O. Mỗi AP sử dụng một thiết bị đầu cuối UART chuyên dụng, ví dụ CPUi sử dụng UARTi cho I/O.

Vì mục tiêu chính ở đây là thể hiện việc quản lý quy trình trong SMP nên hệ thống chỉ hỗ trợ hai hình ảnh chế độ Người dùng, ký hiệu là u1 và

u2. Chúng được sử dụng để minh họa các hoạt động thay đổi hình ảnh của exec. Nếu muốn, người đọc có thể
Machine Translated by Google

Hạt nhân 9.15 SMP để quản lý quy trình 373

tạo thêm hình ảnh chế độ Người dùng để các quy trình thực thi. Sau đây liệt kê các đoạn mã chính của SMP

hệ thống, ký hiệu là C9.10. Để cho ngắn gọn, trình điều khiển thiết bị và trình xử lý ngoại lệ không được hiển thị.

9.15.1 Tệp ts.s

/*************** tệp ts.s của C9.10 ****************/

.chữ

.code 32

.global reset_handler, vectơ_start, vectơ_end

.global proc, iproc, iprocsize, chạy

.global tswitch, bộ lập lịch, goUmode, switchPgdir

.global int_on, int_off, setUlr

.global apStart, slock, sunlock, lock, unlock

.global get_cpuid, Enable_scu, send_sgi

reset_handler: // tất cả CPU bắt đầu thực thi reset_handler

// đặt ngăn xếp SVC thành CAO CẤP của iproc[CPUID]

MRC p15, 0, r11, c0, c0, 5 // đọc thanh ghi ID CPU vào R11

VÀ r11, r11, #0x03 // mặt nạ trong trường ID CPU; R11=CPUID

LDR sp, =iproc // r0 trỏ tới iproc's

LDR r1, =procsize // r1 -> procsize

LDR r2, [r1, #0] // r2 = xử lý

di chuyển r3, r2 // r3 = xử lý

chuyển động r1, r11 // lấy ID CPU

mul r3, r1 // xử lý*CPUID

thêm sp, r3, r3 // sp += r3

thêm sp, r2, r2 // sp->iproc[cpuid] cao cấp

// đặt chế độ trước đó của chế độ SVC thành chế độ NGƯỜI DÙNG

MSR spsr, #0x10 // ghi vào chế độ trước spsr

// chuyển sang chế độ IRQ để đặt ngăn xếp IRQ

MSR cpsr, #0xD // ghi vào cspr, vì vậy bây giờ ở chế độ IRQ

chuyển động r1, r11

thêm r1, r1, #1 // CPUID+1

ldr r0, =irq_stack // 16 KB vùng svc_stack trong t.ld

lsl r2, r1, #12 // r2 = (CPUID+1)*4096

CỘNG r0, r0, r2 // r0 -> irq_stack[cpuid] cao cấp

MOV sp, r0

// vào chế độ ABT để thiết lập ngăn xếp ABT

MSR cpsr, #0xD7

chuyển động r1, r11

thêm r1, r1, #1 // CPUID+1

ldr r0, =abt_stack // 16 KB vùng abt_stack trong t.ld

lsl r2, r1, #12 // r2 = (CPUID+1)*4096

CỘNG r0, r0, r2 // r0 -> irq_stack[cpuid] cao cấp

MOV sp, r0

// vào chế độ UND để thiết lập ngăn xếp UND

MSR cpsr, #0xDB

chuyển động r1, r11

thêm r1, r1, #1 // CPUID+1

ldr r0, =und_stack // 16 KB vùng không ngăn xếp trong t.ld

lsl r2, r1, #12 // r2 = (CPUID+1)*4096

CỘNG r0, r0, r2 // r0 -> irq_stack[cpuid] cao cấp

MOV sp, r0
Machine Translated by Google

374 9 Đa xử lý trong hệ thống nhúng

// quay lại chế độ SVC

MSR cpsr, #0x13 // ghi vào CPSR

chuyển động r1, r11 // CPUID

cmp r1, #0 // nếu không phải CPU0 thì bỏ qua

bỏ qua nhé

// chỉ CPU0 sao chép bảng vectơ và tạo pgdir ban đầu

BL sao chép_vector_table

BL mkPtable // tạo pgdir và pgtable trong C

nhảy:

ldr r0, Mtable // tất cả CPU sử dụng cùng một pgdir để kích hoạt MMU

mcr p15, 0, r0, c2, c0, 0 // đặt TTBase

mcr p15, 0, r0, c8, c7, 0 // tuôn ra TLB

// đặt tên miền 0: 01=client(kiểm tra quyền)

chuyển r0, #0x01 // 01 cho KHÁCH HÀNG

mcr p15, 0, r0, c3, c0, 0

// kích hoạt MMU

mrc p15, 0, r0, c1, c0, 0

hoặc r0, r0, #0x00000001 // đặt bit0

mcr p15, 0, r0, c1, c0, 0 // ghi vào c1

không

không

không

mrc p15, 0, r2, c2, c0

chuyển động r2, r2

// kích hoạt ngắt IRQ

bà r0, cpsr

BIC r0, r0, #0x80 // I bit=0 bật IRQ

MSR cpsr, r0 // ghi vào CPSR

chuyển động r1, r11

cmp r1, #0

bne Skip2

adr r0, khởi động chính // CPU0 gọi main()

máy tính ldr, [r0]

B .

bỏ qua2:

adr r0, APgo // AP gọi APstart()

máy tính ldr, [r0]

bảng: .word 0x4000

khởi đầu chính: .word chính

APgo: .word APstart

irq_handler: // IRQ ngắt điểm vào

phụ lr, lr, #4

stmfd sp!, {r0-r12, lr} // lưu tất cả các reg Umode trong kstack

bl irq_chandler // gọi irq_chandler() trong C trong file svc.c

ldmfd sp!, {r0-r12, pc}^ // bật từ kstack nhưng khôi phục Umode SR

data_handler:

phụ lr, lr, #4

stmfd sp!, {r0-r12, lr}

bl data_abort_handler

ldmfd sp!, {r0-r12, pc}^

chuyển đổi: // tswitch() trong Kmode

// che dấu các ngắt IRQ


Machine Translated by Google

Hạt nhân 9.15 SMP để quản lý quy trình 375

MRS r0, cpsr

ORR r0, r0, #0x80

MSR cpsr, r0

stmfd sp!, {r0-r12, lr}

LDR r4, =chạy // r4=&chạy

MRC p15, 0, r5, c0, c0, 5 // đọc thanh ghi CPUID tới r5

VÀ r5, r5, #0x3 // mặt nạ trong CPUID

di chuyển r6, #4 // r6 = 4

mul r6, r5 // r6 = 4*cpuid

thêm r4, r6 // r4 = &run[cpuid]

ldr r6, [r4, #0] // r6->chạy PROC

str sp, [r6, #4] // lưu sp vào PROC.ksp

lịch trình bl // gọi bộ lập lịch() trong C

LDR r4, =chạy // r4=&chạy

MRC p15, 0, r5, c0, c0, 5 // đọc thanh ghi CPUID tới r5

VÀ r5, r5, #0x3 // chỉ có ID CPU

di chuyển r6, #4 // r6 = 4

mul r6, r5 // r6 = 4*cpuid

thêm r4, r6 // r4 = &run[cpuid]

ldr r6, [r4, #0] // r6->chạy PROC

ldr sp, [r6, #4] // khôi phục sp từ PROC.ksp

// kích hoạt ngắt IRQ

MRS r0, cpsr BIC r0, r0, #0x80

MSR cpsr, r0

Ldmfd sp!, {r0-r12, pc} // tiếp tục chạy PROC tiếp theo

klr: .word 0

svc_entry: // điểm vào tòa nhà; thông số syscall trong r0-r3

stmfd sp!, {r0-r12, lr}

LDR r4, =chạy // r4=&chạy

MRC p15, 0, r5, c0, c0, 5 // đọc thanh ghi CPUID tới r5

VÀ r5, r5, #0x3 // mặt nạ trong CPUID

di chuyển r6, #4 // r6=4

mul r6, r5 // r6 = 4*cpuid

thêm r4, r4, r6 // r4 = &run[cpuid]

ldr r6, [r4, #0] // r6 -> PROC đang chạy

bà r7, spsr // Chế độ người dùng cpsr

str r7, [r6, #16] // lưu vào PROC.spsr

// lấy usp=r13 từ chế độ USER

bà r7, cpsr // r7 = Chế độ SVC cpsr

di chuyển r8, r7 // lưu một bản sao trong r8

orr r7, r7, #0x1F // r7 = chế độ SYS

msr cpsr, r7 // thay đổi cpsr sang chế độ SYS

// bây giờ ở chế độ SYS, r13 giống như chế độ Người dùng sp r14=chế độ người dùng lr

str sp, [r6, #8] // lưu usp vào PROC.usp tại offset 8

str lr, [r6, #12] // lưu Umode PC vào PROC.ups ở offset 12

// chuyển về chế độ SVC

msr cpsr, r8

// đã lưu lr trong kstack quay lại mục nhập svc, KHÔNG phải PC Umode tại syscall

// thay thế lr đã lưu trong kstak bằng Umode PC tại syscall

mov r5, sp

thêm r5, r5, #52 // offset = 13*4 byte từ sp


Machine Translated by Google

376 9 Đa xử lý trong hệ thống nhúng

ldr r7, [r6, #12] // lr trong Umode tại syscall

str r7, [r5]

// kích hoạt ngắt IRQ

MRS r7, cpsr

BIC r7, r7, #0x80 // I bit=0 bật IRQ

MSR cpsr, r7

bl svc_handler // gọi svc_chandler trong C

// thay thế r0 đã lưu trên ngăn xếp bằng giá trị trả về r từ svc_handler()

thêm sp, sp, #4 // bật r0 đã lưu ra khỏi ngăn xếp một cách hiệu quả

stmfd sp!,{r0} // đẩy r là r0 đã lưu vào Umode

goUmode:

LDR r4, =chạy // r4=&chạy

MRC p15, 0, r5, c0, c0, 5 // đọc thanh ghi CPUID tới r5

VÀ r5, r5, #0x3 // mặt nạ trong CPUID

di chuyển r6, #4 // r6 = 4

mul r6, r5 // r6 = 4*cpuid

thêm r4, r4, r6 // r4 = &run[cpuid]

ldr r6, [r4, #0] // r6->chạy PROC

ldr r7, [r6, #16] // khôi phục spsr từ PROC.spsr

msr spsr, r7 // khôi phục spsr

// đặt cpsr sang chế độ SYS để truy cập chế độ người dùng sp

bà r2, cpsr // r2 = Chế độ SVC cpsr

chuyển động r3, r2 // lưu một bản sao vào r3

hoặc r2, r2, #0x1F // r0 = chế độ SYS

msr cpsr, r2 // thay đổi cpsr sang chế độ SYS

// hiện đang ở chế độ SYS

ldr sp, [r6, #8] // khôi phục usp từ PROC.usp

// quay lại chế độ SVC

msr cpsr, r3 // quay lại chế độ SVC

ldmfd sp!, {r0-r12, pc}^ // quay lại Umode

// Các chức năng tiện ích

int_on: // int_on(int sr)

MSR cpsr, r0

di chuyển máy tính, lr

int_off: // int sr=int_off()

MRS r1, cpsr

chuyển động r0, r1 // r0 = r1

ORR r1, r1, #0x80

MSR cpsr, r1

di chuyển máy tính, lr // return r0=cpsr gốc

mở khóa: // mặt nạ trong IRQ trong cpsr

MRS r0, cpsr

BIC r0, r0, #0x80

MSR cpsr, r0

di chuyển máy tính, lr

kho a: // tạo ra IRQ trong cpsr

MRS r0, cpsr

ORR r0, r0, #0x80

MSR cpsr, r0

di chuyển máy tính, lr

switchPgdir: // chuyển pgdir sang pgdir của PROC mới

// r0 chứa địa chỉ pgdir của PROC


Machine Translated by Google

Hạt nhân 9.15 SMP để quản lý quy trình 377

mcr p15, 0, r0, c2, c0, 0 // đặt TTBase

chuyển r1, #0

mcr p15, 0, r1, c8, c7, 0 // tuôn ra TLB

mcr p15, 0, r1, c7, c10, 0 // tuôn ra TLB

mrc p15, 0, r2, c2, c0, 0

// đặt tên miền 0,1: all 01=client(kiểm tra quyền)

mov r0, #0xD //11|01 dành cho người quản lý|khách hàng

mcr p15, 0, r0, c3, c0, 0

mov pc, lr // // trở lại

setUlr: cho các luồng đang xử lý

// sang chế độ SYS; đặt ulr thành r0

bà r7, cpsr // r7 = Chế độ SVC cpsr

di chuyển r8, r7 // lưu một bản sao trong r8

orr r7, r7, #0x1F // r7 = chế độ SYS

msr cpsr, r7

// hiện đang ở chế độ SYS

mov lr, r0 // đặt r13 = r0 = VA(4)

// quay lại chế độ SVC

msr cpsr, r8 // quay lại chế độ SVC

di chuyển máy tính, lr

get_cpuid:

MRC p15, 0, r0, c0, c0, 5 // Đọc thanh ghi ID CPU

VÀ r0, r0, #0x03 // Tắt mặt nạ, để lại trường ID CPU

BX lr

kích hoạt_scu:
MRC p15, 4, r0, c15, c0, 0 // Đọc địa chỉ cơ sở ngoại vi

LDR r1, [r0, #0x0] // Đọc thanh ghi điều khiển SCU

ORR r1, r1, #0x1 // Đặt bit 0 (Bit Kích hoạt)

STR r1, [r0, #0x0] // Viết lại giá trị đã sửa đổi

BX lr

send_sgi: // sgi(filter=r0, CPUs=r1, intID=r2)

AND r0, r0, #0x0F // 4 bit thấp của filter_list

AND r1, r1, #0x0F // 4 bit thấp của CPU

AND r3, r2, #0x0F // thấp 4 bit intID vào r3

ORR r3, r3, r0, LSL #24 // điền vào trường bộ lọc

ORR r3, r3, r1, LSL #16 // điền vào trường CPU

// Lấy địa chỉ của GIC

MRC p15, 4, r0, c15, c0, 0 // Đọc địa chỉ cơ sở ngoại vi

THÊM r0, r0, #0x1F00 // Thêm offset của reg sgi_trigger

STR r3, [r0] // Ghi vào thanh ghi SGI (ICDSGIR)

BX lr

slock: // int slock(&spin)

ldrex r1, [r0]

cmp r1, #0x0

WENE

bn slock

mov r1, #1

strex r2, r1, [r0]

cmp r2, #0x0

bn slock

DMB // rào chắn

bx lr

nắng: // sunlock(&spin)

mov r1, #0x0

DMB
Machine Translated by Google

378 9 Đa xử lý trong hệ thống nhúng

str r1, [r0]

DSB

SEV

bx lr

vectơ_start:

PC LDR, reset_handler_addr

Máy tính LDR, undef_handler_addr

Máy tính LDR, svc_handler_addr

PC LDR, tìm nạp trước_abort_handler_addr

PC LDR, dữ liệu_abort_handler_addr

B .

Máy tính LDR, irq_handler_addr

PC LDR, fiq_handler_addr

đặt lại_handler_addr: .word reset_handler

undef_handler_addr: .word undef_handler

svc_handler_addr: .word svc_entry

prefetch_abort_handler_addr: .word prefetch_abort_handler

dữ liệu_abort_handler_addr: .word data_handler

irq_handler_addr: .word irq_handler

fiq_handler_addr: .word fiq_handler

vectơ_end:

.kết thúc

Giải thích về tệp ts.s: Để hỗ trợ SMP, hệ

thống xác định NCPU = 4 toàn cục (trong tệp kernel.c)

PROC iporc[NCPU]; // PROC ban đầu của CPU

PROC *chạy[NCPU]; // con trỏ tới các CPU thực thi PROC

Trong kernel_init(), chúng ta khởi tạo run[i]=&iproc[i], sao cho mỗi run[i] trỏ đến cấu trúc PROC ban đầu của CPUi. Trong quá
trình vận hành hệ thống, mỗi lần chạy[i] trỏ đến quá trình thực thi trên CPUi. Xuyên suốt mã C của hệ thống, nó sử dụng ký hiệu
chạy

#define đang chạy run[get_cpuid()]

để truy cập PROC đang thực thi trên CPU. Mã lắp ráp trong ts.s bao gồm các phần sau.

(1). Reset_handler: Tất cả các CPU bắt đầu bằng cách thực thi reset_handler ở chế độ SVC với các ngắt bị tắt và MMU bị tắt.
Đầu tiên, nó lấy ID CPU và lưu nó vào R11, để sau này nó không phải lấy lại ID CPU nhiều lần. Nó đặt sp chế độ SVC ở mức cao
nhất của iproc[CPUID], làm cho kstack của iproc[CPUID] trở thành ngăn xếp ban đầu của CPU. Nó đặt chế độ trước đó (SPSR) thành
chế độ Người dùng, giúp CPU sẵn sàng chạy các hình ảnh ở chế độ Người dùng sau này. Sau đó, nó thiết lập ngăn xếp của các chế
độ đặc quyền khác. Trong số các CPU, chỉ CPU0 sao chép bảng vectơ sang địa chỉ 0 và tạo bảng trang một cấp ban đầu bằng cách sử
dụng các phần 1 MB ở 16KB, ID này ánh xạ VA 512 MB thấp tới PA. Sau đó, tất cả các CPU sử dụng cùng một bảng trang để định cấu
hình và kích hoạt MMU cho việc dịch VA sang PA. Sau đó, nó cho phép các chức năng ngắt và gọi IRQ trong tệp tc. CPU0 gọi main()
và tất cả các AP khác gọi APstart() để tiếp tục khởi tạo cho đến khi chúng sẵn sàng chạy các tác vụ.
(2). Trình xử lý IRQ và Ngoại lệ: Chúng giống như trước đây. Để giữ cho hệ thống đơn giản, kernel không có tính ưu tiên, tức
là các ngắt IRQ không chuyển đổi tiến trình trong chế độ kernel. Chúng tôi sẽ mở rộng kernel để cho phép lập kế hoạch xử lý trước
sau này. (3). tswitch:
tswitch dành cho chuyển đổi tiến trình ở chế độ kernel (SVC). Trong tswitch, trước tiên quá trình gọi sẽ vô hiệu hóa các ngắt và
thu được một khóa quay được liên kết với hàng đợi sẵn sàng. Sau khi lưu bối cảnh chế độ nhân tiến trình (trong PROC.kstack) và
con trỏ ngăn xếp, nó gọi bộ lập lịch () để chọn quy trình chạy tiếp theo. Spinlock được giải phóng bởi quá trình chạy tiếp theo
khi nó tiếp tục.
Machine Translated by Google

Hạt nhân 9.15 SMP để quản lý quy trình 379

(4). svc_entry: đây là điểm vào cuộc gọi hệ thống. Các quy trình ở chế độ Người dùng sẽ thực hiện lệnh gọi hệ thống (thông qua SWI) để nhập kernel vào

thực hiện các chức năng hạt nhân. Khi nhập, nó lưu bối cảnh chế độ Người dùng, (upc, ucpsr, usp), vào cấu trúc PROC để quay lại

Chế độ người dùng sau. Sau đó, nó cho phép ngắt và gọi svc_chandler() trong C để xử lý lệnh gọi hệ thống.

(5). goUmode: Khi một tiến trình kết thúc cuộc gọi hệ thống, nó sẽ thoát kernel để trở về chế độ Người dùng. Trong goUmode, nó khôi phục

đã lưu bối cảnh chế độ Người dùng từ cấu trúc PROC, theo sau là LDMFD SP!, {R0-R12, SP}^, khiến quay lại chế độ Người dùng.

Mỗi lệnh gọi hệ thống trả về một giá trị cho chế độ Người dùng, ngoại trừ kexit() không bao giờ trả về. Giá trị trả về là thông qua R0 trong

xử lý kstack.

(6). switchPgdir: Khi hệ thống khởi động, nó sử dụng bảng trang một cấp ban đầu ở mức 16KB với các phần 1MB. Trong kernel_init(),

nó xây dựng bảng trang 2 cấp (pgdir) ở mức 32KB và bảng trang cấp 2 ở mức 5 MB. Sau đó, nó chuyển pgdir sang sử dụng 2 cấp độ

phân trang động. Mỗi tiến trình có bảng trang pgdir và cấp 2 riêng. Trong quá trình chuyển đổi tiến trình, bộ lập lịch sử dụng

switchPgdir để chuyển pgdir của CPU sang tiến trình đang chạy tiếp theo.

(7). Các hàm tiện ích: mã hợp ngữ còn lại chứa các hàm tiện ích, chẳng hạn như slock/sunlock, Enable_scu, get_cpuid,

send_sgi, v.v. để hỗ trợ các hoạt động SMP.

9.15.2 Tệp tc

/****************** tập tin tc của C9.10 ****************/

#include "type.h"

#xác định NCPU 4

#define đang chạy run[get_cpuid()]

#include "uart.c" // trình điều khiển UART

#include "kbd.c" // trình điều khiển KBD

#include "ptimer.c" // trình điều khiển bộ định thời cục bộ

#include "vid.c" // Trình điều khiển LCD

#include "ngoại trừ.c" // trình xử lý ngoại lệ

#include "queue.c" // hàm xếp hàng

#include "kernel.c" // mã hạt nhân

#include "chờ.c" // ngủ/thức, thoát, chờ

#include "fork.c" // hàm phân nhánh

#include "exec.c" // thay đổi hình ảnh

#include "svc.c" // bảng định tuyến cuộc gọi hệ thống

#include "loadelf.c" // Trình tải ảnh ELF

#include "thread.c" // chủ đề

#include "sdc.c" // trình điều khiển SDC

int copy_vector_table(){ // CÙNG như trước }

int config_int(int N, int targetCPU){ // CÙNG như trước }

int aplock = 0; // spinlock cho AP

biến động int ncpu = 1; // số lượng CPU sẵn sàng

int APstart()

slock(&aplock); // thu được spinlock

int cpu = get_cpuid();

printf("CPU%d trong Apstart switchPgdir to 0x8000\n", cpu);

switchPgdir(0x8000); // chuyển sang phân trang 2 cấp

config_int(29,cpu); ptimer_init(); // cấu hình và khởi động bộ đếm thời gian cục bộ

ptimer_start();

printf("CPU%d nhập vòng lặp run_task()\n", cpu);

ncpu++;

khóa nắng(&aplock); // giải phóng spinlock


Machine Translated by Google

380 9 Đa xử lý trong hệ thống nhúng

run_task(); // tất cả các Aps gọi run_task()

int config_gic()

int cpuid = get_cpuid();

// thiết lập thanh ghi mặt nạ ưu tiên int

*(int *)(GIC_BASE + 0x104) = 0xFFFF;

// thiết lập thanh ghi điều khiển giao diện CPU: cho phép ngắt tín hiệu

*(int *)(GIC_BASE + 0x100) = 1;

// đặt thanh ghi điều khiển phân phối để định tuyến các ngắt tới CPU

*(int *)(GIC_BASE + 0x1000) = 1;

config_int(29, 0); // Bộ định thời cục bộ CPU0 tại intID=29

config_int(44, 0); // UART0

config_int(45, 1); // UART1

config_int(46, 2); // UART2

config_int(47, 3); // UART3

config_int(49, 0); // SDC ngắt tới CPU0

config_int(52, 0); // KBD

#define CHIA SẺ 0x00014c06

#define KHÔNG CHIA SẺ 0x00000c1e

#xác định THIẾT BỊ 0x00000c06

int mkPtable() // tạo pgdir một cấp ban đầu

int tôi;

u32 *ut = (u32 *)0x4000; // ở mức 16KB

u32 mục = 0 | CHIA SẺ; // tên miền 0

for (i=0; i<4096; i++) // xóa pgdir entris

ut[i] = 0;

for (i=0; i<512; i++){ // Bản đồ ID 512MB

ut[i] = mục nhập;

mục += 0x100000;

ut[256] = (256*0x100000) | THIẾT BỊ; // Trang I/O có dung lượng 256MB

int irq_chandler()

int intID = *(int *)0x1F00010C; // đọc thanh ghi intID

chuyển đổi (intID) {

trường hợp 29: ptimer_handler(); phá vỡ;

trường hợp 44 : uart_handler(0); phá vỡ;

trường hợp 45 : uart_handler(1); phá vỡ;

trường hợp 46 : uart_handler(2); phá vỡ;

trường hợp 47 : uart_handler(3); phá vỡ;

trường hợp 49 : sdc_handler(); phá vỡ;

trường hợp 52 : kbd_handler(); phá vỡ;

*(int *)0x1F000110 = intID; // viết EOF

int chính()

{ int tôi;

fbuf_init(); // khởi tạo LCD

printf("============ Chào mừng bạn đến với Wanix trong ARM =============\n");

Enable_scu(); // kích hoạt SCU

sdc_init(); // khởi tạo SDC


Machine Translated by Google

Hạt nhân 9.15 SMP để quản lý quy trình 381

kbd_init(); // khởi tạo KBD

uart_init(); // khởi tạo UART

config_gic(); // cấu hình GIC

ptimer_init(); // cấu hình bộ đếm thời gian cục bộ

ptimer_start(); // khởi động bộ đếm thời gian cục bộ

kernel_init(); // khởi tạo hạt nhân

printf("AP khởi động CPU0\n");

int *APaddr = (int *)0x10000030;

*APaddr = (int)0x10000; // AP bắt đầu địa chỉ thực thi

send_sgi(0x00, 0x0F, 0x01); // gửi SGI tới các AP đánh thức

printf("CPU0 đợi AP sẵn sàng\n");

while(ncpu < 4);

printf("CPU0 tiếp tục\n");

// CPU0: tạo các PROC NCPU vào hàng đợi sẵn sàng

cho (i=0; i<NCPU; i++)

kfork("/bin/u1");

run_task(); // CPU0 gọi run_task()

9.15.3 Tệp Kernel.c

/********************* tệp kernel.c của C9.10 *******************/

PROC proc[NPROC+NTHREAD];

PROC *chạy[NCPU] // thực thi con trỏ PROC

PROC iproc[NCPU]; // PROC ban đầu của CPU

PROC *freeList, *tfreeList, *readyQueue, *sleepList;

int khóa tự do, tfreelock, khóa sẵn sàng, khóa ngủ; // spinlocks

int procsize = sizeof(PROC);

char *pname[NPROC]={"mặt trời", "thủy ngân", "sao kim", "trái đất", "sao hỏa",

"Sao Mộc", "Sao Thổ", "Sao Thiên Vương", "Sao Hải Vương", "Sao Diêm Vương"};

// pfreeList,free_Page_list(),palloc(),pdealloc(): phân trang động

int *pfreeList; // danh sách khung trang miễn phí

int pfreelock; // spinlock cho pfreeList

int *palloc() // cấp phát khung trang trống

slock(&pfreelock);

int *p = pfreeList;

Nếu p)

pfreeList = (int *)(*p);

khóa nắng(&pfreelock);

trả lại p;

int pdealloc(int *p) // giải phóng một khung trang

slock(&tfrelock);

int *a = (int *)((int)p & 0xFFFFFF000);

*a = (int)pfreeList;

pfreeList = a;

khóa nắng(&pfreelock);

}
Machine Translated by Google

382 9 Đa xử lý trong hệ thống nhúng

int *free_page_list(int *startva, int *endva)

int *p;

printf("build pfreeList: start=%x end=%x : ", startva, endva); pfreeList = startva;

p = bắt đầu;

while(p < (int *)(endva-1024)){ *p = (int)(p +

1024); p += 1024;

tôi++;

*p = 0;

printf("%d mục 4KB\n", i);

trở lại bắt đầu;

u32 *MTABLE = (u32 *)0x4000;

int *kpgdir = (int *)0x8000;

int kernel_init()

int i, j, *ip;

PROC *p;

int *MTABLE, *mtable, *ktable, *pgtable;

int paddr;

printf("kernel_init(): init procs\n");

for (i=0; i<NCPU; i++){ // khởi tạo cho mỗi biến CPU

p = &iproc[i];

p->pid = 1000 + i; // pid đặc biệt của PROC ban đầu

p->trạng thái = SẴN SÀNG;

p->ưu tiên = 0;

p->ppid = p->pid;

p->pgdir = (int *)0x8000; // pgdir ở 0x8000

chạy[i] = p; // run[i]=&iproc[i]

cho (i=0; i<NPROC; i++){ // khởi tạo procs

p = &proc[i]; p->pid

= tôi;

p->trạng thái = MIỄN PHÍ;

p->ưu tiên = 0; p->ppid

= 0; strcpy(p-

>name, pname[i]); p->tiếp theo = p + 1;

p->pgdir = (int *)

(0x600000 + (p->pid-1)*0x4000); }

for (i=0; i<NTHREAD; i++){ // chủ đề

p = &proc[NPROC+i];

p->pid = NPROC + i;

p->trạng thái = MIỄN PHÍ;

p->ưu tiên = 0;

p->ppid = 0;

p->tiếp theo = p + 1;

proc[NPROC-1].next = 0;

freeList = &proc[1]; // bỏ qua proc[0], để P1 là proc[1]

hàng đợi sẵn sàng = 0;

Danh sách ngủ = 0;

tfreeList = &proc[NPROC];
Machine Translated by Google

Hạt nhân 9.15 SMP để quản lý quy trình 383

proc[NPROC+NTHREAD-1].next = 0;

// CPU0: chạy iproc ban đầu[0]

đang chạy = chạy[0]; // CPU0 chạy iproc[0]

MTABLE = (int *)0x4000; // pgdir ban đầu ở 16KB printf("xây dựng pgdir và

pgtables ở 32KB (ID ánh xạ 512MB VA tới PA)\n"); mtable = (u32 *)0x8000; // pgdir mới có kích thước 32KB

for (i=0; i<4096; i++){ // loại bỏ 4096 mục

mtable[i] = 0;

for (i=0; i<512; i++){ // ĐÁNH GIÁ 512MB PA: ID được ánh xạ VA sang PA mtable[i] = (0x500000 +

i*1024) | 0x01; // DOMAIN0,Type=01

printf("xây dựng pgtable Kmode cấp 2 với dung lượng 5MB\n");

cho (i=0; i<512; i++){

pgtable = (u32 *)((u32)0x500000 + (u32)i*1024);

paddr = i*0x100000 | 0x55E; // tất cả các AP=01|01|01|01|CB=11|type=10

for (j=0; j<256; j++){ // 256 mục, mỗi mục trỏ tới 4KB PA

pgtable[j] = paddr + j*4096; // tăng thêm 4KB

printf("xây dựng 64 pgdir cấp 1 cho PROC ở mức 6 MB\n");

ktable = (u32 *)0x600000; // xây dựng pgdir của 64 proc với dung lượng 6 MB

cho (i=0; i<64; i++){ // vùng 512KB trong 6MB

ktable = (u32 *)(0x600000 + i*0x4000); // mỗi ktable = 16KB for (j=0; j<4096; j++){ ktable[j] =

0;

// sao chép các mục thấp của mtable của P0 [ ], các khoảng trống Kmode giống nhau cho (j=0;

j<1024; j++){

ktable[j] = mtable[j];

// Mục nhập pgdir Umode [2048] sẽ được đặt khi Proc được tạo

ktable[2048] = 0;

printf("chuyển pgdir sang sử dụng phân trang 2 cấp: ");

switchPgdir((u32)mtable);

printf("đã chuyển pgdir OK\n");

// xây dựng danh sách khung trang miễn phí: bắt đầu ở 8 MB kết thúc = 256 MB

pfreeList = free_page_list((int *)0x00800000, (int *)0x10000000);

// tất cả các CPU chạy tác vụ từ cùng một hàng đợi sẵn sàng

int run_task()

trong khi(1){

slock(&readylock);

nếu (sẵn sàng == 0){

khóa nắng(&readylock);

asm("WFI");

khác{

tswitch();

}
Machine Translated by Google

384 9 Đa xử lý trong hệ thống nhúng

bộ lập lịch int()

int cpuid = get_cpuid();

PROC *cũ = đang chạy; //

người gọi đã giữ spinlock ReadyQueue if (running->pid < 1000){ // IDLE

procs KHÔNG nhập ReadyQueue

if (đang chạy->trạng thái == SẴN SÀNG){

enqueue(&readyQueue, đang chạy);

đang chạy = dequeue(&readyQueue); nếu (đang

chạy == 0){ // nếu hàng đợi trống

đang chạy = &iproc[cpuid];// chạy chương trình IDLE

if (đang chạy != cũ){

switchPgdir((u32)running->pgdir);

khóa nắng(&readylock); // spinlock phát hành PROC chạy tiếp theo

9.15.4 Trình điều khiển thiết bị và Trình xử lý ngắt trong SMP

Hệ thống SMP mẫu hỗ trợ các thiết bị I/O sau.

Màn hình LCD: vid.c

UART: uart.c

ĐỒNG HỒ GIỜ: ptimer.c

Bàn phím: kbd.c

SDC: sdc.c

Tất cả các trình điều khiển thiết bị (ngoại trừ màn hình LCD) đều được điều khiển bằng ngắt. Trong trình điều khiển thiết bị điều

khiển ngắt, các quy trình và trình xử lý ngắt chia sẻ bộ đệm dữ liệu và các biến điều khiển, tạo thành Vùng quan trọng (CR). Trong nhân

bộ xử lý đơn, khi một tiến trình thực thi trình điều khiển thiết bị, nó có thể che giấu các ngắt để ngăn chặn sự can thiệp từ trình xử

lý ngắt của thiết bị. Trong SMP, việc che dấu các ngắt không còn đủ nữa. Điều này là do trong khi một tiến trình thực thi trình điều

khiển thiết bị trên một CPU thì một CPU khác có thể thực thi trình xử lý ngắt thiết bị cùng lúc. Để ngăn chặn các tiến trình và trình

xử lý ngắt can thiệp lẫn nhau, trình điều khiển thiết bị trong SMP phải được sửa đổi. Điều này có thể đạt được bằng cách yêu cầu các

tiến trình và trình xử lý ngắt thực thi bên trong một vùng quan trọng chung. Vì trình xử lý ngắt không thể ngủ hoặc chặn nên chúng phải

sử dụng cơ chế spinlock hoặc tương đương. Phía tiến trình của trình điều khiển thiết bị có thể sử dụng cơ chế chặn, chẳng hạn như

semaphore, để chờ nhưng nó phải giải phóng khóa xoay trước khi bị chặn. Sau đây, chúng tôi minh họa các nguyên tắc thiết kế trình điều

khiển SMP bằng cách sử dụng trình điều khiển SDC làm ví dụ cụ thể.

9.15.4.1 Trình điều khiển SDC trong SMP

Trình điều khiển SDC hỗ trợ đọc/ghi các khối đa ngành. Các ngắt SDC được định tuyến đến một CPU cụ thể, ví dụ CPU0, xử lý tất cả các

ngắt SDC. Để tuần tự hóa việc thực thi quy trình và trình xử lý ngắt, cả hai phải thực thi bên trong cùng một vùng quan trọng của khóa

xoay (sdclock). Khi một tiến trình thực thi get_block() hoặc put_block(), trước tiên nó sẽ nhận được spinlock.

Trước khi phát lệnh đọc/ghi tới SDC, nếu tiến trình đang chạy trên CPU0, trước tiên nó phải giải phóng spinlock.

Nếu không, quy trình sẽ tự khóa trên cùng một khóa xoay mà nó vẫn giữ, dẫn đến một dạng bế tắc suy biến. Nếu tiến trình đang chạy trên

các CPU khác, nó có thể giải phóng spinlock sau khi đưa ra các lệnh I/O. Sau đó, quá trình chờ quá trình truyền dữ liệu hoàn tất bằng

P(&sdc_sem), điều này có thể chặn quá trình trên semaphore sdc_sem.

Khi trình xử lý ngắt thực thi (trên CPU0), trước tiên nó sẽ lấy spinlock để xử lý ngắt hiện tại. Nó giải phóng spinlock vào cuối

mỗi quá trình xử lý ngắt. Khi quá trình chuyển khối SDC hoàn tất, nó sẽ phát hành V(&sdc_sem) để bỏ chặn quá trình. Sau đây liệt kê mã

trình điều khiển SDC.


Machine Translated by Google

Hạt nhân 9.15 SMP để quản lý quy trình 385

/****************** tập tin sdc.c của C9.10 *******************/

#include "sdc.h" // các loại và cấu trúc SDC

int sdclock = 0; // khóa xoay trình điều khiển SDC

cấu trúc semaphore sdc_sem; // semaphore để đồng bộ hóa trình điều khiển SDC

int P(struct semaphore *s){ // P thao tác }

int V(struct semaphore *s){ // V thao tác }

// các biến được chia sẻ giữa trình điều khiển SDC và trình xử lý ngắt

char dễ bay hơi *rxbuf, *txbuf;

biến động int rxcount, txcount;

int sdc_handler()

trạng thái u32, status_err, *up;

int i, cpuid=get_cpuid();

slock(&sdclock); // thu được spinlock

// đọc thanh ghi trạng thái để tìm ra TXempty hoặc RxAvail

trạng thái = *(u32 *)(cơ sở + TÌNH TRẠNG);

if (status & (1<<17)){ // RxFull: đọc 16 u32 cùng một lúc;

up = (u32 *)rxbuf;

status_err = trạng thái & (DCRCFAIL | DTIMEOUT | RXOVERR); if (!status_err && rxcount)

{ for (i = 0; i < 16; i++) *(up + i) = *(u32

*)(base + FIFO);

lên += 16;

rxcount -= 64;

rxbuf += 64;

trạng thái = *(u32 *)(cơ sở + TÌNH TRẠNG); // xóa ngắt Rx

nếu (rxcount == 0){ // đọc xong khối

do_command(12, 0, MMC_RSP_R1); // dừng truyền

V(&sdc_sem); // Quá trình V lên

else if (status & (1<<18)){ // TXempty: viết 16 u32 mỗi lần

up = (u32 *)txbuf;

status_err = trạng thái & (DCRCFAIL | DTIMEOUT);

if (!status_err && txcount) {

vì (i = 0; i < 16; i++)

*(u32 *)(cơ sở + FIFO) = *(up + i);

lên += 16;

txcount -= 64;

txbuf += 64; // nâng cao txbuf cho lần viết tiếp theo

trạng thái = *(u32 *)(cơ sở + TÌNH TRẠNG); // xóa ngắt Tx

nếu (txcount == 0){ // ghi khối xong

do_command(12, 0, MMC_RSP_R1); // dừng truyền

V(&sdc_sem); // Quá trình V lên

//printf("ghi vào SDC status_clear register\n");

*(u32 *)(base + STATUS_CLEAR) = 0xFFFFFFFF;

sunlock(&sdclock); // giải phóng spinlock

}
Machine Translated by Google

386 9 Đa xử lý trong hệ thống nhúng

int delay(){ int i; cho (i=0; i<100; i++); }

int do_command(int cmd, int arg, int resp)

*(u32 *)(cơ sở + ARGUMENT) = (u32)arg;

*(u32 *)(cơ sở + LỆNH) = 0x400 | (resp<<6) | cmd;

trì hoãn();

int sdc_init()

u32 RCA = (u32)0x45670000; // RCA mã hóa cứng của QEMU

căn cứ = (u32)0x10005000; // Địa chỉ cơ sở PL180

printf("sdc_init() "); *(u32 *)(cơ

sở + POWER) = (u32)0xBF; // bật nguồn

*(u32 *)(cơ sở + CLOCK) = (u32)0xC6; // CLK mặc định

// gửi chuỗi lệnh init

do_command(0, 0, MMC_RSP_NONE);// trạng thái rảnh

do_command(55, 0, MMC_RSP_R1); // trạng thái sẵn sàng

do_command(41, 1, MMC_RSP_R3); // đối số không được bằng 0

do_command(2, 0, MMC_RSP_R2); // hỏi CID thẻ

do_command(3, RCA, MMC_RSP_R1); // gán RCA

do_command(7, RCA, MMC_RSP_R1); // trạng thái truyền: phải sử dụng RCA

do_command(16, 512, MMC_RSP_R1); // thiết lập độ dài khối dữ liệu

// đặt bit thanh ghi MASK0 ngắt = RxFULL(17)|TxEmpty(18) *(u32 *)(base + MASK0) = (1<<17)|

(1<<18); // khởi tạo semaphore spinlock và sdc_sem

sdclock = 0;

sdc_sem.lock = 0; sdc_sem.value = 0; sdc_sem.queue = 0;

int get_block(int blk, char *buf)

u32 cmd, arg; int

cpuid=get_cpuid();

slock(&sdclock); // quá trình lấy spinlock

rxbuf = buf; rxcount = FBLK_SIZE;

*(u32 *)(cơ sở + DATATIMER) = 0xFFFF0000;

// ghi data_len vào datalength reg

*(u32 *)(cơ sở + DATALENGTH) = FBLK_SIZE;

if (cpuid==0) // CPU0 xử lý các ngắt SDC: phải giải phóng sdclock

sunlock(&sdclock);

cmd = 18; // CMD18: đọc đa cung

arg = ((bsector + blk*2)*512);

do_command(cmd, arg, MMC_RSP_R1);

// 0x93=|9|0011|=|9|DMA=0,0=BLOCK,1=Máy chủ<-Card,1=Bật

*(u32 *)(cơ sở + DATAACTRL) = 0x93;

if (cpuid) // Các CPU khác giải phóng spinlock

sunlock(&sdclock);

P(&sdc_sem); // đợi khối đọc hoàn tất

int put_block(int blk, char *buf)

u32 cmd, arg;

int cpuid = get_cpuid();

slock(&sdclock); // quá trình lấy spinlock

txbuf = buf; txcount = FBLK_SIZE;

*(u32 *)(cơ sở + DATATIMER) = 0xFFFF0000;


Machine Translated by Google

Hạt nhân 9.15 SMP để quản lý quy trình 387

*(u32 *)(cơ sở + DATALENGTH) = FBLK_SIZE;

if (cpuid == 0) // CPU0 xử lý các ngắt SDC, phải giải phóng sdclock

sunlock(&sdclock);

cmd = 25; // CMD25: viết đa cung

arg = (u32)((bsector + blk*2)*512);

do_command(cmd, arg, MMC_RSP_R1);

// ghi 0x91=|9|0001|=|9|DMA=0,BLOCK=0,0=Host->Thẻ, Bật

*(u32 *)(cơ sở + DATAACTRL) = 0x91; // Máy chủ-> thẻ

nếu (cpuid) // các CPU khác giải phóng spinlock

sunlock(&sdclock);

P(&sdc_sem); //đợi trình xử lý ngắt kết thúc

9.15.5 Trình diễn quản lý quy trình trong SMP

Hình 9.10 cho thấy kết quả đầu ra mẫu khi chạy chương trình C9.10, thể hiện quản lý quy trình là SMP.

Khi hệ thống khởi động, CPU0 tạo các tiến trình NCPU=4, từ P1 đến P4, tất cả đều có cùng hình ảnh chế độ Người dùng u1 và nhập chúng vào hàng

đợi sẵn sàng. Sau đó, nó gửi SGI để kích hoạt các CPU (AP) khác. Mỗi AP bắt đầu thực thi reset_handler để tự khởi tạo. Sau đó nó gọi APstart(). Vì

CPU0 được chỉ định để xử lý các ngắt SDC nên nó có thể tải tệp hình ảnh u1 từ SDC trước khi các AP được kích hoạt và chạy. Nếu các ngắt SDC được

định tuyến đến các CPU khác nhau, việc tải hình ảnh phải được thực hiện sau khi các AP hoạt động. Khi các CPU đã sẵn sàng, tất cả chúng đều gọi

run_task(), cố gắng chạy các tác vụ từ cùng một hàng đợi sẵn sàng. Nếu hàng đợi sẵn sàng không trống, CPU sẽ lấy một quy trình từ hàng đợi sẵn sàng

và chuyển sang chạy quy trình đó. Nếu hàng đợi trống, nó sẽ chạy tiến trình không hoạt động, đưa CPU vào trạng thái WFI, chờ ngắt. Vì mỗi CPU có bộ

hẹn giờ cục bộ nên nó sẽ khởi động định kỳ để xử lý ngắt bộ hẹn giờ và sau đó thử chạy lại tác vụ từ hàng đợi sẵn sàng. Ngoài ra, các CPU cũng có

thể sử dụng SGI để liên lạc với nhau. Ví dụ: khi CPU tạo một quy trình sẵn sàng chạy, ví dụ như bằng thao tác fork, Wakeup hoặc V, nó sẽ gửi SGI

đến các CPU khác, khiến chúng chạy lại các tác vụ từ ReadyQueue. Khi chạy ở chế độ Người dùng, mỗi quy trình sẽ hiển thị một menu, bao gồm ID CPU

mà quy trình đang chạy, như trong

proc x chạy trên CPUy: nhập lệnh:


————————————————————————————————————————————————————— ——————————————————

| ps chname switch chờ thoát ngã ba exec vfork thread sgi |


————————————————————————————————————————————————————— ——————————————————

Sau đó, nó sẽ nhắc nhập lệnh đầu vào và thực thi lệnh đó. Hoạt động của các lệnh là

tái bút: in thông tin trạng thái của tất cả PROC

chname: thay đổi chuỗi tên của PROC đang chạy. switch: nhập kernel để chuyển đổi tiến

trình chờ: đợi một ZOMBIE con, trả về pid con và thoát trạng

thái thoát: chấm dứt trong kernel để trở thành ZOMBIE, đánh thức nhánh cha mẹ: phân nhánh một PROC con có

hình ảnh chế độ người dùng giống hệt exec: thay đổi hình ảnh thực thi vfork : vđưa một đứa trẻ và

chờ đợi; vforked child exec để chạy hình ảnh u2 và chấm dứt

thread: tạo một thread và chờ đợi; luồng thực thi và kết thúc.

nói: gửi SGI đến CPU mục tiêu, khiến nó thực thi chức năng xử lý SGI

Người đọc có thể nhập lệnh để kiểm tra hệ thống. Như một hệ quả tự nhiên của SMP, các tiến trình có thể di chuyển từ một dạng
CPU để thực thi trên các CPU khác nhau.
Machine Translated by Google

388 9 Đa xử lý trong hệ thống nhúng

Hình 9.10 Trình diễn quản lý quy trình trong SMP


Machine Translated by Google

9.16 Hệ điều hành SMP mục đích chung 389

9.16 Hệ điều hành SMP mục đích chung

Trong phần này, chúng tôi sẽ trình bày một hệ điều hành SMP có mục đích chung, được ký hiệu là SMP_EOS, cho kiến trúc ARM.

9.16.1 Tổ chức SMP_EOS

Hệ thống SMP_EOS về cơ bản là hệ thống EOS đơn bộ xử lý tương tự của Chap. số 8 thích nghi với SMP.

9.16.1.1 Nền tảng phần cứng SMP_EOS

có thể chạy trên mọi hệ thống dựa trên ARM MPcore hỗ trợ các thiết bị I/O phù hợp. Vì hầu hết người đọc có thể không có quyền truy cập

vào hệ thống phần cứng dựa trên ARM MPcore thực sự nên chúng tôi sẽ sử dụng máy ảo ARM Realview-pbx-a9 mô phỏng trong QEMU làm nền tảng

để triển khai và thử nghiệm. Máy ảo Realview-pbx-a9 được mô phỏng hỗ trợ các thiết bị I/O sau.

(1). SDC: SMP_EOS sử dụng đĩa ảo SDC làm thiết bị lưu trữ dung lượng lớn chính. Để đơn giản, SDC chỉ có một phân vùng, bắt đầu từ khu

vực (mặc định fdisk) 2048. Sau khi tạo tệp SDC ảo, chúng tôi thiết lập một thiết bị lặp cho phân vùng và định dạng nó dưới dạng hệ

thống tệp EXT2 với kích thước khối 4KB và một nhóm khối. Sau đó, chúng tôi gắn thiết bị vòng lặp và điền vào đó các DIR và tệp, làm cho

thiết bị sẵn sàng để sử dụng. Kích thước hệ thống tệp kết quả là 128MB, đủ lớn cho hầu hết các ứng dụng. Giả định nhóm khối đơn trên

ảnh đĩa chỉ nhằm mục đích thuận tiện vì nó đơn giản hóa cả quá trình truyền tải hệ thống tệp cũng như các thuật toán quản lý nút và

khối đĩa. Nếu muốn, SDC có thể được tạo bằng nhiều nhóm khối cho các hệ thống tệp lớn hơn hoặc thậm chí với nhiều phân vùng. Sơ đồ sau

đây hiển thị nội dung SDC.

—————————|————————————————Phân vùng 1 ————————————————————————— ———| |M|booter|siêu|


gd |. . . |bmap|imap|inodes |khối dữ liệu | |
———————————————————————————————————————————————————— ————————————
|< ——————————————— EXT2 FS ——————————————————————————— >| |– bin : tập
tin lệnh thực thi nhị phân |– boot: hình ảnh hạt
nhân có thể khởi động |– dev : tập tin
đặc biệt (thiết bị I/O) |– etc : tập tin
passwd |– người dùng:
thư mục chính của người dùng

Trên SDC, khu vực MBR (0) chứa bảng phân vùng và phần đầu của bộ khởi động. Toàn bộ booter được cài đặt trong các cung từ 2 đến

booter_size, giả sử kích thước của booter không quá 2046 cung. (Kích thước booter thực tế nhỏ hơn 10KB). Trình khởi động được thiết

kế để khởi động hình ảnh hạt nhân từ hệ thống tệp EXT2 trong phân vùng SDC. Sau khi khởi động, hạt nhân SMP_EOS gắn phân vùng SDC làm

hệ thống tệp gốc và chạy trên phân vùng SDC.

(2). LCD: LCD là thiết bị hiển thị chính. Màn hình LCD và bàn phím đóng vai trò là bảng điều khiển hệ thống.

(3). Bàn phím: đây là thiết bị bàn phím của máy ảo Realview-pbx-a9. Nó là thiết bị đầu vào cho cả bảng điều khiển và thiết bị đầu cuối
nối tiếp UART.

(4). UART: đây là (4) UART của Realview-pbx-a9 VM, được sử dụng làm thiết bị đầu cuối nối tiếp để người dùng đăng nhập.

Mặc dù rất khó có khả năng một hệ thống nhúng có nhiều người dùng nhưng mục đích của chúng tôi là chứng tỏ rằng SMP_EOS có khả năng hỗ

trợ nhiều người dùng cùng một lúc.

(5). Bộ hẹn giờ: Realview-pbx-a9 VM có bốn bộ hẹn giờ trong vùng bộ nhớ thiết bị ngoại vi. Ngoài ra, mỗi CPU còn có một bộ định thời cục

bộ riêng. Trong SMP_EOS, mỗi CPU sử dụng bộ đếm thời gian cục bộ của riêng nó để định thời gian và lập lịch xử lý. Chức năng dịch vụ hẹn

giờ cho tất cả các tiến trình được cung cấp bởi CPU0.
Machine Translated by Google

390 9 Đa xử lý trong hệ thống nhúng

9.16.2 Cây tệp nguồn SMP_EOS

Các tệp nguồn của SMP_EOS được tổ chức dưới dạng cây tệp.

SMP_EOS

|– booter1, booter2: bộ khởi động giai đoạn 1 và giai đoạn 2


|– type.h, include.h, mk script

|– kernel |– : tập tin nguồn hạt nhân

trình điều : tập tin trình điều khiển thiết bị

khiển : tập tin hệ thống tập tin

|– fs |– NGƯỜI DÙNG : lệnh và chương trình chế độ người dùng

Booter1 chứa mã nguồn của booter stage1. Khi chạy SMP_EOS trên máy ảo Realview-pbx-a9, QEMU

tải bộ khởi động stage1 về 0x10000 và thực thi nó trước. Trình khởi động Giai đoạn 1 tải trình khởi động giai đoạn 2 từ phần đầu của

SDC thành 2MB và thực thi nó tiếp theo. Booter2 chứa mã nguồn của booter stage2. Nó được thiết kế để khởi động kernel

image từ thư mục /boot trong phân vùng EXT2. Vì SDC chỉ có một phân vùng, bắt đầu từ khu vực 2048,

không cần bộ khởi động stage2 để tìm ra phân vùng nào sẽ khởi động khi nó khởi động. Vì vậy, chúng tôi chỉ cần đặt bộ khởi động stage2

trong các lĩnh vực 2 trở lên, giúp đơn giản hóa tác vụ tải của bộ khởi động giai đoạn 1. Bộ khởi động stage2 tải ảnh kernel vào

1MB và chuyển quyền điều khiển tới ảnh hạt nhân đã tải, khiến hạt nhân khởi động.

gõ.h : Các kiểu cấu trúc dữ liệu hạt nhân SMP_EOS, các kiểu hệ thống tệp EXT2, v.v.

bao gồm.h : hằng số và nguyên mẫu hàm.

mk : tập lệnh sh để biên dịch lại SMP_EOS và cài đặt hình ảnh có thể khởi động vào phân vùng SDC.

9.16.3 Tệp hạt nhân SMP_EOS

———————————— Kernel: Phần quản lý quy trình ————————————————

gõ.h : các kiểu cấu trúc dữ liệu hạt nhân, chẳng hạn như PROC, tài nguyên, v.v.

ts.s : reset_handler, tswitch, mặt nạ ngắt, spinlocks, cpu trong SMP, svc và

mã vào/ra của trình xử lý ngắt, v.v.

eoslib.c : các hàm thư viện hạt nhân; hoạt động memset, memcpy và chuỗi.

ngoại trừ.c : xử lý ngoại lệ

io.c : các hàm I/O hạt nhân

irq.c : cấu hình các điểm vào xử lý ngắt SCU, GIC và IRQ.

hàng đợi.c : enqueue, dequee, các hàm thao tác danh sách

mem.c : chức năng quản lý bộ nhớ sử dụng phân trang 2 cấp dynaminc

chờ đã.c : chức năng ksleep, kwakeup, kwait, kexit

bộ nạp.c : Trình tải hình ảnh thực thi ELF

ngã ba.c : chức năng kfork, fork, vfork

thực thi.c : hàm kexec

thread.c : chủ đề và hàm mutex

ống.c : tạo đường ống và chức năng đọc/ghi

mes.c : truyền tin nhắn: chức năng gửi/recv

tín hiệu.c : tín hiệu và xử lý tín hiệu

syscall.c : các hàm syscall đơn giản

svc.c : bảng định tuyến tòa nhà

tc : mục nhập chính, khởi tạo, các phần của bộ lập lịch quy trình
Machine Translated by Google

9.16 Hệ điều hành SMP mục đích chung 391

-------------- Trình điều khiển thiết bị --------------------------------

vid.c : Trình điều khiển màn hình LCD

ptimer.c : hẹn giờ và chức năng dịch vụ hẹn giờ

pv.c : hoạt động ngữ nghĩa

kbd.c : trình điều khiển bàn phím console

uart.c : Trình điều khiển cổng UART

-------------- Hệ thống tập tin ----------------------------------

fs : triển khai hệ thống tệp EXT2 đơn giản với bộ đệm I/O
————————————————————————————————————————————————————— —————————————————

NGƯỜI DÙNG : mã nguồn của chương trình cấp người dùng.

Tất cả các lệnh của người dùng đều là các lệnh thực thi ELF trong thư mục /bin
————————————————————————————————————————————————————— ——————————————————————————————————————————

SMP_EOS được triển khai chủ yếu bằng C, với ít hơn 2% mã hợp ngữ.

9.16.4 Quản lý quy trình trong SMP_EOS

Phần này mô tả việc quản lý tiến trình trong nhân SMP_EOS

9.16.4.1 Cấu trúc PROC


Trong SMP_EOS, mỗi tiến trình hoặc luồng được biểu diễn bằng cấu trúc PROC, bao gồm ba phần.

. các lĩnh vực quản lý quy trình,

. con trỏ tới cấu trúc tài nguyên trên mỗi quy trình,

. ngăn xếp chế độ kernel: khung trang 4KB được phân bổ động.

Cấu trúc PROC và tài nguyên giống hệt với cấu trúc trong hệ thống EOS của Chap. số 8.

9.16.4.2 Chạy con trỏ PROC


Để tham chiếu các tiến trình chạy trên các CPU khác nhau, nhân SMP_EOS xác định các con trỏ NCPU=4 PROC, mỗi con trỏ

trỏ tới PROC hiện đang thực thi trên CPU.

#define PROC *chạy[NCPU]

#define đang chạy run[get_cpuid()]

Trong mã C của kernel, nó sử dụng ký hiệu đang chạy để truy cập PROC hiện đang thực thi trên CPU. Trong hạt nhân

mã hợp ngữ, nó sử dụng đoạn mã sau để truy cập vào việc thực thi PROC trên CPU.

chạy .global // PROC *chạy[4] toàn cục

LDR r4, =chạy // r4=&chạy

MRC p15, 0, r5, c0, c0, 5 // đọc thanh ghi CPUID tới r5

VÀ r5, r5, #0x3 // mặt nạ trong CPUID

di chuyển r6, #4 // r6 = 4

mul r6, r5 // r6 = 4*cpuid

thêm r4, r4, r6 // r4 = &run[cpuid]

ldr r6, [r4, #0] // r6->chạy PROC

9.16.4.3 Spinlocks và Semaphores


Trong SMP, các tiến trình có thể chạy song song trên các CPU khác nhau. Tất cả các cấu trúc dữ liệu trong hạt nhân SMP phải được bảo vệ để ngăn chặn

tham nhũng từ điều kiện chủng tộc. Các công cụ điển hình được sử dụng để đạt được điều này là spinlocks và semaphores. Spinlocks thích hợp cho

CPU chờ các vùng quan trọng có thời lượng ngắn trong đó việc chuyển đổi tác vụ là không cần thiết hoặc không được phép, ví dụ như trong một

trình xử lý ngắt. Để truy cập vào một vùng quan trọng, trước tiên một quy trình phải có được spinlock liên kết với vùng quan trọng, như trong
Machine Translated by Google

392 9 Đa xử lý trong hệ thống nhúng

int quay = 0; // giá trị ban đầu = 0

slock(&spin); // thu được spinlock

// truy cập vùng quan trọng CR

khóa nắng(&spin); // giải phóng spinlock

Đối với các vùng quan trọng có thời lượng dài hơn, tốt hơn là để quá trình chờ bằng cách từ bỏ CPU. Trong những trường hợp như vậy,

mutex và semaphores phù hợp hơn. Trong SMP, mỗi đèn hiệu phải có trường spinlock để đảm bảo rằng tất cả các thao tác trên đèn hiệu chỉ có

thể được thực hiện trong vùng quan trọng của khóa xoay. Những sửa đổi tương tự cũng cần thiết cho các hoạt động mutex và mutex. Trong

SMP_EOS, chúng ta sẽ sử dụng cả spinlocks và semaphores để bảo vệ cấu trúc dữ liệu kernel.

9.16.4.4 Sử dụng chế độ ngủ/thức dậy trong SMP

Hạt nhân SMP_EOS sử dụng chế độ ngủ/thức dậy (đã sửa đổi) trong quản lý quy trình và đường ống. Là một cơ chế đồng bộ hóa, chế độ ngủ/thức

hoạt động tốt trong các hệ thống bộ xử lý đơn (UP) nhưng không phù hợp với SMP. Điều này là do sự kiện chỉ là một giá trị, không có vị

trí bộ nhớ liên quan để ghi lại sự xuất hiện của sự kiện. Khi một sự kiện xảy ra, Wakeup cố gắng đánh thức tất cả các tiến trình đang ngủ

trên sự kiện đó. Nếu không có tiến trình nào đang ngủ trong sự kiện này thì việc đánh thức sẽ không có hiệu lực. Điều này yêu cầu một tiến

trình phải ngủ trước khi một tiến trình khác hoặc một trình xử lý ngắt cố gắng đánh thức nó sau. Thứ tự ngủ trước và thức dậy sau này luôn

có thể đạt được trong UP nhưng không thể đạt được trong SMP. Trong hệ thống SMP, các tiến trình có thể chạy song song trên các CPU khác

nhau. Không thể đảm bảo thứ tự thực hiện quy trình. Do đó, ở dạng ban đầu, chế độ ngủ/thức dậy không thể được sử dụng trong SMP. Trong chế

độ ngủ/thức đã được sửa đổi, cả hai hoạt động đều phải đi qua một khu vực quan trọng chung được bảo vệ bởi khóa quay (Cox và cộng sự 2011;

Wang 2015). Trong khi giữ spinlock, nếu một tiến trình phải chuyển sang chế độ ngủ, nó sẽ hoàn thành thao tác ngủ và giải phóng spinlock

trong một thao tác không thể phân chia (nguyên tử).

9.16.5 Bảo vệ cấu trúc dữ liệu hạt nhân trong SMP

Trong kernel UP, mỗi lần chỉ có một tiến trình được thực thi. Do đó, cấu trúc dữ liệu trong hạt nhân UP không cần bất kỳ sự bảo vệ nào

chống lại việc thực thi quá trình đồng thời. Khi điều chỉnh kernel UP cho SMP, tất cả cấu trúc dữ liệu kernel phải được bảo vệ để đảm bảo

rằng các tiến trình chỉ có thể truy cập từng cái một. Các sửa đổi cần thiết có thể được phân thành hai loại.

(1). Loại đầu tiên bao gồm các cấu trúc dữ liệu hạt nhân được sử dụng để phân bổ và giải phóng tài nguyên. Bao gồm các

danh sách PROC miễn phí, danh sách khung trang miễn phí, cấu trúc đường ống, bộ đệm thông

báo, bitmap cho các nút và khối đĩa, các nút trong bộ nhớ, bảng tệp mở, bảng gắn kết, v.v.

Mỗi cấu trúc dữ liệu này có thể được bảo vệ bằng spinlock hoặc semaphore khóa. Sau đó sửa đổi các thuật toán phân bổ/giải phóng dưới

dạng các vùng quan trọng của biểu mẫu

phân bổ (tài nguyên)

KHÓA(resource_lock);

// cấp phát tài nguyên từ cấu trúc dữ liệu tài nguyên;

MỞ KHÓA(resource_lock);

truy xuất lại tài nguyên được phân bổ;

phân bổ (tài nguyên)

KHÓA(tài nguyên_lock)

// giải phóng tài nguyên vào cấu trúc dữ liệu tài nguyên;

MỞ KHÓA(resource_lock);

}
Machine Translated by Google

9.16 Hệ điều hành SMP mục đích chung 393

trong đó LOCK/UNLOCK biểu thị slock/sunlock trên spinlocks hoặc P/V trên các ngữ nghĩa khóa. Vì P/V yêu cầu các thao tác ngầm trên

spinlock của semaphore nên sử dụng trực tiếp spinlock sẽ hiệu quả hơn. Ví dụ: trong SMP_EOS, chúng tôi xác định spinlock freelock =

0 để bảo vệ danh sách PROC miễn phí và sửa đổi get_proc()/put_proc() như sau.

PROC *get_proc(danh sách PROC **)

slock(&freelock);

// PROC *p = lấy con trỏ PROC từ *list;

khóa nắng(&freelosck);

trả lại p;

void put_proc(danh sách PROC **, PROC *p)

slock(sfreelist);

// nhập p vào *list;

khóa nắng(sfreelist);

Trong khi giữ spinlock, các thao tác trên danh sách PROC miễn phí hoàn toàn giống với thao tác trong kernel UP. Tương tự, chúng

ta có thể sử dụng spinlock để bảo vệ các tài nguyên khác. Nói tóm lại, danh mục này bao gồm tất cả các cấu trúc dữ liệu hạt nhân mà

hành vi của một quy trình là truy cập cấu trúc dữ liệu mà không cần tạm dừng.

(2). Loại thứ hai bao gồm các trường hợp trong đó một tiến trình trước tiên phải có được một khóa để tìm kiếm mục cần thiết trong

cấu trúc dữ liệu. Nếu mục cần thiết đã tồn tại, quá trình không được tạo lại mục đó mà có thể phải đợi mục đó. Nếu vậy, nó phải nhả

khóa để cho phép chạy đồng thời. Tuy nhiên, điều này có thể dẫn đến tình trạng chủng tộc sau đây.

Sau khi nhả khóa nhưng trước khi tiến trình hoàn tất thao tác chờ, mục này có thể bị thay đổi bởi các tiến trình khác chạy trên các

CPU khác nhau, khiến nó chờ sai mục. Loại tình trạng chủng tộc này không thể xảy ra trong UP nhưng rất có thể xảy ra trong SMP. Có

hai cách có thể để ngăn chặn tình trạng chủng tộc như vậy.

(2).1. Đặt cờ đặt trước trên vật phẩm trước khi mở khóa. Đảm bảo rằng cờ đặt trước chỉ có thể được thao tác bên trong vùng quan

trọng của khóa. Ví dụ: trong hàm iget() của hệ thống tệp, trả về một minode bị khóa, mỗi minode có một trường refCount, biểu thị số

lượng quy trình vẫn đang sử dụng minode đó. Khi một tiến trình thực thi iget(), đầu tiên nó sẽ nhận được một khóa và sau đó tìm

kiếm một minode cần thiết. Nếu minode đã tồn tại, nó sẽ tăng refCount của minode lên 1 để dự trữ minode. Sau đó nó nhả khóa và cố

gắng khóa minode. Khi một tiến trình giải phóng một minode bằng iput(), nó phải thực thi trong cùng vùng quan trọng với iget(). Sau

khi giảm refCount của minode đi 1, nếu refCount khác 0, nghĩa là minode vẫn còn người dùng thì nó không giải phóng minode. Vì cả

iget() và iput() đều thực thi trong cùng một vùng quan trọng của khóa nên tình trạng chạy đua không thể xảy ra.

(2).2. Đảm bảo rằng quy trình hoàn tất thao tác chờ trên mục cần thiết trước khi mở khóa, điều này giúp loại bỏ khoảng cách về thời

gian. Khi sử dụng spinlock, đây là kỹ thuật tương tự như yêu cầu cả chế độ ngủ và thức để thực thi trong cùng một vùng quan trọng

của spinlock. Khi sử dụng khóa semaphore, chúng ta có thể sử dụng thao tác CP có điều kiện để kiểm tra xem semaphore đã bị khóa hay

chưa. Nếu semaphore đã bị khóa, chúng ta sử dụng thao tác PV(s1,s2), thao tác này sẽ chặn một tiến trình trên semaphore s1 một cách

nguyên tử trước khi giải phóng semaphore s2, để chờ semaphore bị khóa. Ví dụ, hãy xem xét lại các hàm iget()/iput(). Giả sử rằng

mlock là một semaphore khóa cho tất cả các minode trong bộ nhớ và mỗi minode có một semaphore khóa minode.sem=1. Chúng ta chỉ cần

sửa đổi iget()/iput() một chút như sau.

MINODE *iget(int dev, int ino) // trả về một minode bị khóa=(dev,ino)

P(mlock); // lấy khóa minodes

if (minode cần thiết đã tồn tại){

if (!CP(minode.sem) // nếu minode đã bị khóa

PV(minode.sem, mlock); // về mặt nguyên tử (P(s1),V(s2))

trả về minode; // trả về minode bị khóa

}
Machine Translated by Google

394 9 Đa xử lý trong hệ thống nhúng

// minode cần thiết không có trong bộ nhớ, vẫn giữ mlock

phân bổ một minode miễn phí;

P(minode.sem); // khóa minode

V(mlock); // nhả khóa minodes

tải inode từ đĩa vào minode;

trả về minode; // trả về minode bị khóa

void iput(MINDOE minode)

// người gọi đã giữ khóa minode.sem

P(mlock);

// thực hiện các thao tác minode như bình thường;

V(minode.sem);

V(mlock);

9.16.6 Ngăn chặn bế tắc trong SMP

Hạt nhân SMP dựa vào cơ chế khóa để bảo vệ cấu trúc dữ liệu. Nói chung, bất kỳ cơ chế khóa nào cũng có thể dẫn đến

bế tắc (Silberschatz và cộng sự 2009; Wang 2015). Có một số phương án xử lý deadlock, trong đó bao gồm deadlock

phòng ngừa, tránh bế tắc và phát hiện bế tắc với quá trình phục hồi. Trong một hệ điều hành thực, sơ đồ thực tế duy nhất

là phòng ngừa bế tắc. Trong sơ đồ này, hệ thống SMP có thể được thiết kế để ngăn chặn xảy ra bế tắc. bên trong

SMP_EOS, chúng tôi sử dụng các chiến lược sau để đảm bảo hệ thống không bị bế tắc.

(1). Khi sử dụng spinlocks hoặc semaphores, hãy đảm bảo rằng thứ tự khóa luôn là một chiều, để việc chờ đợi vòng tròn có thể
không bao giờ xảy ra.

(2). Nếu không thể tránh khỏi khóa chéo, hãy sử dụng khóa có điều kiện và lùi lại để ngăn chặn bất kỳ cơ hội khóa nào.

Hiệu quả của (1) là hiển nhiên. Chúng tôi minh họa kỹ thuật của (2) bằng một ví dụ.

9.16.6.1 Ngăn chặn bế tắc trong thuật toán song song


Trong nhân hệ điều hành, cấu trúc PROC là trọng tâm của quá trình tạo và kết thúc quá trình. Trong kernel UP, chỉ cần

duy trì tất cả các cấu trúc PROC miễn phí trong một danh sách miễn phí. Trong SMP, một danh sách trống có thể là một nút thắt cổ chai nghiêm trọng vì nó

làm giảm tính đồng thời. Để cải thiện khả năng tương tranh, chúng tôi có thể chia PROC miễn phí thành các danh sách miễn phí riêng biệt, mỗi danh sách được liên kết

với một CPU. Điều này cho phép các tiến trình thực thi trên các CPU khác nhau tiến hành song song trong quá trình phân nhánh và chờ đợi tiến trình con

chấm dứt. Nếu danh sách PROC miễn phí của CPU trở nên trống, chúng tôi sẽ cho phép nó lấy PROC từ các danh sách miễn phí khác một cách linh hoạt. Như vậy, trong một

PROC không có nhân SMP có thể được quản lý bằng thuật toán song song như sau.

Xác định: PROC *freelist[NCPU]; // danh sách PROC miễn phí, một danh sách cho mỗi CPU

int procspin[NCPU]={0}; // spinlocks cho danh sách PROC

PROC *get_proc() // phân bổ PROC miễn phí trong fork/vfork;

int cpuid = get_cpuid(); // mã CPU

trong khi(1){

(1). slock(procspin[cpuid]); // lấy spinlock của CPU

(2). if (danh sách tự do[cpuid]==0){ // nếu danh sách trống của CPU trống

(3). if (refill(cpuid)==0) // nạp lại danh sách miễn phí của CPU

{ sunlock(procspin[cpuid]);

Tiếp tục; // thử lại

}
Machine Translated by Google

9.16 Hệ điều hành SMP mục đích chung 395

(4). phân bổ PROC *p từ danh sách tự do ĐỊA PHƯƠNG[cpuid];

(5). sunlock(procspin[cpuid]); // giải phóng spinlock của CPU;

trả lại p;

nạp int(int cpuid) // nạp lại danh sách PROC miễn phí của CPU

int tôi, n = 0;

for (i=0; i<NCPU && i!=cpuid; i++){// thử danh sách miễn phí của CPU khác

if (!cslock(procspin[i])) // nếu khóa có điều kiện không thành công

Tiếp tục; if // thử danh sách PROC miễn phí tiếp

(danh sách tự do[i]==0){ theo // nếu danh sách miễn phí khác

khóa nắng(procspin[i]) trống // giải phóng

Tiếp tục; spinlock // thử danh sách miễn phí của CPU tiếp theo

xóa PROC khỏi danh sách tự do[i];// nhận một PROC miễn phí được chèn vào danh

sách tự do[cpuid]; // thêm vào danh sách của CPU

n++;

sunlock(procspin[i]); // giải phóng spinlock

trả lại n;

void put_proc(PROC *p) // giải phóng p vào danh sách PROC miễn phí của CPU

int cpuid = get_cpuid(); (1).

slock(procspin[cpuid]; (2).nhập p vào // lấy spinlock của CPU

freelist[cpuid];(3).sunlock(procspin[cpuid]);

// giải phóng spinlock của CPU

Trong get_proc(), trước tiên một tiến trình sẽ khóa danh sách PROC miễn phí của CPU mà nó đang thực thi. Nếu danh sách PROC miễn phí của

CPU không trống, nó sẽ nhận được PROC miễn phí, giải phóng khóa và quay lại. Tương tự, trong put_proc(), một tiến trình chỉ cần khóa danh

sách PROC miễn phí trên mỗi CPU. Trong điều kiện bình thường, các tiến trình chạy trên các CPU khác nhau có thể tiến hành song song do chúng

không cạnh tranh để giành cùng một danh sách PROC miễn phí. Sự tranh chấp duy nhất có thể xảy ra là khi danh sách PROC miễn phí của CPU trở nên trống.

Trong trường hợp đó, tiến trình thực thi thao tác Refill(), thao tác này cố gắng lấy PROC miễn phí từ mọi CPU khác vào danh sách PROC miễn phí

của CPU hiện tại. Vì tiến trình đã giữ khóa spin của CPU nên việc cố gắng lấy khóa spin của CPU khác có thể dẫn đến bế tắc. Vì vậy, nó sử dụng

khóa có điều kiện, cslock(), thay vào đó. Nếu khóa có điều kiện không thành công, quá trình sẽ lùi lại để ngăn chặn bất kỳ nguy cơ bế tắc nào.

Nếu sau khi nạp lại danh sách PROC miễn phí của CPU vẫn trống, nó sẽ giải phóng khóa quay và thử lại thuật toán. Điều này ngăn chặn tình trạng

tự bế tắc trên cùng một CPU.

9.16.7 Thuật toán điều chỉnh UP cho SMP

Ngoài việc sử dụng các khóa để bảo vệ cấu trúc dữ liệu kernel, nhiều thuật toán sử dụng trong kernel UP phải được sửa đổi cho phù hợp với SMP.

Chúng tôi minh họa điều này bằng các ví dụ.

9.16.7.1 Thuật toán lập lịch trình quy trình Adapt UP cho SMP Hạt nhân UP thường

chỉ có một hàng đợi lập lịch quy trình duy nhất. Chúng tôi có thể điều chỉnh thuật toán lập kế hoạch quy trình UP cho SMP như sau. Xác định

một spinlock, sẵn sàng, để bảo vệ hàng đợi lập lịch. Trong quá trình chuyển đổi tác vụ, trước tiên, một quy trình phải có được khóa sẵn sàng,

khóa này sẽ được giải phóng bởi quy trình đang chạy tiếp theo khi nó tiếp tục.
Machine Translated by Google

396 9 Đa xử lý trong hệ thống nhúng

9.16.7.2 Điều chỉnh thuật toán đường ống UP cho SMP Trong EOS

của Chap. 8, các đường ống được triển khai bằng thuật toán UP, sử dụng chế độ ngủ/thức thông thường để đồng bộ hóa. Chúng tôi có thể điều chỉnh

thuật toán ống UP cho SMP bằng cách thêm khóa xoay vào mỗi ống và yêu cầu đầu đọc và ghi ống thực thi trong cùng một vùng quan trọng của khóa xoay.

Trong khi giữ spinlock, nếu một tiến trình phải ngủ để lấy dữ liệu hoặc chỗ trống trong đường ống, thì nó phải hoàn thành thao tác ngủ trước khi

giải phóng spinlock. Những điều này có thể được thực hiện bằng cách thay thế thao tác ngủ (sự kiện) thông thường bằng thao tác ngủ (sự kiện, khóa

quay) đã sửa đổi.

9.16.7.3 Thuật toán quản lý bộ đệm I/O thích ứng UP cho SMP Hệ thống tệp EOS sử
dụng bộ đệm I/O cho các thiết bị khối. Thuật toán quản lý bộ đệm I/O sử dụng các ngữ nghĩa để đồng bộ hóa quy trình. Thuật toán chỉ
hoạt động trong UP vì nó giả định chỉ có một tiến trình chạy tại một thời điểm. Chúng tôi có thể điều chỉnh các thuật toán cho SMP
bằng cách thêm một spinlock và đảm bảo rằng cả getblk() và brelse() đều được thực thi trong cùng một vùng quan trọng. Trong
getblk(), trong khi giữ spinlock, nếu một quá trình tìm thấy bộ đệm cần thiết đã tồn tại nhưng đang bận, nó sẽ tăng số lượng
người dùng của bộ đệm lên 1 để dự trữ bộ đệm. Sau đó, nó giải phóng spinlock và đợi bộ đệm bằng P trên semaphore khóa của bộ đệm.
Khi một tiến trình (hoặc trình xử lý ngắt) giải phóng bộ đệm bằng brelse(), nó phải thực thi trong cùng vùng quan trọng của
getblk(). Sau khi giảm số lượng người dùng của bộ đệm xuống 1, nếu số lượng người dùng khác 0, nghĩa là bộ đệm vẫn còn người
dùng, nó không giải phóng bộ đệm mà V semaphore khóa của bộ đệm để bỏ chặn một tiến trình. Tương tự, chúng tôi có thể điều chỉnh
các thuật toán UP khác cho SMP.

9.16.8 Trình điều khiển thiết bị và Trình xử lý ngắt trong SMP

Trong trình điều khiển thiết bị điều khiển ngắt, quy trình và trình xử lý ngắt thường chia sẻ bộ đệm dữ liệu và các biến điều khiển, tạo thành một

vùng quan trọng giữa quy trình và trình xử lý ngắt. Trong hạt nhân UP, khi một tiến trình thực thi trình điều khiển thiết bị, nó có thể che giấu

các ngắt để ngăn chặn sự can thiệp từ trình xử lý ngắt. Trong SMP, việc che dấu các ngắt không còn đủ nữa.

Điều này là do trong khi một tiến trình thực thi trình điều khiển thiết bị, một CPU khác có thể thực thi trình xử lý ngắt thiết bị cùng lúc. Để tuần

tự hóa việc thực thi các quy trình và trình xử lý ngắt, trình điều khiển thiết bị trong SMP cũng phải được sửa đổi.

Vì trình xử lý ngắt không thể ngủ hoặc bị chặn nên chúng phải sử dụng cơ chế spinlock hoặc tương đương. Trong phần tiếp theo, chúng tôi minh

họa các nguyên tắc thiết kế trình điều khiển SMP bằng các ví dụ cụ thể trong nhân SMP_EOS.

(1). Màn hình LCD là một thiết bị được ánh xạ bộ nhớ, không sử dụng các ngắt. Để đảm bảo các tiến trình thực thi kputc() lần lượt, chỉ cần bảo vệ

trình điều khiển bằng khóa xoay là đủ.

(2). Trong trình điều khiển hẹn giờ, quy trình và trình xử lý ngắt chia sẻ dịch vụ hẹn giờ và hàng đợi lập lịch quy trình nhưng quy trình không bao

giờ chờ ngắt hẹn giờ. Trong trường hợp này, chỉ cần bảo vệ từng cấu trúc dữ liệu phụ thuộc vào bộ định thời bằng một khóa xoay là đủ.

(3). Trình điều khiển thiết bị Char cũng sử dụng bộ đệm I/O để có hiệu quả tốt hơn. Trong EOS, tất cả trình điều khiển thiết bị char đều sử dụng

ngữ nghĩa để đồng bộ hóa. Để điều chỉnh các bộ dẫn xuất cho phù hợp với SMP, mỗi trình điều khiển sử dụng một spinlock để tuần tự hóa việc thực thi

quy trình và trình xử lý ngắt. Trong khi giữ spinlock, nếu một tiến trình phải đợi dữ liệu hoặc chỗ trống trong bộ đệm I/O, thì nó sẽ sử dụng PU(s,

spin), chờ một semaphore s và giải phóng spinlock trong một thao tác nguyên tử duy nhất.

(4). Trong trình điều khiển SDC, xử lý và xử lý ngắt các cấu trúc dữ liệu được chia sẻ. Đối với các trình điều khiển như vậy, chỉ cần sử dụng một

spinlock để tuần tự hóa việc thực thi quy trình và trình xử lý ngắt.

(5). Trình xử lý quá trình và ngắt trong trình điều khiển thiết bị SMP phải tuân theo thứ tự thời gian sau.

Quá trình Trình xử lý ngắt


-------------------------------------------------- --------

(Một). vô hiệu hóa ngắt

(b). có được spinlock | | (Một). có được spinlock (c). bắt

đầu thao tác I/O | (b). xử lý ngắt | (c). bắt đầu I/O tiếp theo, nếu cần | (d). phát hành

spinlock | (e). phát hành EOI

(d). phát hành spinlock

(e). kích hoạt ngắt


-------------------------------------------------- --------
Machine Translated by Google

9.16 Hệ điều hành SMP mục đích chung 397

Trong cả hai trường hợp, thứ tự của (d) và (e) phải được tuân thủ nghiêm ngặt để ngăn quá trình tự khóa trên cùng một spinlock.

9.17 Hệ thống trình diễn SMP_EOS

SMP_EOS là hệ điều hành SMP có mục đích chung được thiết kế cho kiến trúc ARM MPcode. Phần này mô tả các hoạt động và khả năng của hệ thống

SMP_EOS.

9.17.1 Trình tự khởi động của SMP_EOS

Trình tự khởi động của hệ thống SMP_EOS bao gồm các bước sau.

(1). Khởi động kernel SMP_EOS: QEMU tải bộ khởi động giai đoạn 1 lên 16KB và thực thi nó trước. Bộ khởi động giai đoạn 1 tải bộ khởi động giai

đoạn 2 từ SDC lên 2MB và thực thi nó tiếp theo. Bộ khởi động giai đoạn 2 tải hạt nhân SMP_EOS lên 1MB và chuyển quyền điều khiển sang hình ảnh

hạt nhân đã tải. Trong phần sau đây, các Bước (2) đến (7) chỉ được thực thi bởi bộ xử lý khởi động CPU0.

(2). Reset_handler: CPU0 thực thi reset_handler để đặt con trỏ ngăn xếp SVC và chế độ đặc quyền, sao chép vectơ sang địa chỉ 0, tạo bảng trang

một cấp ban đầu, bật MMU, gọi main() trong C.

(3). Main(): Kích hoạt SCU, định cấu hình GIC cho các ngắt IRQ, khởi tạo trình điều khiển thiết bị, gọi kernel_init() để khởi tạo kernel.

(4). Kernel_init(): khởi tạo cấu trúc dữ liệu kernel, đặt con trỏ chạy của CPU tới PROC ban đầu của chúng với các pid đặc biệt 1000 đến 1003,

chạy quy trình ban đầu với pid=1000. Xây dựng bảng trang 2 cấp và danh sách trang miễn phí, chuyển sang bảng trang 2 cấp để sử dụng phân trang

động.

(5). Fs_init(): Khởi tạo hệ thống tập tin và mount hệ thống tập tin gốc.

(6). Tạo quy trình INIT: Tạo quy trình INIT P1 và nhập nó vào hàng đợi sẵn sàng.

(7). Kích hoạt các CPU khác: Gửi SGI để kích hoạt các CPU phụ (CPU1 đến CPU3), sau đó gọi run_task() để chạy tiến trình INIT P1.

——————————————————————————————— CPU thứ cấp trong SMP ————————————————— ——————————————————————

(số 8). Mỗi CPU phụ: bắt đầu thực thi reset_hanlder ở mức 1MB, đặt ngăn xếp SVC và chế độ đặc quyền, sử dụng bảng trang một cấp ở mức 16KB để

bật MMU, sau đó gọi APstart().

(9). APstart(): mỗi CPU phụ: chuyển sang bảng trang 2 cấp với dung lượng 32KB, định cấu hình và khởi động bộ đếm thời gian cục bộ, sau đó gọi

run_task(), cố gắng chạy các tác vụ từ cùng một ReadyQueue.


———————————————————————————————————————————————————

(10). Quá trình INIT P1 (chạy trên CPU0): phân nhánh các quy trình đăng nhập trên bảng điều khiển và thiết bị đầu cuối nối tiếp để cho phép người

dùng đăng nhập. Sau đó P1 đợi bất kỳ đứa trẻ nào kết thúc. Khi quá trình đăng nhập khởi động, hệ thống đã sẵn sàng để sử dụng.

(11). Đăng nhập người dùng: Sau khi người dùng đăng nhập, quy trình đăng nhập sẽ trở thành quy trình người dùng, thực thi trình thông dịch lệnh sh.

(12). Tiến trình Sh: Người dùng nhập lệnh để sh thực thi. Khi quá trình sh của người dùng kết thúc (bằng cách đăng xuất của người dùng hoặc control-

D), nó sẽ đánh thức quy trình INIT P1, tiến trình này sẽ phân nhánh một quy trình đăng nhập khác trên thiết bị đầu cuối.

9.17.2 Khả năng của SMP_EOS

Hạt nhân SMP_EOS bao gồm quản lý quy trình, quản lý bộ nhớ, trình điều khiển thiết bị và hệ thống tệp hoàn chỉnh. Nó hỗ trợ việc tạo và chấm

dứt quá trình động. Nó cho phép quá trình thay đổi hình ảnh để thực thi các tập tin khác nhau. Mỗi quy trình chạy trong không gian địa chỉ ảo

riêng tư ở chế độ Người dùng. Quản lý bộ nhớ bằng cách phân trang động hai cấp độ. Lập kế hoạch quy trình theo mức độ ưu tiên của cả quy

trình theo thời gian và quy trình động. Là một hệ thống SMP, hạt nhân SMP_EOS có khả năng hỗ trợ lập lịch trình quy trình ưu tiên. Để đơn

giản, việc lập lịch xử lý ưu tiên bị vô hiệu hóa để tránh chuyển đổi tác vụ quá mức trong chế độ kernel. Trong hệ thống trình diễn, quá trình

chuyển đổi chỉ xảy ra khi các tiến trình thoát khỏi kernel để trở về chế độ người dùng. Nó hỗ trợ một hệ thống tệp EXT2 hoàn chỉnh hoàn toàn

tương thích với Linux. Nó sử dụng khối I/O của thiết bị


Machine Translated by Google

398 9 Đa xử lý trong hệ thống nhúng

đệm giữa hệ thống tệp và trình điều khiển SDC để cải thiện hiệu quả và hiệu suất. Nó hỗ trợ nhiều người dùng đăng nhập từ bảng điều khiển và

thiết bị đầu cuối nối tiếp. Trình thông dịch lệnh sh hỗ trợ thực thi các lệnh đơn lẻ với chuyển hướng I/O, cũng như nhiều lệnh được kết

nối bằng đường ống. Nó cung cấp dịch vụ định thời gian cho các tiến trình và hỗ trợ liên lạc giữa các tiến trình bằng tín hiệu, đường ống và

truyền thông điệp. Nó thống nhất các ngoại lệ với quá trình xử lý tín hiệu, cho phép người dùng cài đặt bộ thu tín hiệu để xử lý các ngoại

lệ trong chế độ Người dùng.

9.17.3 Trình diễn SMP_EOS

Hình 9.11 cho thấy kết quả đầu ra mẫu khi chạy hệ thống SMP_EOS. Nó sử dụng UART0 để hiển thị thông tin trong quá trình khởi động và cũng

như một thiết bị đầu cuối nối tiếp để người dùng đăng nhập sau khi hệ thống khởi động. Sau khi đăng nhập, người đọc có thể nhập lệnh để

kiểm tra hệ thống. Tất cả các lệnh của người dùng đều nằm trong thư mục /bin. Như hình minh họa, các tiến trình có thể chạy trên các CPU khác nhau.

Hình 9.11 Trình diễn hệ thống SMP_EOS


Machine Translated by Google

9.18 Tóm tắt 399

9.18 Tóm tắt

Chương này đề cập đến đa xử lý trong các hệ thống nhúng. Nó chỉ ra các yêu cầu của hệ thống Đa xử lý đối xứng (SMP) và so sánh các cách

tiếp cận SMP của Intel với ARM. Nó liệt kê các bộ xử lý ARM MPcore và mô tả các thành phần cũng như chức năng của bộ xử lý ARM MPcore hỗ

trợ SMP. Tất cả các hệ thống dựa trên ARM MPcore đều phụ thuộc vào Bộ điều khiển ngắt chung (GIC) để định tuyến các ngắt và liên lạc giữa

các bộ xử lý. Nó cho thấy cách cấu hình GIC để định tuyến các ngắt và trình bày cách lập trình GIC bằng các ví dụ. Nó cho thấy cách khởi

động ARM MPcores và chỉ ra nhu cầu đồng bộ hóa trong môi trường SMP. Nó cho thấy cách sử dụng các hướng dẫn kiểm tra và thiết lập cổ điển

hoặc các hướng dẫn tương đương để triển khai cập nhật nguyên tử và các vùng quan trọng, đồng thời chỉ ra những thiếu sót của chúng. Sau

đó, nó giải thích các tính năng mới của ARM Mpcores hỗ trợ SMP. Chúng bao gồm các hướng dẫn LDRES/STRES và các rào cản bộ nhớ. Nó cho thấy

cách sử dụng các tính năng mới của ARM Mpcores để triển khai spinlocks, mutexes và semaphores để đồng bộ hóa quy trình trong SMP. Nó xác

định các khóa có điều kiện để ngăn chặn bế tắc trong hạt nhân SMP. Nó cũng bao gồm các tính năng bổ sung của ARM MMU cho SMP. Nó trình bày

một phương pháp chung để điều chỉnh nhân hệ điều hành đơn bộ xử lý thành SMP. Sau đó, nó áp dụng các nguyên tắc để phát triển hệ điều hành

SMP hoàn chỉnh cho các hệ thống nhúng.

Danh sách các chương trình mẫu

C9.1. Chương trình ví dụ lập trình GIC C9.2. Ví dụ

khởi động ARM SMP 1 C9.3. Ví dụ khởi động

ARM SMP 2 C9.4. Khởi động SMP với Spinlock

C9.5. Khởi động SMP với Mutex Lock C9.6.

Bộ tính giờ cục bộ trong SMP

C9.7. Bản đồ không gian VA thống nhất

C9.8. Bản đồ không gian VA không đồng nhất C9.9.

Hệ thống tính toán song song C9.10. Quản

lý quy trình trong SMP C9.11.Trình diễn


SMP_EOS

Các vấn đề

1. Trong chương trình ví dụ C9.1, sửa đổi mã config_gic() để định tuyến các ngắt tới các CPU khác nhau nhằm quan sát hiệu ứng.

2. Thay vì bộ định thời cục bộ của CPU, hãy sử dụng bộ định thời toàn cục để cung cấp một nguồn định thời duy nhất cho tất cả

CPU. 3. Thay vì một hàng đợi lập lịch duy nhất, hãy triển khai nhiều hàng đợi lập lịch, ví dụ: một hàng đợi lập lịch cho mỗi CPU, để tăng

tốc độ lập lịch tác vụ trong hệ thống SMP.

4. SMP_EOS sử dụng sơ đồ ánh xạ bộ nhớ ảo KML, trong đó không gian VA của kernel được ánh xạ tới các địa chỉ ảo thấp. Triển khai lại hệ

thống bằng cách sử dụng sơ đồ ánh xạ bộ nhớ ảo KMH, sơ đồ này ánh xạ không gian VA của hạt nhân tới 2GB.

Người giới thiệu

ARM 926EJ-S : Tấm nền ứng dụng đa năng ARM cho ARM926EJ-S Hướng dẫn sử dụng, Trung tâm thông tin ARM, 2010
ARM11: Hướng dẫn tham khảo kỹ thuật bộ xử lý ARM11 MPcore, r2p0, Trung tâm thông tin ARM, 2008
ARM Cortex-A9 MPCore: Bản sửa đổi sổ tay tham khảo kỹ thuật: r4p1, Trung tâm thông tin ARM, 2012
ARM GIC: Hướng dẫn tham khảo kỹ thuật Bộ điều khiển ngắt chung ARM (PL390), Trung tâm thông tin ARM, 2013
ARM Linux SMP: Khởi động ARM Linux SMP trên MPCore http://www.linux-arm.org/LinuxBootLoader/SMPBoot, Bộ hẹn
giờ ARM 2010: Hướng dẫn tham khảo kỹ thuật Mô-đun hẹn giờ kép ARM (SP804), Trung tâm thông tin Arm,
2004 Cox, R., Kaashoek, F., Morris, R. "xv6 một hệ điều hành giảng dạy đơn giản, giống Unix, xv6-book
@pdos.csail.mit.edu, tháng 9 năm 2011 Intel:
Đặc tả đa bộ xử lý, v1.4, Intel, 1997 Stallings, W. "Hệ điều hành: Nội bộ và Nguyên tắc
thiết kế ( Ấn bản thứ 7)", Prentice Hall, 2011 Silberschatz, A. , PA Galvin, PA, Gagne, G, "Khái niệm hệ điều
hành, Phiên bản thứ 8", John Wiley & Sons, Inc. 2009 Wang, KC, "Thiết kế và triển khai Hệ điều hành MTX, Nhà xuất bản Quốc tế Springer AG, 2015
Machine Translated by Google

Hệ điều hành nhúng thời gian thực


10

10.1 Khái niệm về RTOS

Hệ điều hành thời gian thực (RTOS) (Dietrich và Walker 2015) là hệ điều hành dành cho các ứng dụng thời gian thực.

Các ứng dụng thời gian thực thường có yêu cầu rất nghiêm ngặt về thời gian. Đầu tiên, RTOS phải có khả năng phản hồi nhanh chóng với các sự kiện bên

ngoài, ví dụ như trong một khoảng thời gian rất ngắn, được gọi là độ trễ gián đoạn. Thứ hai, nó phải hoàn thành mọi dịch vụ được yêu cầu trong một

thời hạn quy định, được gọi là thời hạn nhiệm vụ. Nếu một hệ thống thời gian thực luôn có thể đáp ứng được những yêu cầu quan trọng về thời gian này

thì nó được gọi là hệ thống thời gian thực cứng. Nếu nó chỉ có thể đáp ứng các yêu cầu trong hầu hết thời gian chứ không phải luôn luôn thì nó được

gọi là hệ thống thời gian thực mềm. Để đáp ứng các yêu cầu nghiêm ngặt về thời gian, hệ điều hành thời gian thực thường được thiết kế với các khả

năng sau.

. Độ trễ ngắt tối thiểu: Độ trễ ngắt là khoảng thời gian kể từ thời điểm nhận được ngắt cho đến thời điểm CPU bắt đầu thực thi trình xử lý ngắt. Để

giảm thiểu độ trễ ngắt, hạt nhân RTOS không được che giấu các ngắt trong thời gian dài. Điều này thường có nghĩa là hệ thống phải hỗ trợ các ngắt

lồng nhau để đảm bảo việc xử lý các ngắt có mức ưu tiên thấp không làm trì hoãn việc xử lý các ngắt có mức ưu tiên cao.

. Các vùng quan trọng ngắn: Tất cả các nhân hệ điều hành đều dựa vào các vùng quan trọng để bảo vệ các đối tượng dữ liệu dùng chung cũng như để đồng

bộ hóa quy trình. Trong nhân RTOS, tất cả các vùng quan trọng phải càng ngắn càng tốt.

. Lập kế hoạch nhiệm vụ ưu tiên: Ưu tiên có nghĩa là một nhiệm vụ có mức độ ưu tiên cao hơn có thể được ưu tiên trước một nhiệm vụ có mức độ ưu

tiên thấp hơn bất cứ lúc nào. Để đáp ứng thời hạn nhiệm vụ, hạt nhân RTOS phải hỗ trợ lập lịch tác vụ ưu tiên. Thời gian chuyển đổi nhiệm vụ cũng
phải ngắn.

. Thuật toán lập lịch tác vụ nâng cao: Lập lịch ưu tiên là điều kiện cần nhưng chưa đủ cho các hệ thống thời gian thực. Nếu không có lịch trình ưu

tiên, các nhiệm vụ có mức độ ưu tiên cao có thể bị trì hoãn bởi các nhiệm vụ có mức độ ưu tiên thấp trong một khoảng thời gian khác nhau, khiến cho

nhiệm vụ không thể đáp ứng được thời hạn. Tuy nhiên, ngay cả với việc lập kế hoạch trước, không có gì đảm bảo rằng các nhiệm vụ sẽ có thể đáp ứng

được thời hạn. Hệ thống phải sử dụng thuật toán lập lịch phù hợp để giúp đạt được mục tiêu này.

10.2 Lập lịch tác vụ trong RTOS

Trong hệ điều hành có mục đích chung, chính sách lập lịch tác vụ thường được thiết kế để đạt được hiệu suất hệ thống cân bằng giữa các mục tiêu xung

đột khác nhau, chẳng hạn như thông lượng, sử dụng tài nguyên và phản hồi nhanh cho người dùng tương tác, v.v. Ngược lại, trong RTOS, mục tiêu duy

nhất là để đảm bảo phản ứng nhanh và đảm bảo thời hạn nhiệm vụ. Chính sách lập kế hoạch phải ưu tiên những nhiệm vụ có ràng buộc về thời gian cấp

bách nhất. Những ràng buộc như vậy có thể được chuyển thành các ưu tiên của nhiệm vụ. Do đó, thuật toán lập lịch của RTOS phải được ưu tiên dựa

trên mức độ ưu tiên của nhiệm vụ. Có hai loại thuật toán lập lịch tác vụ chính cho RTOS, được gọi là Lập kế hoạch theo tỷ lệ đơn điệu (RMS) và lập

lịch thời hạn sớm nhất trước (EDF) và các dạng biến thể của chúng.

© Springer International Publishing AG 2017 401


KC Wang, Hệ điều hành nhúng và thời gian thực, DOI
10.1007/978-3-319-51517-5_10
Machine Translated by Google

402 10 hệ điều hành nhúng thời gian thực

10.2.1 Lập kế hoạch đơn điệu tốc độ (RMS)

Lập kế hoạch đơn điệu tốc độ (RMS) (Liu và Layland 1973) là một thuật toán lập lịch dựa trên mức độ ưu tiên tĩnh cho thời gian thực

hệ thống. Mô hình RMS giả định các điều kiện sau.

(1). Nhiệm vụ định kỳ: nhiệm vụ mang tính định kỳ với thời hạn hoàn toàn bằng với thời gian.

(2). Ưu tiên tĩnh: nhiệm vụ có thời gian ngắn hơn được giao mức độ ưu tiên cao hơn.

(3). Ưu tiên: hệ thống luôn chạy nhiệm vụ có mức độ ưu tiên cao nhất, nhiệm vụ này sẽ ưu tiên ngay lập tức các nhiệm vụ khác có mức độ ưu tiên thấp hơn.

(4). Nhiệm vụ không chia sẻ tài nguyên có thể khiến chúng bị chặn hoặc chờ đợi.

(5). Chuyển đổi ngữ cảnh và thời gian thực hiện tác vụ khác, ví dụ như thời gian phát hành và khởi động, đều bằng không.

RMS phân tích mô hình hệ thống như vậy và rút ra điều kiện theo đó các nhiệm vụ có thể được lên lịch để đáp ứng

thời hạn. Liu và Layland đã chứng minh rằng đối với một tập n nhiệm vụ định kỳ với các khoảng thời gian duy nhất, một lịch trình khả thi sẽ luôn luôn

đáp ứng thời hạn nhiệm vụ tồn tại nếu hệ số sử dụng CPU U ở dưới mức giới hạn cụ thể. Điều kiện lập lịch của RMS là

Bạn ¼ TỔNG ð Þ Ci=Ti \¼ n 2ð ð Þ 1=n 1 QUẦN QUÈ

tôi¼1

trong đó Ci là thời gian tính toán của taski, Ti là khoảng thời gian của taski và n là số lượng nhiệm vụ được lên lịch. Vì

ví dụ: U <= 0,8284 cho hai nhiệm vụ. Khi số lượng nhiệm vụ n lớn thì giá trị của U có xu hướng tiến tới giá trị giới hạn là

ln 2ð Þ¼ 0:693247:...

Điều này ngụ ý rằng RMS có thể đáp ứng tất cả các thời hạn của nhiệm vụ nếu mức sử dụng CPU dưới 69,32%. Hơn nữa, RMS là

tối ưu cho các hệ thống đơn xử lý ưu tiên theo nghĩa là, nếu bất kỳ thuật toán lập lịch ưu tiên tĩnh nào có thể đáp ứng tất cả các

thời hạn, thì thuật toán RMS cũng vậy. Điều đáng chú ý là việc kiểm tra khả năng lập kế hoạch của RMS chỉ là đủ chứ không phải

Điều kiện cần thiết. Ví dụ: đối với tác vụ được đặt task1 = (C1=2, T1=4), task2 = (C2=4, T2=8), giá trị của U là 1,0, trong đó

lớn hơn giới hạn RMS 0,828, nhưng các tác vụ có thể lập lịch với mức sử dụng CPU 100%, như thời gian sau

sơ đồ cho thấy.

thời gian: 0 1 2 3 4 5678


-------------------------

|nhiệm vụ1| |nhiệm vụ1| |

| |nhiệm vụ2| |nhiệm vụ2|

-------------------------

Nói chung, nếu các khoảng thời gian của nhiệm vụ là hài hòa, nghĩa là đối với mỗi nhiệm vụ, khoảng thời gian của nó là bội số chính xác của mọi nhiệm vụ mà

có khoảng thời gian ngắn hơn, các nhiệm vụ có thể lập lịch trình với hệ số sử dụng cao hơn giới hạn RMS.

10.2.2 Lập kế hoạch sớm nhất-thời hạn đầu tiên (EDF)

Trong mô hình Thời hạn sớm nhất trước tiên (EDF) (Leung và cộng sự 1982), các nhiệm vụ có thể là định kỳ hoặc không định kỳ. Mỗi nhiệm vụ có một

thời hạn xác định. Các nhiệm vụ được duy trì trong hàng ưu tiên được sắp xếp theo thời hạn gần nhất của chúng, tức là các nhiệm vụ có thời hạn ngắn hơn

có mức độ ưu tiên cao hơn. Thuật toán lập lịch EDF luôn thực hiện nhiệm vụ có thời hạn gần nhất. Giống như RMS, EDF cũng

tối ưu cho các hệ thống đơn xử lý ưu tiên. Khi lập lịch các nhiệm vụ định kỳ với thời hạn bằng các khoảng thời gian, EDF có một

Giới hạn sử dụng CPU là 100%. Điều kiện lập lịch của EDF là

Bạn ¼ TỔNG ð Þ Ci=Ti \¼1


tôi¼1

trong đó Ci và Ti là thời gian tính toán trong trường hợp xấu nhất và khoảng thời gian giữa các nhiệm vụ. So với RMS, EDF có thể

đảm bảo thời hạn thực hiện nhiệm vụ với hệ số sử dụng CPU cao hơn nhưng nó cũng có hai nhược điểm. Đầu tiên, EDF khó thực hiện hơn

thực hiện vì các ưu tiên của nhiệm vụ không còn tĩnh nữa mà là động. Nó phải theo dõi thời hạn nhiệm vụ và cập nhật nhiệm vụ

xếp hàng bất cứ khi nào xảy ra sự kiện lên lịch lại. Thứ hai, khi hệ thống bị quá tải (hệ số sử dụng CPU > 1), các tác vụ
Machine Translated by Google

10.2 Lập lịch tác vụ trong RTOS 403

sẽ trễ thời hạn trong RMS thường là những thời hạn có thời gian dài hơn (mức độ ưu tiên thấp). Trong mô hình EDF, những nhiệm vụ như vậy phần lớn

không thể đoán trước được, có nghĩa là bất kỳ nhiệm vụ nào cũng có thể bị trễ thời hạn. Tuy nhiên, cũng có những phân tích so sánh giữa EDF và EDF.

RMS có xu hướng bác bỏ những tuyên bố như vậy (Buttazzo 2005). Mặc dù vậy, hầu hết các RTOS thực tế đều thích RMS hơn EDF, chủ yếu là do các ưu tiên

nhiệm vụ tĩnh và tính chất xác định của RMS.

10.2.3 Lập kế hoạch đơn thời hạn (DMS)

Lập lịch trình đơn thời hạn (DMS) (Audsley 1990) là một dạng RMS tổng quát. Trong RMS, thời hạn và thời gian thực hiện nhiệm vụ được coi là hoàn toàn

bằng nhau. Mô hình DMS nới lỏng điều kiện này để cho phép thời hạn thực hiện nhiệm vụ nhỏ hơn hoặc bằng thời gian thực hiện nhiệm vụ, tức là

thời gian tính toán nhiệm vụ \¼ thời hạn \¼ giai đoạn

Trong DMS, các nhiệm vụ có thời hạn ngắn hơn sẽ được giao mức độ ưu tiên cao hơn. Vì lý do này, DMS còn được gọi là Lập kế hoạch nghịch đảo thời

hạn (IDS). Lập lịch thời gian chạy cũng giống như trong RMS, tức là theo mức độ ưu tiên của nhiệm vụ được ưu tiên. Khi thời hạn và thời gian thực

hiện nhiệm vụ bằng nhau, DMS sẽ giảm xuống RMS như một trường hợp đặc biệt. Trong (Audsley và cộng sự 1993), điều kiện RMS được nới lỏng để cho phép

thời hạn thực hiện nhiệm vụ ngắn hơn thời gian thực hiện nhiệm vụ. Nó cũng mở rộng mô hình DMS để bao gồm các nhiệm vụ không định kỳ và các bài kiểm

tra khả năng lập kế hoạch xuất phát cho những trường hợp như vậy.

Bất chấp các mô hình hệ thống thời gian thực và kết quả phân tích này, cả RMS và EDF chỉ có thể được sử dụng làm hướng dẫn chung trong thiết kế

RTOS thực. Vấn đề trở nên nổi bật hơn khi triển khai các hệ thống RTOS thực, chủ yếu là do những lý do sau. Một lỗ hổng cơ bản trong cả hai mô hình

RMS và EDF là các tác vụ không thể chia sẻ tài nguyên, điều này có thể khiến chúng bị chặn hoặc chờ và việc chuyển đổi tác vụ không phát sinh chi phí

nào. Những điều kiện này là không thực tế vì trong hệ thống thực việc chia sẻ tài nguyên giữa các tác vụ là điều không thể tránh khỏi và thời gian

chuyển đổi tác vụ không bao giờ bằng 0.

Mặc dù có một số nỗ lực nhằm mở rộng cả mô hình RMS và EDF để cho phép chia sẻ tài nguyên bằng cách đưa thời gian chặn nhiệm vụ vào phân tích khả năng

lập lịch, nhưng kết quả thường liên quan đến nhiều biến số khó định lượng, vì vậy chúng chỉ giả định các giá trị trong trường hợp xấu nhất. Ngoài ra,

việc cho phép chia sẻ tài nguyên cũng dẫn đến các vấn đề khác, chẳng hạn như đảo ngược mức độ ưu tiên và thời hạn, phải được xử lý đúng cách trong

RTOS thực.

10.3 Đảo ngược ưu tiên

Giống như bất kỳ HĐH nào, nhân RTOS phải sử dụng các vùng quan trọng (CR) để bảo vệ tài nguyên được chia sẻ. Các công cụ phần mềm được sử dụng để thực

thi các vùng quan trọng bao gồm Event-Control-Bocks (ECB), mutexes, semaphores và hàng đợi tin nhắn, v.v. Tất cả các cơ chế này đều dựa trên giao thức

khóa, trong đó một tác vụ sẽ bị chặn nếu không thể nhận được CR. Ngoài các vấn đề thông thường về khóa, sự bế tắc và thiếu hụt như vậy, việc cho phép

giành quyền ưu tiên trong các khu vực quan trọng còn dẫn đến một vấn đề duy nhất được gọi là đảo ngược mức độ ưu tiên (Sha et al. 1990), có thể được

mô tả tốt nhất như sau.

Gọi TL, TM, TH lần lượt là các nhiệm vụ có mức độ ưu tiên thấp, trung bình và cao. Giả sử TL đã nhận được CR và đang thực thi bên trong CR. Tiếp

theo, khi TH sẵn sàng chạy, nó sẽ ưu tiên TL. Giả sử TH cũng cần CR tương tự mà TL vẫn nắm giữ. Vì vậy TH tự chặn CR, chờ TL giải phóng CR. Sau đó,

TM sẵn sàng chạy mà KHÔNG cần CR. Vì TM có mức độ ưu tiên cao hơn TL nên nó ngay lập tức chiếm ưu thế trước TL. Bây giờ TM đang chạy nhưng mức độ

ưu tiên của nó thấp hơn TH, dẫn đến đảo ngược mức độ ưu tiên. Trong trường hợp xấu nhất, TH có thể bị trì hoãn trong một khoảng thời gian không xác

định vì TM có thể bị ưu tiên bởi các nhiệm vụ khác có mức độ ưu tiên giữa TH và TM, v.v. Hiện tượng này được gọi là đảo ngược mức ưu tiên không giới

hạn. Tương tự, trong mô hình EDF, các ưu tiên của nhiệm vụ được chỉ định linh hoạt theo thời hạn gần nhất của chúng. Nếu các tác vụ chia sẻ tài

nguyên, điều này có thể khiến các tác vụ có mức độ ưu tiên cao hơn bị chặn thì vấn đề đảo ngược mức độ ưu tiên tương tự sẽ xảy ra, dẫn đến đảo ngược

thời hạn.

Cần lưu ý rằng trong nhân hệ điều hành thông thường, việc đảo ngược mức độ ưu tiên cũng có thể xảy ra nhưng tác dụng của nó thường không được

chú ý và vô hại vì tất cả những gì nó làm là trì hoãn một số tác vụ có mức độ ưu tiên cao hơn trong một thời gian, cuối cùng sẽ thu được các CR cần

thiết và tiếp tục. Trong RTOS, việc trì hoãn các nhiệm vụ có mức độ ưu tiên cao có thể khiến chúng bị trễ thời hạn, điều này có thể gây ra cảnh báo lỗi

hệ thống. Ví dụ nổi tiếng nhất về sự đảo ngược mức ưu tiên là vấn đề thiết lập lại hệ thống xảy ra trong sứ mệnh Mars Pathfinder (Jones 1997; Reeves

1997). Sự cố chỉ được giải quyết sau khi các kỹ sư tại JPL tái tạo sự cố trên trái đất, xác định nguyên nhân và sửa đổi bộ lập lịch tác vụ trên tàu để

tránh đảo ngược mức độ ưu tiên.


Machine Translated by Google

404 10 hệ điều hành nhúng thời gian thực

10.4 Ngăn chặn đảo ngược ưu tiên

Có nhiều cách để ngăn chặn việc đảo ngược mức độ ưu tiên. Điều đầu tiên là không cho phép các tác vụ chia sẻ tài nguyên, theo yêu cầu của

cả mô hình RMS và EDF, nhưng điều này rõ ràng là không thực tế. Cách thứ hai là giao tất cả các nhiệm vụ có cùng mức độ ưu tiên, điều này

cũng không thực tế. Cho đến nay, cách thực tế duy nhất để ngăn chặn việc đảo ngược mức độ ưu tiên là các kế hoạch sau.

10.4.1 Mức trần ưu tiên

Trong sơ đồ trần ưu tiên, nó giả định rằng đối với mỗi CR, mức ưu tiên trần của CR lớn hơn mức ưu tiên cao nhất trong tất cả các nhiệm vụ

có thể cạnh tranh cho CR. Bất cứ khi nào một nhiệm vụ giành được quyền kiểm soát CR, mức độ ưu tiên của nó sẽ ngay lập tức được nâng lên

mức ưu tiên trần của CR, do đó ngăn chặn sự ưu tiên từ bất kỳ nhiệm vụ nào khác có mức ưu tiên thấp hơn mức ưu tiên trần.

Điều này cũng ngụ ý rằng một tác vụ có thể khóa CR nếu mức độ ưu tiên của nó cao hơn mức ưu tiên trần của tất cả các CR bị khóa bởi các tác

vụ khác. Khi một tác vụ thoát khỏi CR, nó sẽ trở lại mức độ ưu tiên ban đầu. Mức trần ưu tiên dễ thực hiện nhưng có thể ngăn cản việc giành

quyền ưu tiên một cách không cần thiết. Ví dụ: trong khi giữ CR, một nhiệm vụ có mức độ ưu tiên thấp sẽ ngăn chặn sự ưu tiên của bất kỳ

nhiệm vụ nào khác thấp hơn mức ưu tiên trần ngay cả khi nó không cần cùng một CR.

10.4.2 Kế thừa ưu tiên

Trong sơ đồ kế thừa ưu tiên (Sha et al. 1990) , trong khi một nhiệm vụ giữ CR, nếu một nhiệm vụ khác có mức độ ưu tiên cao hơn cố gắng

giành được cùng một CR, thì nó sẽ tạm thời tăng mức độ ưu tiên của nhiệm vụ giữ CR lên mức ưu tiên của yêu cầu. nhiệm vụ. Điều này đảm bảo

rằng mức độ ưu tiên của tác vụ thực thi bên trong CR luôn bằng mức ưu tiên cao nhất của các tác vụ bị chặn trên CR.

Khi tác vụ thực thi thoát khỏi CR, nó sẽ trở lại mức ưu tiên ban đầu. Kế thừa ưu tiên linh hoạt hơn mức trần ưu tiên nhưng nó cũng phát

sinh nhiều chi phí hơn để thực hiện. Trong sơ đồ trần ưu tiên, mức độ ưu tiên của tác vụ thực thi bên trong CR là tĩnh, không thay đổi cho

đến khi thoát khỏi CR. Trong sơ đồ kế thừa ưu tiên, bất cứ khi nào một tác vụ sắp bị chặn trên CR, nó phải kiểm tra xem mức độ ưu tiên của

nó có cao nhất trong số tất cả các tác vụ đang chờ CR hay không và nếu có thì nó phải chuyển mức độ ưu tiên cho nhiệm vụ nắm giữ. CR. Do đó,

trong khi thực thi bên trong CR, mức độ ưu tiên của tác vụ có thể thay đổi linh hoạt.

10.5 Khảo sát RTOS

Không giống như các hệ điều hành có mục đích chung đòi hỏi nhiều khả năng, hệ điều hành thời gian thực thường được thiết kế để chỉ cung cấp

các chức năng hạn chế dành cho các môi trường chuyên biệt. Do đó, RTOS thường đơn giản hơn nhiều so với hệ điều hành có mục đích chung. Ví

dụ: trong hầu hết các RTOS, tất cả các tác vụ đều chạy trong cùng một không gian địa chỉ, do đó chúng không có chế độ nhân và chế độ người

dùng riêng biệt. Hơn nữa, hầu hết ROTS không hỗ trợ hệ thống tệp và giao diện người dùng, v.v. Mặc dù có yêu cầu nghiêm ngặt về thời gian,

RTOS thực sự dễ phát triển hơn so với hệ điều hành có mục đích chung. Điều này được chứng minh bằng số lượng lớn RTOS được đăng trên

Internet, từ ROTS nguồn mở dành cho những người có sở thích đến RTOS độc quyền dành cho thị trường thương mại. Trong phần này, chúng tôi

sẽ trình bày một nghiên cứu điển hình ngắn gọn về một số RTOS phổ biến.

10.5.1 FreeRTOS

FreeRTOS (2016) là kernel thời gian thực nguồn mở được thiết kế dành riêng cho các hệ thống nhúng nhỏ. FreeRTOS về cơ bản là một kernel cơ

bản, cung cấp hỗ trợ cơ bản để phát triển các ứng dụng thời gian thực. Các tính năng chính của Free-RTOS là

Nhiệm vụ: Các đơn vị thực thi trong FreeRTOS được gọi là nhiệm vụ. Mỗi tác vụ được thể hiện bằng Khối điều khiển tác vụ (TCB).

Tất cả các tác vụ thực thi trong cùng một không gian địa chỉ của kernel. Hạt nhân cung cấp hỗ trợ cho việc tạo, tạm dừng, tiếp tục, thay đổi

mức độ ưu tiên và xóa. Nó cũng hỗ trợ các đồng quy trình, là các đơn vị thực thi không cần nhiều không gian ngăn xếp.
Machine Translated by Google

10.5 Khảo sát RTOS 405

Lập lịch: Lập lịch tác vụ trong FreeRTOS được ưu tiên trước. Đối với các nhiệm vụ có cùng mức độ ưu tiên, nó cũng hỗ trợ hợp tác và luân chuyển

vòng tròn với các tùy chọn phân chia thời gian. Chuyển đổi tác vụ không được phép trong quá trình xử lý các ngắt lồng nhau. Trình lập lịch tác vụ

có thể bị vô hiệu hóa để ngăn chặn việc chuyển đổi tác vụ ở những vùng quan trọng dài.

Đồng bộ hóa: Đồng bộ hóa tác vụ trong FreeRTOS dựa trên hoạt động hàng đợi. Nó sử dụng cả các ẩn dụ nhị phân và đếm để đồng bộ hóa tác vụ chung

và nó sử dụng các biến đổi đệ quy với tính kế thừa ưu tiên để bảo vệ các vùng quan trọng. Một mutex đệ quy có thể được chủ sở hữu khóa/mở khóa

theo cách đệ quy ở độ sâu tối đa 256 cấp.

Bảo vệ bộ nhớ: FreeRTOS nói chung không có tính năng bảo vệ bộ nhớ, nhưng nó hỗ trợ bảo vệ bộ nhớ trên một số bo mạch ARM cụ thể, ví dụ FreeRTOS-

MPU hỗ trợ Bộ bảo vệ bộ nhớ ARM Cortex-M3 (MPU).

Dịch vụ hẹn giờ: Nhân FreeRTOS hỗ trợ cả bộ hẹn giờ và bộ hẹn giờ phần mềm.

Nó cũng hỗ trợ chế độ không tích tắc giúp ngăn chặn các ngắt định kỳ của bộ đếm thời gian để giảm mức tiêu thụ điện năng.

Tính di động: Hạt nhân FreeRTOS chỉ bao gồm một số tệp, hầu hết được viết bằng C. Nó đã được chuyển sang một số kiến trúc khác nhau, bao gồm

ARM, Intel x86 và PowerPC, v.v. DNX (DNX 2015) là RTOS dựa trên freeRTOS . Nó bổ sung giao diện API giống Unix, hỗ trợ hệ thống tệp và trình điều

khiển thiết bị mới vào nhân freeRTOS cơ bản.

10.5.2 MicroC/OS (µC/OS)

MicroC/OS (uC/OS) (Labrosse 1999) là một hạt nhân đa nhiệm thời gian thực ưu tiên dành cho bộ vi xử lý, bộ vi điều khiển và Bộ xử lý tín hiệu số

(DSP). Phiên bản hiện tại là uC/OS-III do Micrium (2016) duy trì và tiếp thị.

(1). Nhiệm vụ: Trong uC/OS, các đơn vị thực thi được gọi là nhiệm vụ, về cơ bản là các luồng thực thi trong cùng một không gian địa chỉ của hạt

nhân. Nhân uC/OS cung cấp hỗ trợ cho việc tạo, tạm dừng, tiếp tục, xóa và thống kê tác vụ. uC/OS-II hỗ trợ tới 256 tác vụ. Trong uC/OS-III, số

lượng tác vụ có thể thay đổi, chỉ bị giới hạn bởi dung lượng bộ nhớ khả dụng.

(2). Lập kế hoạch: Lập kế hoạch nhiệm vụ theo mức độ ưu tiên tĩnh ưu tiên. Ưu tiên nhiệm vụ được người dùng chỉ định, có lẽ là bằng thuật toán

RMS. Trong µC/OS-II, tất cả các tác vụ đều có mức độ ưu tiên khác nhau (theo ID tác vụ). µC/OS-III cho phép nhiều tác vụ chạy ở cùng mức độ ưu

tiên, do đó, nó cũng hỗ trợ lập lịch luân chuyển theo lát thời gian.

(3). Quản lý bộ nhớ: Nhân uC/OS không cung cấp bất kỳ biện pháp bảo vệ bộ nhớ nào. Nó cho phép người dùng xác định các phân vùng của vùng bộ nhớ

bao gồm các khối bộ nhớ có kích thước cố định. Việc phân bổ bộ nhớ được thực hiện theo các khối có kích thước cố định.

(4). Đồng bộ hóa: Nhân uC/OS dựa vào các ngắt tắt/bật để bảo vệ các vùng quan trọng ngắn và nó sử dụng bộ lập lịch tác vụ tắt/bật để bảo vệ các

vùng quan trọng dài. Các cơ chế khác được sử dụng để đồng bộ hóa tác vụ bao gồm các khối điều khiển sự kiện (ECB), tín hiệu ẩn dụ, hộp thư và

hàng đợi tin nhắn. Nhân uC/OS-III sử dụng các sema-phores (mutexes) loại trừ lẫn nhau với tính kế thừa ưu tiên để ngăn chặn sự đảo ngược mức độ

ưu tiên.

(5). Ngắt: Nhân uC/OS hỗ trợ các ngắt lồng nhau. Việc xử lý ngắt được thực hiện trực tiếp bên trong các thủ tục ISR.

Lập lịch tác vụ bị vô hiệu hóa trong quá trình xử lý các ngắt lồng nhau. Chuyển đổi tác vụ chỉ xảy ra khi tất cả các ngắt lồng nhau đã kết thúc.

(6). Dịch vụ hẹn giờ: µC/OS yêu cầu nguồn thời gian định kỳ để theo dõi độ trễ thời gian và thời gian chờ. Hạt nhân chỉ cung cấp các chức năng để

một tác vụ tự tạm dừng trong một số tích tắc hẹn giờ được chỉ định. Một tác vụ bị treo sẽ sẵn sàng để chạy lại khi hết thời gian trễ. Trước khi

hết thời gian yêu cầu, một tác vụ tạm dừng cũng có thể được tiếp tục bằng một tác vụ khác. Nó không cung cấp các chức năng dịch vụ hẹn giờ chung,

chẳng hạn như bộ hẹn giờ ngắt quãng với thông báo và hủy, v.v.

(7). Cổng: uC/OS được viết chủ yếu bằng ANSI-C. Cú pháp, quy ước đặt tên tệp và môi trường phát triển của nó dựa trên Microsoft IDE trên kiến

trúc Intel-x86. Có thông tin cho rằng uS/OS-III đã được chuyển sang một số nền tảng khác, chẳng hạn như kiến trúc ARM.

10.5.3 NuttX

NuttX (Nutt 2016) là một RTOS tập trung vào việc tuân thủ các tiêu chuẩn POSIX và ANSI. Nó bao gồm các API POSIX 1003 tiêu chuẩn và cả các API

được áp dụng từ các RTOS phổ biến khác. Một số API, chẳng hạn như task_creat, waitpid, vfork, execv, v.v. được điều chỉnh để phù hợp với môi

trường nhúng.
Machine Translated by Google

406 10 hệ điều hành nhúng thời gian thực

(1). Nhiệm vụ: Hạt nhân NuttX hỗ trợ tạo, chấm dứt, xóa, khởi tạo, kích hoạt và khởi động lại nhiệm vụ, v.v. Nhiệm vụ có thể được tạo bởi

task_creat trong một bước. Ngoài ra, chúng cũng có thể được tạo và khởi động bởi task_init, sau đó là task_activate theo hai bước. Không

giống như hầu hết các RTOS khác, NuttX cố gắng tuân thủ chặt chẽ tiêu chuẩn POSIX. Ví dụ: các tác vụ trong NuttX tuân theo mối quan hệ cha-

con. Tác vụ con có thể kế thừa các luồng tệp, ví dụ stdin, stdout và stderr, từ cha mẹ và cha mẹ có thể đợi sự chấm dứt của con. Nó cho phép

các tác vụ thay đổi hình ảnh thực thi thành các tệp khác nhau bằng execv. Trên một số nền tảng phần cứng, nó thậm chí còn hỗ trợ vfork, tạo

ra khung tác vụ mà không có hình ảnh thực thi. Một tác vụ vforked có thể sử dụng execv để tạo hình ảnh của chính nó từ một tệp thực thi.

(2). Lập kế hoạch: Lập kế hoạch nhiệm vụ là theo mức độ ưu tiên của nhiệm vụ ưu tiên. Mỗi tác vụ có thể đặt chính sách lập lịch của nó là

FIFO (Vào trước xuất trước) hoặc RR (Round-Robin). Các nhiệm vụ có cùng mức độ ưu tiên được lên lịch theo FIFO hoặc Round-robin với khoảng

thời gian quy định. Ngoài ra, các tác vụ cũng có thể thay đổi mức độ ưu tiên và nhường CPU cho các tác vụ khác có cùng mức độ ưu tiên.

(3). Đồng bộ hóa: Hạt nhân NuttX sử dụng semaphore đếm với kế thừa ưu tiên, có sẵn dưới dạng tùy chọn có thể định cấu hình. Nó hỗ trợ hàng

đợi tin nhắn được đặt tên của POSIX để liên lạc giữa các tác vụ. Bất kỳ tác vụ nào cũng có thể gửi/nhận tin nhắn đến/từ hàng đợi tin nhắn

được đặt tên. Trình xử lý ngắt cũng có thể gửi tin nhắn qua hàng đợi tin nhắn được đặt tên. Để ngăn việc chặn tác vụ trên hàng đợi tin nhắn

đầy đủ, tin nhắn có thể được gửi với tùy chọn hết thời gian chờ. Trong NuttX, thời gian chờ được triển khai bằng tín hiệu POSIX như trong

Unix.

(4). Tín hiệu: Ngoài các ẩn dụ và hàng đợi tin nhắn, NuttX còn sử dụng tín hiệu để liên lạc giữa các tác vụ. Nó cho phép bất kỳ tác vụ hoặc

trình xử lý ngắt nào gửi tín hiệu đến bất kỳ tác vụ nào (theo ID tác vụ). Tín hiệu là một sự gián đoạn (phần mềm) đối với một tác vụ, khiến

tác vụ đó thực thi chức năng xử lý tín hiệu được quy định. Không giống như Unix có trình xử lý tín hiệu được xác định trước, không có hành

động nào được xác định trước cho tín hiệu trong NuttX. Hành động mặc định cho tất cả các tín hiệu là bỏ qua tín hiệu đó. Nó cho phép người

dùng cài đặt bộ xử lý tín hiệu để xử lý tín hiệu.

(5). Dịch vụ đồng hồ và hẹn giờ: Hạt nhân NuttX hỗ trợ các chức năng dịch vụ hẹn giờ và hẹn giờ tương thích POSIX.

Mỗi tác vụ có thể tạo bộ đếm thời gian cho mỗi tác vụ dựa trên đồng hồ, cung cấp các tích tắc đồng hồ. Một tác vụ có thể đặt yêu cầu hẹn giờ theo khoảng thời gian.

Khi bộ hẹn giờ hết thời gian, tín hiệu hết thời gian chờ sẽ được gửi đến tác vụ, cho phép tác vụ xử lý tín hiệu bằng bộ xử lý tín hiệu được

cài đặt sẵn.

(6). Hệ thống tệp và Giao diện mạng: NuttX bao gồm một hệ thống tệp tùy chọn, không cần thiết để NuttX chạy. Nếu hệ thống tệp được bật, NuttX

sẽ bắt đầu bằng hệ thống tệp gốc giả trong bộ nhớ. Hệ thống tệp thực có thể được gắn trên hệ thống tệp gốc giả. Giao diện hệ thống tệp là một

tập hợp các API POSIX tiêu chuẩn, chẳng hạn như mở, đóng, đọc, ghi, v.v. Nó cung cấp khả năng mạng hạn chế bởi một tập hợp con các chức năng

giao diện ổ cắm.

10.5.4 VxWorks

VxWorks (2016) là RTOS độc quyền được Wind River phát triển cho các hệ thống nhúng. Do tính chất độc quyền của nó, chúng tôi chỉ có thể thu

thập một số thông tin chung về hệ thống dựa trên các tài liệu đã xuất bản và hướng dẫn sử dụng có sẵn trong phạm vi công cộng. Các tính năng

chính của hệ thống bao gồm những điều sau đây.

(1). Nhiệm vụ và Lập kế hoạch: hạt nhân đa nhiệm với khả năng lập lịch ưu tiên và luân phiên cũng như phản hồi ngắt nhanh.

(2). Đồng bộ hóa: Các ngữ nghĩa nhị phân và đếm, các mutex có tính kế thừa ưu tiên.

(3). Giao tiếp giữa các quá trình: Hàng đợi tin nhắn cục bộ và phân tán.

(4). Môi trường phát triển: Như một thông lệ phổ biến trong các hệ thống nhúng, VxWorks sử dụng môi trường phát triển biên dịch chéo. Phần

mềm ứng dụng được phát triển trên hệ thống máy chủ, ví dụ Linux. Máy chủ cung cấp một môi trường phát triển tích hợp (IDE) bao gồm trình

soạn thảo, chuỗi công cụ biên dịch, trình gỡ lỗi và trình mô phỏng. Các ứng dụng được biên dịch chéo để chạy trên các hệ thống đích, bao gồm

ARM, Intel x86 và PowerPC. Ngoài IDE, VxWorks còn bao gồm các gói hỗ trợ bo mạch, ngăn xếp mạng TCP/IP, phát hiện/báo cáo lỗi và gỡ lỗi biểu

tượng.

(5). Hệ thống tệp: VxWorks hỗ trợ một số hệ thống tệp, bao gồm Hệ thống tệp tin cậy (HRFS), hệ thống tệp FAT (DOSFS), hệ thống tệp mạng (NFS)

và TFFS cho các thiết bị flash. Đây có lẽ là những phần của IDE nhằm hỗ trợ môi trường phát triển. Không rõ liệu có ứng dụng thời gian thực

nào có thể bao gồm hỗ trợ hệ thống tệp hay không.


Machine Translated by Google

10.5 Khảo sát RTOS 407

10.5.5 QNX

QNX (2015) là một RTOS giống Unix độc quyền chủ yếu nhắm vào thị trường hệ thống nhúng. Một tính năng độc đáo của QNX là nó là một hệ thống

dựa trên vi nhân. Hạt nhân QNX chỉ chứa lập lịch CPU, giao tiếp giữa các quá trình, chuyển hướng ngắt và bộ hẹn giờ. Tất cả các chức năng

khác được thực thi khi người dùng xử lý bên ngoài vi hạt nhân. Đặc điểm chính của QNX
là:

(1). Giao tiếp liên tiến trình (IPC) Hạt vi mô QNX hỗ trợ các tiến trình. Mỗi tiến trình nằm trong một không gian địa chỉ duy nhất. Các tiến

trình giao tiếp với nhau bằng cách trao đổi thông điệp thông qua vi hạt nhân. QNX IPC bao gồm việc gửi tin nhắn từ tiến trình này sang tiến

trình khác và chờ phản hồi. Do chi phí trao đổi tin nhắn cao nên hầu hết các hệ thống dựa trên vi nhân đều không hoạt động tốt. QNX khắc phục

vấn đề này bằng cách sử dụng cơ chế truyền tin nhắn hiệu quả hơn. Trong QNX, msgSend, cho phép một tiến trình gửi tin nhắn và chờ phản hồi,

là một thao tác đơn lẻ. Thông báo được kernel sao chép từ không gian địa chỉ của quá trình gửi sang không gian địa chỉ của quá trình nhận.

Nếu quá trình nhận đang chờ tin nhắn, quyền điều khiển của CPU sẽ được chuyển sang quá trình nhận cùng lúc, điều này giúp loại bỏ nhu cầu bỏ

chặn rõ ràng quá trình nhận và gọi bộ lập lịch. Sự tích hợp chặt chẽ giữa truyền tin nhắn và lập lịch CPU là cơ chế chính giúp vi nhân QNX

hoạt động. Tất cả các hoạt động I/O, hoạt động hệ thống tệp và hoạt động mạng đều dựa trên việc truyền thông điệp. Việc xử lý tin nhắn được

ưu tiên theo mức độ ưu tiên của luồng. Vì các yêu cầu I/O được thực hiện bằng cách truyền thông điệp nên các luồng có mức độ ưu tiên cao

sẽ nhận được dịch vụ I/O trước các luồng có mức độ ưu tiên thấp. Các phiên bản sau của QNX giảm số lượng quy trình riêng biệt và tích hợp

ngăn xếp mạng và các khối chức năng khác vào các ứng dụng đơn lẻ để cải thiện hiệu suất hệ thống.

(2). Luồng: Trong QNX, thực thể thực thi nhỏ nhất là luồng. Mỗi tiến trình chứa một số luồng, là các đơn vị thực thi độc lập trong cùng một

không gian địa chỉ của một tiến trình. Hạt nhân QNX hỗ trợ các API tuân thủ Pthreads để tạo, quản lý và đồng bộ hóa luồng.

(3). Lập lịch trình: Lập lịch trình luồng theo mức độ ưu tiên ưu tiên. Ngoài ra, nó cũng hỗ trợ lập lịch phân vùng thích ứng (APS), đảm bảo

tỷ lệ phần trăm CPU tối thiểu cho các nhóm luồng đã chọn, mặc dù các luồng khác có thể có mức độ ưu tiên cao hơn.

(4). Đồng bộ hóa: Trong QNX, IPC dành cho việc truyền thông điệp giữa các tiến trình trong các không gian địa chỉ khác nhau. Nó không dành

cho các luồng trong cùng một không gian địa chỉ của một tiến trình. Để đồng bộ hóa luồng, QNX sử dụng API tuân thủ POSIX để hỗ trợ các mutex,

biến điều kiện, ngữ nghĩa, rào cản và khóa trình đọc-ghi, v.v. Khi các tiến trình được phép chia sẻ bộ nhớ, hầu hết các cơ chế cũng có thể

áp dụng cho các luồng trong các tiến trình khác nhau.

(5). Bộ tải khởi động: Một thành phần quan trọng khác của QNX là bộ tải khởi động, có thể tải một hình ảnh không chỉ chứa kernel mà còn bất

kỳ bộ sưu tập chương trình người dùng và thư viện chia sẻ mong muốn nào. Nó cho phép các chương trình người dùng, trình điều khiển thiết

bị và thư viện hỗ trợ được tích hợp vào cùng một image khởi động.

(6). Nền tảng: Theo tài liệu QNX mới nhất, QNX Neutrino hỗ trợ SMP và MP với mối quan hệ giữa bộ xử lý, khóa từng ứng dụng với một CPU cụ

thể. Do kiến trúc vi nhân của nó, việc thích ứng QNX với môi trường phân tán cũng sẽ dễ dàng hơn.

10.5.6 Linux thời gian thực

Linux tiêu chuẩn là một hệ điều hành có mục đích chung, không được thiết kế cho các ứng dụng thời gian thực. Mặc dù vậy, Linux tiêu chuẩn có

hiệu suất trung bình tuyệt vời và thậm chí có thể cung cấp độ chính xác lập lịch tác vụ ở mức mili giây. Tuy nhiên, nó không được thiết kế

để cung cấp các dịch vụ thời gian thực yêu cầu độ chính xác dưới một phần nghìn giây và đảm bảo về thời gian đáng tin cậy. Nguyên nhân cơ

bản là do nhân Linux không có tính ưu tiên. Theo truyền thống, nhân Linux chỉ cho phép ưu tiên nhiệm vụ trong một số trường hợp nhất định:

. Khi tác vụ đang chạy ở chế độ người dùng.

Khi tác vụ trở về từ cuộc gọi hệ thống hoặc xử lý gián đoạn trở lại chế độ người dùng. Khi tác vụ

ngủ hoặc chặn trong kernel để nhường quyền kiểm soát rõ ràng cho quy trình khác

Trong khi một tác vụ thực thi trong nhân Linux, nếu một sự kiện xảy ra khiến tác vụ có mức ưu tiên cao sẵn sàng chạy thì tác vụ có mức ưu tiên cao đó

không thể ưu tiên tác vụ đang chạy cho đến khi tác vụ sau mang lại quyền kiểm soát một cách rõ ràng. Trong trường hợp xấu nhất, độ trễ khi chuyển sang

tác vụ có mức độ ưu tiên cao có thể lên tới hàng trăm mili giây hoặc hơn. Do đó, trong nhân Linux tiêu chuẩn, các tác vụ có mức độ ưu tiên cao có thể được
Machine Translated by Google

408 10 hệ điều hành nhúng thời gian thực

bị trì hoãn trong một khoảng thời gian tùy ý, khiến hệ thống không thể phản hồi nhanh chóng các sự kiện hoặc đảm bảo thời hạn thực hiện nhiệm vụ.

Có nhiều nỗ lực sửa đổi nhân Linux để cải thiện khả năng thời gian thực của nó. Sau đây, chúng ta sẽ thảo luận về hai cách tiếp cận khác nhau

đối với vấn đề này.

10.5.6.1 Bản vá Linux thời gian thực

Nhân Linux 2.6 có tùy chọn cấu hình CONFIG_PREEMPT_VOLUNTARY, tùy chọn này có thể được bật khi biên dịch ảnh hạt nhân. Nó giới thiệu các biện

pháp kiểm tra đối với các nguyên nhân phổ biến nhất gây ra độ trễ dài trong mã hạt nhân, cho phép hạt nhân tự nguyện nhường quyền kiểm soát cho

tác vụ có mức độ ưu tiên cao hơn. Ưu điểm của sơ đồ này là rất dễ thực hiện và chỉ có tác động hạn chế đến thông lượng hệ thống. Nhược điểm

là mặc dù nó làm giảm sự xuất hiện của độ trễ dài nhưng không loại bỏ hoàn toàn chúng. Để tiếp tục khắc phục sự cố, nhân Linux 2.6 cung cấp một

tùy chọn bổ sung, CONFIG_PREEMPT, khiến tất cả mã nhân bên ngoài các vùng được bảo vệ bằng spinlock và trình xử lý ngắt có đủ điều kiện để được

ưu tiên thực hiện các tác vụ có mức độ ưu tiên cao hơn. Với tùy chọn này, độ trễ trong trường hợp xấu nhất giảm xuống khoảng một mili giây

chữ số (Hagen 2005), mặc dù một số trình điều khiển thiết bị có thể có bộ xử lý ngắt vẫn gây ra độ trễ dài hơn nhiều. Để hỗ trợ các tác vụ thời

gian thực yêu cầu độ trễ dưới mili giây một chữ số, nhân Linux hiện tại vẫn có một tùy chọn khác, CONFIG_PREEMPT_RT, được gọi là bản vá RT-

Preempt, giúp chuyển đổi nhân Linux thành một nhân hoàn toàn có thể sử dụng trước bằng các phương tiện sau.

. Làm cho các khóa nguyên thủy (spinlock) trong nhân có thể được ưu tiên bằng cách triển khai lại với các mustex thời gian thực (rt-mutexes).

Rt_mutexes mở rộng ngữ nghĩa của các mutex đơn giản với tính kế thừa ưu tiên, trong đó chủ sở hữu có mức ưu tiên thấp của rt-mutex sẽ kế thừa

mức ưu tiên cao nhất trong tất cả các tác vụ đang chờ rt-mutex. Trong một chuỗi yêu cầu dành cho rt-mutex, nếu chủ sở hữu của rt-mutex bị chặn

trên một rt-mutex khác, nó sẽ truyền mức độ ưu tiên được tăng cường cho chủ sở hữu của rt_mutex khác. Việc tăng mức độ ưu tiên sẽ bị xóa ngay

lập tức sau khi rt_mutex được mở khóa. Việc triển khai rt-mutexes được thực hiện hiệu quả hơn bằng cách sử dụng danh sách p của kernel, danh

sách này theo dõi mức độ ưu tiên cao nhất của các tác vụ bị chặn. Trên các kiến trúc hỗ trợ hoạt động nguyên tử cmp-xhg (so sánh và trao đổi),

các hoạt động khóa/mở khóa trên rt-mutex có thể được tối ưu hóa hơn nữa.

. Chuyển đổi các trình xử lý ngắt thành các luồng hạt nhân có thể sử dụng trước. Bản vá RT-Preempt xử lý việc thực thi các trình xử lý ngắt như

các luồng giả hạt nhân, có mức độ ưu tiên cao hơn tất cả các luồng thông thường nhưng chúng có thể được ưu tiên trước (bởi các luồng giả

khác có mức độ ưu tiên cao hơn), cho phép nhân Linux hỗ trợ các ngắt lồng nhau bằng cách ngắt những ưu tiên.

. Sử dụng bộ đếm thời gian thực có độ phân giải cao. Chuyển đổi API hẹn giờ Linux cũ thành cơ sở hạ tầng riêng biệt cho bộ hẹn giờ hạt nhân có độ phân giải cao

cộng với bộ hẹn giờ giống như con chó canh gác để hết thời gian chờ, cho phép bộ hẹn giờ POSIX có độ phân giải cao trong không gian người dùng.

Hiệu suất của nhân Linux 2.6 với bản vá RT-Preempt đã được nghiên cứu trong (Hagen 2005). Kết quả thử nghiệm cho thấy giảm đáng kể tình

trạng giật cục trong quá trình xử lý ngắt, dẫn đến hệ thống Linux phản ứng nhanh hơn và có thể dự đoán được nhiều hơn.

10.5.6.2 RTLinux RTLinux

(Yodaiken Yodaiken 1999) là một RTOS chạy các tác vụ thời gian thực đặc biệt và các trình xử lý ngắt trên cùng một máy với Linux tiêu chuẩn. Nó

coi Linux như một tác vụ ưu tiên có mức ưu tiên thấp nhất, chỉ chạy nếu không có tác vụ thời gian thực nào có thể chạy được và nó có thể được

ưu tiên bất cứ khi nào một tác vụ thời gian thực sẵn sàng chạy. Nguyên tắc cơ bản của RTLinux khá đơn giản. Nó đặt một lớp phần mềm mô phỏng,

nhân RTLinux, giữa Linux và phần cứng bộ điều khiển ngắt. Tất cả các ngắt phần cứng đều được trình mô phỏng phát hiện trước tiên. Nó xử lý trực

tiếp các ngắt liên quan đến thời gian thực và chuyển tiếp các ngắt không liên quan đến thời gian thực khác tới nhân Linux. Trong mã nhân Linux

(trên kiến trúc Intel x86), tất cả các lệnh cli (vô hiệu hóa các ngắt), sti (cho phép ngắt) và iret (trả về từ các ngắt) lần lượt được thay thế

bằng các macro mô phỏng S_CLI, S_STI và S_IRET. Macro S_CLI xóa biến toàn cục SFIF về 0, cho biết nhân Linux vừa thực thi cli để tắt các ngắt.

Macro S_STI mô phỏng một ngắt thực sự bằng cách tạo một khung ngăn xếp bao gồm thanh ghi FLAG CPU đã lưu, thanh ghi DS nhân Linux và một địa

chỉ trả về nhưng thay vào đó lại thực thi macro S_IRET. Khi xảy ra gián đoạn Linux, trình mô phỏng sẽ kiểm tra biến SFIF. Nếu nó được đặt, tức

là nhân Linux đã kích hoạt các ngắt, nó sẽ gọi trình xử lý ngắt Linux ngay lập tức. Mặt khác, nó đặt một bit trong biến SFIF để biểu thị một

ngắt Linux đang chờ xử lý. Khi nhân Linux cho phép ngắt bằng sti, macro S_IRET sẽ quét biến SFIF để tìm các ngắt Linux đang chờ xử lý (các bit

khác 0). Đối với mỗi ngắt Linux đang chờ xử lý, nó sẽ gọi trình xử lý ngắt Linux tương ứng cho đến khi tất cả các ngắt đang chờ xử lý được xử

lý.

RTLinux được cấu trúc như một thành phần cốt lõi nhỏ và một tập hợp các thành phần tùy chọn. Thành phần cốt lõi cho phép cài đặt các trình

xử lý ngắt có độ trễ rất thấp mà bản thân Linux và một số cấp độ thấp không thể trì hoãn hoặc ưu tiên trước.
Machine Translated by Google

10.5 Khảo sát RTOS 409

các thủ tục điều khiển ngắt và đồng bộ hóa. Bên trong RTLinux, các tác vụ thời gian thực được cài đặt dưới dạng mô-đun Linux thực thi

trong cùng không gian địa chỉ với nhân Linux. Giao tiếp giữa quy trình Linux và các tác vụ thời gian thực được thực hiện bằng bộ nhớ dùng

chung hoặc các ống FIFO chuyên dụng. Một số thử nghiệm trước đó (Yokaiken 1999) cho thấy lõi RTLinux có thể hỗ trợ các tác vụ thời gian

thực với độ trễ khoảng hàng chục micro giây trên các CPU Intel x86 cũ hơn. Tuy nhiên, cũng có thông tin cho rằng việc gián đoạn thời gian

thực rất thường xuyên sẽ khiến Linux không thể chạy hoàn toàn.

10.5.7 Phê bình RTOS hiện có

Trong phần này, chúng ta sẽ đánh giá các RTOS khác nhau và xây dựng một bộ hướng dẫn chung cho việc thiết kế và triển khai RTOS.

10.5.7.1 Tổ chức RTOS Dựa trên các

nghiên cứu điển hình ở trên, chúng ta có thể thấy rằng hầu hết RTOS đều dựa trên cách tiếp cận từ dưới lên, trong đó RTOS được xây dựng

với nhân cơ bản để hỗ trợ các tác vụ, lập lịch tác vụ, các vùng quan trọng và đồng bộ hóa tác vụ. Sau đó, thêm các chức năng bổ sung vào

nhân cơ bản, ví dụ như theo dõi thực thi, ghi nhật ký sự kiện, gỡ lỗi, hệ thống tệp và kết nối mạng, v.v. để cải thiện khả năng của hệ

thống. Hầu hết các RTOS độc quyền nhắm vào thị trường thương mại thường cũng cung cấp môi trường phát triển tích hợp (IDE) để tạo điều

kiện thuận lợi cho việc phát triển ứng dụng của người dùng. Hạn chế chính của cách tiếp cận từ dưới lên là thiếu tính đồng nhất. Các

RTOS khác nhau có thể phát triển và thúc đẩy các giao diện hệ thống độc quyền của riêng chúng, làm phức tạp quá trình phát triển ứng dụng

người dùng. Để khắc phục sự cố này, nhiều RTOS cố gắng tuân thủ các tiện ích mở rộng thời gian thực POSIX 1003.1b. Bất chấp những nỗ lực

này, tính khả dụng của các chức năng dịch vụ hệ thống tiêu chuẩn vẫn khác nhau giữa các hệ thống.

Một cách tiếp cận khác để thiết kế RTOS là cách tiếp cận từ trên xuống, nhằm mục đích chuyển đổi một hệ điều hành hiện có, chẳng hạn

như Linux, để hỗ trợ các hoạt động thời gian thực. Ưu điểm của phương pháp này là rõ ràng. Ngoài việc bổ sung các khả năng thời gian

thực, nó sẽ làm cho tất cả các chức năng của một hệ điều hành hoàn chỉnh có thể truy cập trực tiếp được. Hạn chế chính của phương pháp

này là kích thước hệ thống lớn, có thể không phù hợp với các hệ thống nhúng hoặc thời gian thực nhỏ.

10.5.7.2 Nhiệm vụ trong RTOS

Trong hệ điều hành, các tiến trình đề cập đến các thực thể thực thi có không gian địa chỉ riêng biệt và các luồng là các đơn vị thực thi

trong cùng một địa chỉ của một tiến trình. Để cung cấp cho mỗi quy trình một không gian địa chỉ duy nhất, các quy trình thường thực thi

ở hai chế độ khác nhau; chế độ kernel và chế độ người dùng. Khi ở chế độ kernel, tất cả các tiến trình đều chia sẻ cùng một không gian

địa chỉ của kernel. Khi ở chế độ người dùng, mỗi quy trình thực thi trong một không gian địa chỉ riêng biệt được cách ly và bảo vệ khỏi

các quy trình khác. Điều này thường đạt được thông qua việc ánh xạ địa chỉ ảo bằng phần cứng quản lý bộ nhớ. Trong tất cả các hệ thống

giống Unix, các quy trình được tạo bởi mô hình fork-exec. Fork tạo một tiến trình con có hình ảnh (chế độ người dùng) giống hệt với tiến

trình gốc. Exec cho phép một tiến trình thay đổi hình ảnh thực thi sang một tệp khác. Ngoài ra, các tiến trình còn tuân theo mối quan hệ

cha-con. Một tiến trình cha có thể đợi quá trình con kết thúc. Khi một tiến trình kết thúc, nó sẽ thông báo cho tiến trình cha, tiến trình

này sẽ thu thập trạng thái thoát của tiến trình con và cuối cùng giải phóng tiến trình con để tái sử dụng. Tuy nhiên, mô hình quy trình

không phù hợp với các hệ thống nhúng đơn giản và hệ thống thời gian thực, ngoại trừ các hệ thống dựa trên vi nhân, chẳng hạn như QNX.

Trong hầu hết tất cả RTOS, các tác vụ về cơ bản là các luồng vì chúng đều thực thi trong cùng một không gian địa chỉ của nhân hệ thống.

10.5.7.3 Quản lý bộ nhớ trong RTOS Quản lý bộ nhớ đề

cập đến ba khía cạnh riêng biệt: ánh xạ không gian địa chỉ ảo, phân bổ bộ nhớ động trong quá trình thực thi và kiểm tra tràn ngăn xếp

trong thời gian chạy. Sau đây, chúng ta sẽ thảo luận về từng sơ đồ quản lý bộ nhớ được sử dụng trong hầu hết các RTOS.

Nhiệm vụ không gian địa

chỉ thực trong hầu hết RTOS thực thi trong cùng không gian địa chỉ của kernel. Môi trường không gian địa chỉ duy nhất có nhiều lợi thế.

Đầu tiên, nó loại bỏ chi phí liên quan đến ánh xạ địa chỉ ảo của phần cứng quản lý bộ nhớ.

Thứ hai, nó cho phép các tác vụ chia sẻ bộ nhớ trực tiếp để liên lạc giữa các tác vụ nhanh chóng. Nhược điểm chính của sơ đồ không gian

địa chỉ đơn là thiếu khả năng bảo vệ bộ nhớ. Bất kỳ tác vụ nào cũng có thể làm hỏng bộ nhớ dùng chung, khiến các tác vụ khác hoặc toàn bộ

hệ thống bị lỗi.
Machine Translated by Google

410 10 hệ điều hành nhúng thời gian thực

Không gian địa chỉ ảo Hầu hết

RTOS không cho phép các tác vụ có không gian địa chỉ riêng. Vì vậy không có ánh xạ địa chỉ ảo và bảo vệ bộ nhớ. Vì lý do bảo mật và độ tin cậy,

việc bảo vệ bộ nhớ có thể cần thiết. Nếu vậy, RTOS nên sử dụng sơ đồ ánh xạ bộ nhớ đơn giản nhất của phần cứng Bộ quản lý bộ nhớ (MMU) để có

hiệu quả tốt hơn. Ví dụ: trên kiến trúc Intel, nó nên sử dụng phân đoạn thay vì phân trang (2 cấp). Tương tự, trên kiến trúc ARM nên sử dụng

phân trang một cấp với kích thước trang lớn thay vì phân trang hai cấp. Một số mục nhập trang, ví dụ như các trang kernel dùng chung, có thể

được đặt trong TLB dưới dạng các mục nhập khóa để giảm thiểu chi phí MMU trong quá trình chuyển đổi tác vụ.

Phân bổ bộ nhớ động Nhiều RTOS cho

phép các tác vụ phân bổ bộ nhớ động trong quá trình thực thi. Một số RTOS thậm chí còn hỗ trợ các hàm malloc()/free() tiêu chuẩn của thư viện C

để phân bổ/giải phóng bộ nhớ một cách linh hoạt, có lẽ là từ một vùng heap của hệ thống. Tuy nhiên, việc kiểm tra kỹ hơn các yêu cầu hệ thống

thời gian thực sẽ tiết lộ rằng việc cho phép cấp phát bộ nhớ động trong thời gian chạy có thể không phải là một ý tưởng hay. Không giống như các

tác vụ thông thường, yêu cầu chính của các tác vụ thời gian thực là hành vi của chúng phải mang tính xác định và có thể dự đoán được. Việc phân

bổ bộ nhớ động trong thời gian chạy sẽ gây ra độ trễ khác nhau cho việc thực thi tác vụ, khiến chúng không thể đoán trước được. Nhu cầu chính

đáng duy nhất cho việc phân bổ bộ nhớ là cung cấp cho các tác vụ bộ nhớ dùng chung để liên lạc giữa các tác vụ nhanh chóng. Trong trường hợp đó,

bộ nhớ cần thiết phải được phân bổ tĩnh hoặc dưới dạng khối có kích thước cố định thay vì theo khối có kích thước thay đổi.

Tràn ngăn xếp Kiểm tra tất cả

việc tạo tác vụ hỗ trợ RTOS. Khi tạo một tác vụ mới, người dùng có thể chỉ định một hàm sẽ được tác vụ đó thực thi, kích thước ngăn xếp tác vụ

và một con trỏ tới các tham số ban đầu của hàm. Hầu hết RTOS đều hỗ trợ kiểm tra tràn ngăn xếp trong thời gian chạy.

Đây có vẻ là một tính năng hay nhưng thực ra lại hời hợt, vì những lý do sau. Đầu tiên, việc sử dụng ngăn xếp của bất kỳ chương trình nào phải

được kiểm soát cẩn thận trong quá trình thiết kế chương trình. Sau khi chương trình được viết, kích thước ngăn xếp cần thiết trong quá trình

thực thi có thể được ước tính bằng độ dài của lệnh gọi hàm và lượng không gian biến cục bộ trong hàm. Kích thước ngăn xếp thực tế của một

chương trình có thể được quan sát thông qua thử nghiệm. Kích thước ngăn xếp tối đa có thể được đặt thành kích thước quan sát được, cộng với

hệ số an toàn, trong mã chương trình cuối cùng. Xét cho cùng, đây là cách thực hành tiêu chuẩn được tất cả các nhà thiết kế hạt nhân hệ điều

hành sử dụng. Nếu mọi chương trình đều được phát triển theo cách này thì sẽ không có lý do gì khiến bất kỳ tác vụ nào hết dung lượng ngăn xếp

trong khi thực thi. Thứ hai, nếu không có phần cứng bảo vệ bộ nhớ, việc kiểm tra tràn ngăn xếp phải được thực hiện bằng phần mềm, ví dụ: kiểm

tra con trỏ ngăn xếp dựa trên giá trị giới hạn ngăn xếp đặt trước khi truy cập vào mọi chức năng, nhưng điều này sẽ gây ra thêm chi phí thời

gian chạy và độ trễ. Thứ ba, ngay cả khi chúng tôi bao gồm việc kiểm tra tràn ngăn xếp trong thời gian chạy, vẫn chưa rõ ràng những gì có thể

được thực hiện nếu một tác vụ gây ra tràn ngăn xếp. Việc hủy bỏ nhiệm vụ có thể là điều không thể. Việc mở rộng không gian ngăn xếp và cho phép

tác vụ tiếp tục có thể gây ra độ trễ không thể chấp nhận được. Hầu hết RTOS chỉ để lại câu hỏi này cho người dùng. Như thường lệ, cách tiếp cận

tốt nhất để xử lý tình trạng tràn ngăn xếp là ngăn nó xảy ra trong quá trình phát triển chương trình thông qua thử nghiệm.

10.5.7.4 API tuân thủ POSIX POSIX (2016) chỉ

định một bộ tiêu chuẩn cho các hệ thống giống Unix. POSIX.1 chỉ định các chức năng dịch vụ cốt lõi, chẳng hạn như các hoạt động tín hiệu, đường

ống, tệp và thư mục. POSIX.1b thêm tiện ích mở rộng thời gian thực và POSIX1.c thêm hỗ trợ luồng. Mục tiêu của tiêu chuẩn POSIX là cung cấp giao

diện người dùng thống nhất để tạo điều kiện thuận lợi cho việc phát triển các chương trình ứng dụng di động trên hệ thống giống Unix. Nhiều hệ

thống thời gian thực cố gắng tuân thủ POSIX. Tuy nhiên, việc tuân thủ nghiêm ngặt các tiêu chuẩn POSIX thực sự có thể cản trở hoạt động trong

thời gian thực. Trong Unix tiêu chuẩn, đơn vị thực thi là các tiến trình. Trong hầu hết các hệ thống thời gian thực, các tác vụ về cơ bản là các

luồng vì chúng thực thi trong cùng một không gian địa chỉ của kernel. Sẽ rất hợp lý khi cung cấp các cơ chế đồng bộ hóa luồng, chẳng hạn như các

biến mutex, rào cản và điều kiện, để đồng bộ hóa tác vụ, nhưng sẽ rất khó giải thích tại sao các tác vụ phải sử dụng mở-đóng-đọc-ghi trên bộ mô

tả tệp cho I/O. Nếu một tác vụ thời gian thực cần I/O, việc gọi trực tiếp trình điều khiển thiết bị sẽ hiệu quả hơn nhiều thay vì phải trải qua

các lớp ánh xạ bổ sung thông qua bộ mô tả tệp.

10.5.7.5 Nguyên thủy đồng bộ hóa Tất cả RTOS đều

cung cấp một bộ công cụ để đồng bộ hóa tác vụ. Sau đây, chúng tôi thảo luận về tính phù hợp của các công cụ như vậy trong RTOS.

. Vô hiệu hóa các ngắt và lập lịch tác vụ: Một số ROTS cho phép các chương trình người dùng vô hiệu hóa các ngắt khi đi vào các vùng quan trọng,

nhưng điều này mâu thuẫn với yêu cầu về độ trễ ngắt ngắn của hệ thống thời gian thực. Nhiều RTOS cho phép chương trình người dùng vô hiệu hóa
Machine Translated by Google

10.5 Khảo sát RTOS 411

lập lịch tác vụ để ngăn chặn việc chuyển đổi tác vụ trong các vùng quan trọng dài, nhưng điều này mâu thuẫn với việc lập lịch tác vụ trước. Trong

ROTS, các ứng dụng của người dùng phải được bảo vệ hoặc thậm chí không được phép thực hiện các hoạt động cấp thấp này.

. Khóa Mutex: Hầu hết tất cả RTOS đều sử dụng các mutex đơn giản với tính kế thừa ưu tiên để đảm bảo quyền truy cập độc quyền vào các khu vực quan trọng.

Một số RTOS hỗ trợ các mutex đệ quy, chủ sở hữu có thể khóa/mở khóa đệ quy. Khi viết các chương trình đồng thời sử dụng bất kỳ loại cơ chế khóa nào,

yêu cầu cơ bản là chương trình phải không có bế tắc.

Điều này thường đạt được bằng cách đảm bảo rằng thứ tự khóa luôn là một chiều. Không thể tưởng tượng được rằng bất kỳ nhiệm vụ nào cũng cần phải có

được cùng một khóa mà nó đã nắm giữ. Vì vậy, không có nhu cầu thực sự về các mutex đệ quy.

. Semaphores nhị phân và đếm: Semaphores là công cụ thuận tiện để đồng bộ hóa và hợp tác nhiệm vụ chung.

Nhiều RTOS hỗ trợ cả ngữ nghĩa nhị phân và ngữ nghĩa đếm, đòi hỏi ngữ nghĩa và cách triển khai khác nhau. Vì việc đếm các đèn hiệu có tính chất tổng

quát hơn so với các đèn hiệu nhị phân nên không cần có hai loại đèn hiệu. Trong một số RTOS, khi một tác vụ cố gắng lấy một semaphore, nó có thể chỉ

định một tham số hết thời gian chờ. Nếu tác vụ bị chặn trên semaphore, nó sẽ được bỏ chặn khi giá trị hết thời gian chờ hết hạn. Tính hữu ích của tính

năng này rất đáng nghi ngờ do nó có thể gây ra nhiều vấn đề. Đầu tiên, điều này sẽ gây ra độ trễ bổ sung cho bộ xử lý ngắt hẹn giờ, bộ xử lý này phải

xử lý thời gian còn lại của tất cả các tác vụ bị chặn và bỏ chặn chúng khi hết thời gian. Các câu hỏi khác bao gồm: giá trị thời gian chờ là bao nhiêu?

Nhiệm vụ nên làm gì trong trường hợp hết thời gian chờ? Một giải pháp tốt hơn là sử dụng thao tác CP có điều kiện trên các ẩn dụ. Nó cho phép người

dùng chỉ định một hành động thay thế ngay lập tức nếu một tác vụ không thể lấy được semaphore, thay vì phải chờ thời gian chờ.

. Khối điều khiển sự kiện (ECB): Không giống như mutexes và semaphores, cờ sự kiện cho phép các tác vụ chờ số lượng sự kiện thay đổi, giúp tăng thêm

tính linh hoạt cho hệ thống.


. Giao tiếp giữa các tác vụ: Sử dụng bộ nhớ dùng chung, kết hợp với mutexes để bảo vệ, vừa thuận tiện vừa hiệu quả để liên lạc giữa các tác vụ nhanh

chóng. Nhiều RTOS cung cấp hàng đợi tin nhắn tĩnh hoặc động để các tác vụ trao đổi tin nhắn. Chúng kém hiệu quả hơn bộ nhớ và đường ống dùng chung

nhưng chúng cho phép các chương trình ứng dụng linh hoạt hơn.

Một số RTOS, ví dụ như NuttX, cố gắng sử dụng tín hiệu, đây sẽ là một lựa chọn kém vì tín hiệu thông thường không phù hợp với giao tiếp giữa các quá

trình (Wang 2015) và tín hiệu mở rộng kém hiệu quả hơn tin nhắn.

10.5.7.6 Xử lý ngắt trong RTOS Một trong những yêu cầu

cơ bản của RTOS là độ trễ ngắt tối thiểu, ngụ ý rằng RTOS phải cho phép các ngắt lồng nhau. Tất cả ROTS đều hỗ trợ các ngắt lồng nhau, nhưng chúng có

thể xử lý các ngắt lồng nhau theo những cách khác nhau. Trong hầu hết các RTOS, việc xử lý ngắt được thực hiện trực tiếp bên trong các trình xử lý

ngắt. Chuyển đổi tác vụ bị vô hiệu hóa cho đến khi tất cả quá trình xử lý ngắt lồng nhau hoàn tất. Trong Linux với các bản vá thời gian thực, các ngắt

được xử lý bằng các luồng giả trong kernel. Điều này cho phép phản hồi ngắt nhanh hơn vì mỗi trình xử lý ngắt chỉ cần kích hoạt một luồng giả thay vì

thực sự xử lý ngắt. Trong sơ đồ này, việc xử lý các ngắt lồng nhau được đẩy lên mức giả luồng. Việc lựa chọn xử lý các ngắt lồng nhau một cách trực

tiếp hay bằng các tác vụ giả phải dựa trên phần cứng ngắt. Đối với CPU Intel x86 không có ngăn xếp chế độ ngắt riêng biệt, các ngắt được xử lý trong

bối cảnh tác vụ bị gián đoạn, tác vụ này có thể sử dụng cùng một ngăn xếp để xử lý các ngắt lồng nhau. Trong trường hợp này, sẽ tốt hơn nếu xử lý các

ngắt lồng nhau trực tiếp bên trong ISR. Đối với CPU ARM sử dụng ngăn xếp chế độ IRQ riêng biệt, bối cảnh bị gián đoạn phải được chuyển sang ngăn xếp

chế độ đặc quyền khác trước khi cho phép một ngắt khác. Trong trường hợp này, sẽ tốt hơn nếu xử lý các ngắt lồng nhau bằng các tác vụ giả.

10.5.7.7 Thời hạn nhiệm vụ Mặc dù

tất cả RTOS đều dành cho (hoặc được cho là) các hệ thống thời gian thực cứng, nhưng trên thực tế, hầu hết RTOS chỉ cung cấp một khuôn khổ cơ bản để

phát triển các ứng dụng thời gian thực. Như vậy, không có RTOS nào được xuất bản thực sự có thể đảm bảo thời hạn thực hiện nhiệm vụ. Kết quả phân tích

của RMS và EDF chỉ cung cấp hướng dẫn chung cho RTOS trong những điều kiện lý tưởng và đơn giản nhất. Chúng không tính đến chi phí xử lý ngắt, thời

gian chặn tác vụ do chia sẻ tài nguyên, lập lịch tác vụ và thời gian chuyển đổi, v.v. trong hệ thống ROTS thực. Khi sử dụng RTOS để phát triển các ứng

dụng thời gian thực, người dùng hoàn toàn phụ thuộc vào việc xác định xem các tác vụ có thể đáp ứng thời hạn hay không. Điều này không có gì đáng ngạc

nhiên khi xét đến phạm vi rộng các ứng dụng thời gian thực có thể có. Để giúp khắc phục vấn đề này, hầu hết RTOS đều cung cấp phương tiện theo dõi

thời gian chạy, cho phép người dùng theo dõi thời gian trong đó các ngắt bị vô hiệu hóa và/hoặc lượng thời gian thực hiện các tác vụ trong các vùng

quan trọng, v.v. Bất chấp những nỗ lực này, việc đánh giá hiệu suất của hệ thống thời gian thực về cơ bản tập trung vào việc phân tích từng trường hợp

riêng lẻ.
Machine Translated by Google

412 10 hệ điều hành nhúng thời gian thực

10.6 Nguyên tắc thiết kế của RTOS

Dựa trên các cuộc thảo luận ở trên, chúng tôi đề xuất một bộ hướng dẫn chung cho việc thiết kế và triển khai các thành phần chính của RTOS.

10.6.1 Xử lý ngắt

RTOS phải hỗ trợ các ngắt lồng nhau. Tùy thuộc vào phần cứng ngắt, việc xử lý ngắt có thể được thực hiện trực tiếp trong trình xử lý ngắt hoặc dưới dạng tác vụ

giả có mức độ ưu tiên cao hơn tác vụ thông thường.

10.6.2 Quản lý tác vụ

RTOS sẽ hỗ trợ tạo tác vụ. Bất cứ khi nào có thể, nhiệm vụ nên ở trạng thái tĩnh. Nhiệm vụ năng động nên được coi là không cần thiết. Đối với RTOS đơn giản, các

tác vụ sẽ thực thi trong cùng một không gian địa chỉ của kernel. Đối với RTOS có yêu cầu về độ tin cậy và bảo mật cao, các tác vụ phải chạy ở chế độ người dùng

trong các không gian địa chỉ ảo riêng biệt, nhưng hệ thống nên sử dụng sơ đồ ánh xạ địa chỉ ảo đơn giản nhất để có hiệu quả tốt hơn.

10.6.3 Lập lịch tác vụ

Lập kế hoạch nhiệm vụ phải được ưu tiên dựa trên mức độ ưu tiên của nhiệm vụ. Mặc dù RMS tĩnh và đơn giản hơn nhưng mức độ ưu tiên của nhiệm vụ phải dựa trên

EDF vì nó thực tế hơn về mặt đáp ứng thời hạn nhiệm vụ.

10.6.4 Công cụ đồng bộ hóa

Sử dụng mutex đơn giản với tính kế thừa ưu tiên để bảo vệ các vùng quan trọng. Sử dụng semaphore đếm để hợp tác nhiệm vụ. Để tăng tính linh hoạt, hãy sử dụng cờ

sự kiện để cho phép tác vụ chờ số lượng sự kiện khác nhau.

10.6.5 Giao tiếp nhiệm vụ

Sử dụng bộ nhớ dùng chung được bảo vệ bởi các đột biến đơn giản để liên lạc trực tiếp với tác vụ. Sử dụng các đường ống cho các tác vụ để chia sẻ luồng dữ liệu.

Sử dụng tính năng truyền tin nhắn đồng bộ cho các tác vụ để trao đổi tin nhắn.

10.6.6 Quản lý bộ nhớ

Tránh ánh xạ địa chỉ ảo nếu có thể. Cho phép các tác vụ phân bổ bộ nhớ theo các khối có kích thước cố định, nhưng không phải theo các khối có kích thước tùy ý.

Hỗ trợ kiểm tra tràn ngăn xếp trong quá trình phát triển nhưng không có trong hệ thống cuối cùng.

10.6.7 Hệ thống tập tin

Nếu cần hệ thống tệp, hãy triển khai hệ thống tệp dưới dạng đĩa RAM trong bộ nhớ. Tải hệ thống tệp từ SDC vào đĩa RAM khi hệ thống khởi động và chuyển mọi thay

đổi đối với hệ thống tệp trở lại SDC theo định kỳ.
Machine Translated by Google

10.6 Nguyên tắc thiết kế của RTOS 413

10.6.8 Truy tìm và gỡ lỗi

RTOS phải cung cấp các phương tiện theo dõi và gỡ lỗi để cho phép người dùng giám sát tiến trình của các tác vụ, ít nhất là trong

phát triển.

Trong các phần sau, chúng tôi sẽ trình bày cách thiết kế và triển khai hai RTOS, một dành cho hệ thống bộ xử lý đơn (UP)

và cái còn lại dành cho hệ thống đa bộ xử lý (MP).

10.7 RTOS bộ xử lý đơn (UP_RTOS)

UP_RTOS là hệ điều hành thời gian thực dành cho các hệ thống bộ xử lý đơn (UP). Nó dựa trên kernel UP hoàn toàn được ưu tiên

được phát triển ở Chap. 5 (Mục 5.14.3) nhưng với các phần mở rộng sau để hỗ trợ các ứng dụng thời gian thực.

(1). Lập lịch tác vụ ưu tiên theo mức độ ưu tiên của tác vụ (tĩnh)

(2). Hỗ trợ các ngắt lồng nhau

(3). Mutexes có tính kế thừa ưu tiên cho các vùng quan trọng và chia sẻ tài nguyên

(4). Semaphores có tính kế thừa ưu tiên để hợp tác quy trình

(5). Bộ nhớ dùng chung, đường dẫn và tin nhắn để liên lạc giữa các quá trình

(6). Tác vụ ghi nhật ký, ghi lại các hoạt động của tác vụ vào SDC để theo dõi và gỡ lỗi

Phần sau đây mô tả hạt nhân UP_RTOS.

10.7.1 Quản lý tác vụ trong UP_RTOS

UP_RTOS hỗ trợ nhiều tác vụ khác nhau. Số lượng tác vụ tối đa (NPROC) trong hệ thống là có thể định cấu hình

tham số, có thể được đặt thành giá trị phù hợp với nhu cầu. Nhiệm vụ có thể là tĩnh hoặc động. Khi sử dụng các tác vụ tĩnh, tất cả

các tác vụ được tạo trong quá trình khởi tạo hệ thống và chúng tồn tại vĩnh viễn trong hệ thống. Khi sử dụng tác vụ động, tác vụ có thể

được tạo ra theo yêu cầu và chúng chấm dứt sau khi hoàn thành công việc của mình. Mỗi tác vụ được tạo với mức độ ưu tiên tĩnh. Tất cả nhiệm vụ

thực thi trong cùng một không gian địa chỉ của kernel. Tạo tác vụ bằng API

chức năng kforkðint fðÞtask; ưu tiên intÞ;

tạo ra một tác vụ để thực thi task_function() với mức độ ưu tiên được chỉ định. Mỗi nhiệm vụ được thể hiện bằng một cấu trúc PROC.

#define NPROC 256

quy trình cấu trúc typedef{

struct proc *next; // con trỏ tới PROC tiếp theo

int *ksp; // lưu sp khi không chạy

int trạng thái; // trạng thái

int pid; // ID nhiệm vụ

int tạm ngừng; // tạm dừng thời gian

int sẵn sàng_thời gian; // giải phóng nhiệm vụ hoặc thời gian sẵn sàng

int sự ưu tiên; // mức độ ưu tiên hiệu quả

int sự ưu tiên; // mức độ ưu tiên thực sự

bột ngọt *mqueue // Hàng đợi tin nhắn

SEMAPHORE nmsg; // số lượng tin nhắn trong hàng đợi tin nhắn

mqlock MUTEX // khóa hàng đợi tin nhắn

SEMAPHORE wchild; // đợi con ZOMBIE

int kstack[SSIZE]; // Vùng ngăn xếp tác vụ 4KB đến 8KB


Machine Translated by Google

414 10 hệ điều hành nhúng thời gian thực

}THỦ TỤC;

Quy trình PROC[NPROC]; // Cấu trúc NPROC PROC

PROC *readyQueue; // hàng đợi sẵn sàng theo mức độ ưu tiên PROC //

PROC *đang chạy; con trỏ PROC đang chạy hiện tại

10.7.2 Đồng bộ hóa tác vụ trong UP_RTOS

UP_RTOS chỉ sử dụng chế độ ngủ/thức để đồng bộ hóa tác vụ trong đường ống. Nó sử dụng mutex và semaphore đếm để đồng bộ hóa tác
vụ chung. Mutexes được sử dụng làm khóa độc quyền để bảo vệ các khu vực quan trọng. Semaphores được sử dụng để hợp tác nhiệm vụ.
Cả hai đều được triển khai với tính năng kế thừa ưu tiên để ngăn chặn việc đảo ngược mức độ ưu tiên. Mỗi mutex đã có một
trường chủ sở hữu, trường này xác định tác vụ hiện tại giữ khóa mutex. Để hỗ trợ tính kế thừa ưu tiên trong các đèn hiệu, chúng
tôi sửa đổi cấu trúc đèn hiệu để chứa trường chủ sở hữu, trường này xác định tác vụ hiện tại chứa đèn hiệu. Để đơn giản, kế
thừa ưu tiên chỉ có một cấp độ, tức là nó chỉ áp dụng cho từng mutex hoặc semaphore chứ không có tính bắc cầu trong một chuỗi
các yêu cầu mutex hoặc semaphore. Việc mở rộng kế thừa ưu tiên cho các yêu cầu mutex hoặc semaphore lồng nhau được để lại dưới
dạng bài tập trong phần Sự cố.

10.7.3 Lập lịch tác vụ trong UP_RTOS

Lập kế hoạch nhiệm vụ được ưu tiên trước. Ưu tiên nhiệm vụ được thực hiện như sau. Đầu tiên, kernel sử dụng bộ đếm toàn cục
để theo dõi các mức lồng ghép ngắt. Khi nhập trình xử lý ngắt, bộ đếm sẽ tăng thêm 1. Khi thoát khỏi trình xử lý ngắt, bộ đếm sẽ
giảm đi 1, v.v. Chuyển đổi tác vụ không được phép nếu mức lồng ngắt khác 0.
Thứ hai, các thao tác duy nhất có thể khiến một tác vụ sẵn sàng chạy là tạo tác vụ, mutex_unlock và V trên các ẩn dụ.
Bất cứ khi nào một tác vụ sẵn sàng được nhập vào hàng đợi sẵn sàng, nó sẽ gọi hàm reschedule() để sắp xếp lại các tác vụ. Sau
đây cho thấy thuật toán ưu tiên nhiệm vụ.

/********* Thuật toán ưu tiên nhiệm vụ **********/

int intnest; // ngắt bộ đếm lồng nhau, ban đầu là 0

int swflag; // chuyển cờ nhiệm vụ

sắp xếp lại

if (readyQueue->priority > Running->priority){

if (intsest==0)// if không có trong trình xử lý ngắt

tswitch(); // ưu tiên tác vụ đang chạy ngay lập tức

khác{

swflag = 1; // trì hoãn quyền ưu tiên cho đến khi kết thúc ngắt

ngắt_handler_exit // chuyển tác vụ nếu swflag được đặt và kết thúc IRQ

if (!intnest && swflag)

tswtich() ở chế độ SVC;

Trong reschedule(), nếu tác vụ đang chạy hiện tại không còn có mức ưu tiên cao nhất và việc thực thi không nằm trong bất kỳ trình xử lý ngắt nào, thì

nó sẽ ưu tiên tác vụ đang chạy hiện tại ngay lập tức. Nếu quá trình thực thi vẫn nằm trong trình xử lý ngắt, nó sẽ đặt cờ tác vụ chuyển đổi, cờ này

sẽ trì hoãn việc chuyển đổi tác vụ cho đến khi tất cả quá trình xử lý ngắt lồng nhau kết thúc.

10.7.4 Giao tiếp tác vụ trong UP_RTOS

Nhân UP_RTOS cung cấp ba loại cơ chế để liên lạc giữa các tác vụ.
Machine Translated by Google

10.7 RTOS bộ xử lý đơn (UP_RTOS) 415

10.7.4.1 Bộ nhớ dùng chung


Khi hệ thống khởi động, nó sẽ khởi tạo một số cố định (32) vùng bộ nhớ 64KB, ví dụ từ 32MB đến 34MB, để liên lạc giữa
các tác vụ thông qua bộ nhớ dùng chung. Mỗi vùng bộ nhớ dùng chung được biểu diễn bằng một cấu trúc

#define NPID NPROC/sizeof(int)

cấu trúc shmem{

int procID[NPID]; // vectơ bit cho ID tác vụ NPROC

MUTEX mutex; // khóa mutex cho vùng chia sẻ // địa chỉ bắt

địa chỉ char *; đầu của vùng bộ nhớ

}shmem[32];

Khi hệ thống khởi động, nó khởi tạo cấu trúc shmem để chứa procID = {0}; mutex =

MỞ KHÓA; địa chỉ // chưa có tác vụ nào sử dụng shmem

= con trỏ tới vùng bộ nhớ // để có quyền truy cập độc quyền vào vùng chia sẻ

64KB duy nhất

Để sử dụng bộ nhớ dùng chung, trước tiên một tác vụ phải tự gắn nó vào cấu trúc shmem bằng cách

int shmem_attach(struct shmem *mp);

shmem_attach() ghi lại bit ID tác vụ trong procID (để kiểm tra khả năng truy cập) và trả về số lượng tác vụ hiện được gắn vào bộ nhớ dùng chung.

Sau khi gắn vào bộ nhớ dùng chung, các tác vụ có thể sử dụng

shmem_read( struct shmem *mp, char buf[ ], int nbytes);

shmem_write(struct shmem *mp, char buf[ ], int nbytes);

để đọc/ghi dữ liệu từ/đến bộ nhớ dùng chung. Các chức năng đọc/ghi chỉ đảm bảo mỗi thao tác đọc/ghi là nguyên tử (bằng khóa mutex của bộ nhớ dùng

chung). Định dạng dữ liệu và ý nghĩa của nội dung bộ nhớ dùng chung hoàn toàn do người dùng quyết định.

10.7.4.2 Ống cho luồng dữ liệu Đây là cơ chế

ống tương tự của Chap. 5 (Mục 5.13.2). Nó cho phép các tác vụ sử dụng các đường ống để đọc/ghi các luồng dữ liệu.

10.7.4.3 Hàng đợi tin nhắn Đây là cơ

chế truyền tin nhắn đồng bộ giống như Chap. 5 (Mục 5.13.4). Nó cho phép các nhiệm vụ giao tiếp bằng tin nhắn trao đổi. Như trong trường hợp bộ

nhớ dùng chung, người dùng có thể thiết kế định dạng và nội dung tin nhắn phù hợp với nhu cầu.

10.7.5 Bảo vệ các khu vực quan trọng

Trong UP_RTOS, tất cả các vùng quan trọng đều được bảo vệ bằng khóa mutex. Thứ tự khóa mutex luôn là một chiều, do đó bế tắc không bao giờ có thể

xảy ra trong hạt nhân UP_RTOS.

10.7.6 Hệ thống tệp và ghi nhật ký

Việc ghi nhật ký thường yêu cầu ghi thông tin vào tệp nhật ký trong hệ thống tệp, điều này có thể gây ra một khoảng thời gian trễ khác nhau cho

các tác vụ. Chúng tôi không thấy bất kỳ nhu cầu chính đáng nào đối với các tác vụ trong hệ thống thời gian thực để thực hiện các thao tác với tệp.

Để đơn giản và hiệu quả, chúng tôi triển khai ghi nhật ký bằng một tác vụ ghi nhật ký đặc biệt, tác vụ này nhận yêu cầu ghi nhật ký từ các tác vụ

khác bằng tin nhắn và ghi trực tiếp thông tin ghi nhật ký vào thiết bị lưu trữ, chẳng hạn như SDC. Khi hệ thống khởi động, nó sẽ tạo một tác vụ

ghi nhật ký có mức ưu tiên thấp thứ hai là 1 (cao hơn tác vụ không hoạt động ở mức ưu tiên 0). Các tác vụ khác sử dụng
Machine Translated by Google

416 10 hệ điều hành nhúng thời gian thực

log char ð Þ thông tin nhật ký

để ghi lại một dòng trong nhật ký. Thao tác ghi nhật ký sẽ gửi một thông báo đến tác vụ ghi nhật ký để định dạng thông tin nhật ký ở dạng

dấu thời gian: taskID: dòng

và ghi nó vào khối SDC (1KB) bất cứ khi nào nó chạy. Nó cũng ghi các dòng đã ghi vào cổng UART để cho phép người dùng xem các hoạt động ghi nhật

ký trực tuyến. Khi hệ thống chấm dứt, thông tin nhật ký có thể được truy xuất từ SDC để theo dõi và phân tích.

Chúng tôi trình bày cách triển khai hạt nhân UP_RTOS và chứng minh khả năng của nó bằng các chương trình ví dụ sau.

C10.1: UP_RTOS với các tác vụ định kỳ tĩnh và lập lịch luân chuyển C10.2: UP_RTOS với

các tác vụ định kỳ tĩnh và lập lịch ưu tiên C10.3: UP_RTOS với các tác vụ động và lập

lịch ưu tiên

10.7.7 UP_RTOS với các tác vụ định kỳ tĩnh và lập kế hoạch luân phiên

Ví dụ UP_RTOS đầu tiên, được ký hiệu là C10.1, thể hiện các tác vụ định kỳ tĩnh với lập kế hoạch quay vòng. Chúng tôi giả định rằng tất cả các

nhiệm vụ đều mang tính định kỳ với cùng khoảng thời gian, do đó có cùng mức độ ưu tiên theo thuật toán lập lịch RMS.

Là một hệ thống đơn bộ xử lý (UP), hạt nhân UP_RTOS duy trì một hàng đợi sẵn sàng duy nhất để lập lịch tác vụ. Trong ReadyQueue, các tác vụ được

sắp xếp theo mức độ ưu tiên. Các nhiệm vụ có cùng mức độ ưu tiên được sắp xếp theo thứ tự Nhập trước xuất trước (FIFO). Vì tất cả các nhiệm vụ

đều có cùng mức độ ưu tiên nên chúng được lên lịch thực hiện theo vòng tròn. Khi hệ thống khởi động, nó tạo ra 4 tác vụ tĩnh, tất cả đều thực thi

cùng một hàm taskCode() trong cùng một khoảng thời gian. Mỗi tác vụ thực thi một vòng lặp vô hạn, trong đó tác vụ đầu tiên tự chặn chính nó trên

một semaphore duy nhất (khởi tạo là 0). Một tác vụ bị chặn sẽ được cập nhật theo thời gian định kỳ theo khoảng thời gian thực hiện tác vụ. Khi một

tác vụ được bỏ chặn và sẵn sàng chạy, chúng tôi sẽ lấy thời gian sẵn sàng của tác vụ đó theo thời gian chung và ghi lại nó vào cấu trúc PROC của tác vụ.

Khi một tác vụ chạy, đầu tiên nó sẽ nhận được thời gian bắt đầu. Sau đó, nó thực hiện một số tính toán được mô phỏng bằng vòng lặp trễ. Khi kết

thúc vòng thực thi, mỗi tác vụ sẽ nhận được thời gian kết thúc và tính thời gian thực hiện cũng như thời hạn như sau.

Thời gian thực hiện Ci ¼ thời gian kết thúc thời gian bắt đầu;

Thời gian kết thúc Di ¼ thời gian kết thúc thời gian sẵn sàng;

Nó so sánh thời gian thực hiện nhiệm vụ với thời gian thực hiện nhiệm vụ để xem liệu nó có đáp ứng được thời hạn thực hiện hay không (bằng thời

gian thực hiện nhiệm vụ). Sau đó nó lặp lại vòng lặp một lần nữa. Sau đây liệt kê mã của chương trình C10.1. Để chương trình đơn giản, hệ thống

chỉ hỗ trợ các ngắt lồng nhau, lập lịch tác vụ ưu tiên, không có kế thừa ưu tiên sẽ được thực hiện sau này. (1).ts.s tệp của C10.1: Tính năng

chính của tệp ts.s là nó hỗ trợ các ngắt IRQ lồng nhau. Chuyển đổi tác vụ được trì hoãn cho đến khi quá trình xử lý ngắt lồng nhau kết thúc. Chi

tiết về quyền ưu tiên nhiệm vụ sẽ được giải thích sau.

/*************** tệp ts.s của C10.1 ****************/

.chữ

.code 32

.global vectơ_start, vectơ_end .global proc,

procsize .global tswitch, bộ lập

lịch, chạy .global int_off, int_on, khóa, mở

khóa .global swflag, intnest, int_end

.set vectorAddr, 0x10140030 // địa chỉ cơ sở vectơ VIC

reset_handler:

// đặt ngăn xếp SVC lên cấp cao nhất của proc[0].kstack

ldr r0, =proc

ldr r1, = kích thước

ldr r2, [r1, #0]


Machine Translated by Google

10.7 RTOS bộ xử lý đơn (UP_RTOS) 417

thêm r0, r0, r2

mov sp, r0

// sao chép bảng vectơ vào địa chỉ 0

bl copy_vectors

// vào chế độ IRQ, đặt ngăn xếp IRQ

msr cpsr, #0x92

ldr sp, =irq_stack_top

// gọi main() ở chế độ SVC khi bật IRQ: tất cả các tác vụ chạy ở chế độ SVC

msr cpsr, #0x13

bl chính

b.

irq_handler: // hỗ trợ các ngắt lồng nhau trong chế độ SVC

phụ lr, lr, #4

stmfd sp!, {r0-r12, lr}

bà r0, spsr

stmfd sp!, {r0} // đẩy SPSR

ldr r0, =intnest // r0->intnest

ldr r1, [r0]

thêm r1, #1

str r1, [r0] // intnest++

mov r1, sp // lấy irq sp vào r1

ldr sp, =irq_stack_top // đặt lại con trỏ ngăn xếp IRQ về đỉnh ngăn xếp IRQ

// chuyển sang chế độ SVC // để cho phép các IRQ lồng nhau: xóa nguồn IRQ

MSR cpsr, #0x93 // sang chế độ SVC với các ngắt TẮT

phụ sp, #60 // dec Chế độ SVC sp tăng 15 mục

mov r0, sp // r0=Đỉnh ngăn xếp SVC

// sao chép ngăn xếp IRQ vào ngăn xếp SVC

phim r3, #15 // 15 lần

sao chép_stack:

ldr r2, [r1], #4 // lấy một mục từ ngăn xếp IRQ

str r2, [r0], #4 // ghi vào kstack của Proc

phụ r3, #1 // sao chép 15 mục từ ngăn xếp IRQ sang kstack của PROC

cmp r3, #0

chúc mừng copy_stack

// đọc thanh ghi địa chỉ vector: PHẢI!!! nếu không thì không bị gián đoạn

ldr r1, =vectorAddr

ldr r0, [r1] // đọc thanh ghi vectorAddr tới ngắt ACK

stmfd sp!, {r0-r3, lr}

msr cpsr, #0x13 // vẫn ở chế độ SVC nhưng bật IRQ

bl irq_chandler // xử lý ngắt ở chế độ SVC, tắt IRQ

msr cpsr, #0x93

ldmfd sp!, {r0-r3, lr}

ldr r0, =intnest // kiểm tra mức độ tổ ngắt

ldr r1, [r0]

phụ r1, #1

str r1, [r0] // intnest--

cmp r1, #0 // if intnest != 0 => no_switch

không có gì

// intnest==0: KẾT THÚC IRQ: if swflag=1: chuyển đổi tác vụ

ldr r0, =swflag

ldr r0, [r0]

cmp r0, #0

tôi sẽ làm_switch // if swflag=0: không có chuyển đổi tác vụ

no_switch:

ldmfd sp!, {r0}


Machine Translated by Google

418 10 hệ điều hành nhúng thời gian thực

msr spsr, r0 // khôi phục SPSR

// irq_chandler() đã phát hành EOI

ldmfd sp!, {r0-r12, pc}^ // trả về qua ngăn xếp SVC

do_switch: // vẫn ở chế độ IRQ

bl endIRQ // hiển thị "ở cuối IRQ"

bl tswitch // gọi tswitch(): tiếp tục tới đây

// sẽ chuyển nhiệm vụ nên phải đưa ra EOI

ldr r1, =vectorAddr

str r0, [r1] // phát hành EOI

ldmfd sp!, {r0}

msr spsr, r0

ldmfd sp!, {r0-r12, pc}^ // trả về qua ngăn xếp SVC

chuyển đổi: // để chuyển đổi tác vụ ở chế độ SVC

// vô hiệu hóa các ngắt IRQ

bà r0, cpsr

orr r0, r0, #0x80 // đặt bit I thành CHECK các ngắt IRQ

msr cpsr, r0

stmfd sp!, {r0-r12, lr}

ldr r0, =đang chạy // r0=&đang chạy

ldr r1, [r0, #0] // r1->runningPROC

str sp, [r1, #4] // đang chạy->ksp = sp

lịch trình bl

ldr r0, = đang chạy

ldr r1, [r0, #0] // r1->runningPROC

ldr sp, [r1, #4]

// kích hoạt ngắt IRQ

bà r0, cpsr

bic r0, r0, #0x80 // bit xóa có nghĩa là ngắt IRQ UNMASK

msr cpsr, r0

ldmfd sp!, {r0-r12, pc}

// các hàm tiện ích: int_on/int_off/lock/unlock: KHÔNG hiển thị

vectơ_start:

PC LDR, reset_handler_addr

Máy tính LDR, undef_handler_addr

Máy tính LDR, swi_handler_addr

PC LDR, tìm nạp trước_abort_handler_addr

PC LDR, dữ liệu_abort_handler_addr

B .

Máy tính LDR, irq_handler_addr

PC LDR, fiq_handler_addr

đặt lại_handler_addr: .word reset_handler

undef_handler_addr: .word undef_handler

swi_handler_addr: .word swi_handler

prefetch_abort_handler_addr: .word prefetch_abort_handler

dữ liệu_abort_handler_addr: .word data_abort_handler

irq_handler_addr: .word irq_handler

fiq_handler_addr: .word fiq_handler

vectơ_end:

Giải thích về tệp ts.s:

Reset_handler: Như thường lệ, reset_handler là điểm vào. Đầu tiên, nó đặt con trỏ ngăn xếp chế độ SVC ở mức cao nhất của

proc[0] và sao chép bảng vectơ tới địa chỉ 0. Tiếp theo, nó chuyển sang chế độ IRQ để đặt ngăn xếp chế độ IRQ. Sau đó nó gọi main

() ở chế độ SVC. Trong quá trình vận hành hệ thống, tất cả các tác vụ đều chạy ở chế độ SVC trên cùng một địa chỉ của kernel.
Machine Translated by Google

10.7 RTOS bộ xử lý đơn (UP_RTOS) 419

Irq_handler: Chuyển đổi tác vụ thường được kích hoạt bởi các ngắt. Điều này có thể khiến tác vụ bị chặn sẵn sàng chạy. Do đó, irq_handler là

đoạn mã lắp ráp quan trọng nhất có liên quan đến quyền ưu tiên xử lý. Vì vậy, chúng tôi chỉ tập trung vào mã irq_handler. Như đã chỉ ra ở Chap.

2, CPU ARM không thể xử lý các ngắt lồng nhau ở chế độ IRQ. Việc xử lý các ngắt lồng nhau phải được thực hiện ở chế độ đặc quyền khác. Để hỗ trợ

việc ưu tiên quá trình do bị gián đoạn, chúng tôi chọn xử lý các ngắt IRQ ở chế độ SVC. Thuật toán của irq_handler có thể được mô tả tốt nhất

bằng thuật toán sau.

/********* Thuật toán xử lý IRQ để giành quyền ưu tiên hoàn thành nhiệm vụ ********/

(1). Khi vào, điều chỉnh return lr; lưu ngữ cảnh, bao gồm spsr, trong ngăn xếp IRQ (2).

Tăng bộ đếm lồng nhau ngắt lên 1 (3). Chuyển sang chế

độ SVC với các ngắt bị tắt (4). Chuyển ngữ cảnh từ ngăn xếp

IRQ sang ngăn xếp SVC của Proc; đặt lại ngăn xếp IRQ (5). Xác nhận và xóa nguồn ngắt

(ngăn chặn các vòng lặp vô hạn từ cùng một nguồn ngắt)

(6). Kích hoạt ngắt IRQ; lưu các thanh ghi làm việc trong ngăn xếp SVC (7).

Gọi ISR ở chế độ SVC có bật ngắt IRQ (8). (trở về từ ISR): vô hiệu

hóa các ngắt IRQ; khôi phục các thanh ghi làm việc (9). Giảm bộ đếm lồng ghép ngắt đi

1 (10). Nếu vẫn còn bên trong trình xử lý ngắt (bộ

đếm khác 0): goto no_switch (11). (kết thúc IRQ): nếu swflag được đặt: goto

do_switch (12). no_switch: trả về qua ngữ cảnh đã lưu trong

ngăn xếp SVC (13). do_switch: ghi EOI vào bộ điều khiển ngắt; gọi

tswitch() để chuyển tác vụ (14). (khi tác vụ đã tắt tiếp tục): quay lại qua ngữ cảnh ngắt đã

lưu trong ngăn xếp SVC

(2). Tệp uart.c: Trình điều khiển UART: chỉ dành cho đầu ra bằng các ngắt TX

(3). vid.c file: Trình điều khiển LCD: CÙNG như trước ngoại trừ bộ đệm khung ở mức 4MB

(4). file time.c: Sử dụng clock0 để kích hoạt các tác vụ theo định kỳ

/** tập tin time.c của C10.1 **/

#define TLOAD 0x0

#define TVALUE 0x1

#define TCNTL 0x2

#define TINTCLR 0x3

#xác định TRIS 0x4

#define TMIS 0x5

#define TBGLOAD 0x6

bộ đếm thời gian cấu trúc typedef {

u32 *cơ sở; // địa chỉ cơ sở của bộ định thời

int tích tắc, hh, mm, ss; // mỗi vùng dữ liệu hẹn giờ

đồng hồ char[16];

}Đồng hồ hẹn giờ;

Hẹn giờ hẹn giờ [4]; // 4 bộ định thời, chỉ sử dụng bộ định thời0

làm trống bộ đếm thời gian_init()

int tôi;

THỜI GIAN *tp;

printf("timer_init(): ");

gtime = 0;

for (i=0; i<4; i++){ // 4 bộ định thời nhưng chỉ sử dụng bộ định thời0

tp = &timer[i];

if (i==0) tp->base = (u32 *)0x101E2000;

if (i==1) tp->base = (u32 *)0x101E2020;

if (i==2) tp->base = (u32 *)0x101E3000;

if (i==3) tp->base = (u32 *)0x101E3020;

*(tp->base+TLOAD) = 0x0; // cài lại


Machine Translated by Google

420 10 hệ điều hành nhúng thời gian thực

*(tp->base+TVALUE)= 0xFFFFFFFF;

*(tp->base+TRIS) = 0x0;

*(tp->base+TMIS) = 0x0;

*(tp->base+TLOAD) = 0x100;

//0x62=|011-0000=|NOTEn|Pe|IntE|-|scal=00|32-bit|0=wrap|

*(tp->base+TCNTL) = 0x62;

*(tp->base+TBGLOAD) = 0xF00; // đếm thời gian

tp->tick = tp->hh = tp->mm = tp->ss = 0;

strcpy((char *)tp->clock, "00:00:00");

void time_handler(int n)

int tôi;

THỜI GIAN *t = &timer[n];

gtime++; // tăng thời gian toàn cầu

t->đánh dấu++; // cho đồng hồ treo tường địa phương

nếu (t->tick >= 64){

t->đánh dấu=0; t->ss++;

nếu (t->ss == 60){

t->ss=0; t->mm++;

nếu (t->mm==60){

t->mm=0; t->hh++;

// hiển thị đồng hồ treo tường

if (t->tick == 0){ // hiển thị đồng hồ treo tường

cho (i=0; i<8; i++){

unkpchar(t->clock[i], 0, 70+i);

t->clock[7]='0'+(t->ss%10); t->clock[6]='0'+(t->ss/10);

t->clock[4]='0'+(t->mm%10); t->đồng hồ[3]='0'+(t->mm/10);

t->clock[1]='0'+(t->hh%10); t->clock[0]='0'+(t->hh/10);

cho (i=0; i<8; i++){

kpchar(t->clock[i], 0, 70+i);

// bỏ chặn nhiệm vụ theo thời gian

if ((gtime % Period)==0){ // chu kỳ = N*T tích tắc hẹn giờ

cho (i=1; i<=4; i++){

V(&ss[i]); // kích hoạt nhiệm vụ i

proc[i].start_time = gtime; // thời gian tác vụ sẵn sàng chạy

hẹn giờ_clearInterrupt(n);

void time_start(int n) // time_start(0), 1, v.v.

THỜI GIAN *tp = &timer[n];

printf("timer_start %d\n", n); *(tp->base+TCNTL)

|= 0x80; // đặt bit kích hoạt 7

}
Machine Translated by Google

10.7 RTOS bộ xử lý đơn (UP_RTOS) 421

int time_clearInterrupt(int n) // time_start(0), 1, v.v.

THỜI GIAN *tp = &timer[n];

*(tp->base+TINTCLR) = 0xFFFFFFFF;

(5). Tệp hạt nhân của C10.1:

/*********** file pv.c của C10.1 *************/

PROC bên ngoài *đang chạy;

PROC bên ngoài *readyQueue;

swflag int bên ngoài;

intnest bên ngoài;

int P(struct semaphore *s) // không có quyền kế thừa ưu tiên

int SR = int_off();

s->giá trị--;

if (s->value < 0){ đang chạy-

>trạng thái = BLOCK; enqueue(&s-

>queue, đang chạy);

int_on(SR);

tswitch();

int_on(SR);

int V(struct semaphore *s) // không có ưu tiên kế thừa

PROC *p; int cpsr;

int SR = int_off();

s->giá trị++;

nếu (s->giá trị <= 0){

p = dequeue(&s->queue);

p->trạng thái = SẴN SÀNG;

enqueue(&readyQueue, p);

printf("V up task%d pri=%d; đang chạy pri=%d\n",

p->pid, p->ưu tiên, chạy->ưu tiên);

sắp xếp lại(); // có thể chiếm trước tác vụ đang chạy

int_on(SR);

/****************** tệp kernel.c của C10.1 *************/

#define NPROC 256

PROC proc[NPROC], *đang chạy, *freeList, *readyQueue;

int procsize = sizeof(PROC);

int swflag = 0; // chuyển cờ nhiệm vụ

int intnest; // ngắt mức lồng nhau

int kernel_init()

int tôi, j;

PROC *p;

kprintf("kernel_init()\n");

cho (i=0; i<NPROC; i++){

p = &proc[i];

p->pid = tôi;
Machine Translated by Google

422 10 hệ điều hành nhúng thời gian thực

p->trạng thái = SẴN SÀNG;

p->run_time = 0;

p->tiếp theo = p + 1;

proc[NPROC-1].next = 0;

freeList = &proc[0];

Danh sách ngủ = 0;

hàng đợi sẵn sàng = 0;

intnest = 0;

đang chạy = getproc(&freeList); // tạo và chạy P0

đang chạy->ưu tiên = 0;

printf("đang chạy = %d\n", đang chạy->pid);

bộ lập lịch int()

printf("task%d switch task: ", Running->pid);

if (đang chạy->trạng thái==SẴN SÀNG)

enqueue(&readyQueue, đang chạy);

printQ(readyQueue);

đang chạy = dequeue(&readyQueue);

printf("lần chạy tiếp theo = task%d pri=%d realpri=%d\n",

đang chạy->pid, đang chạy->ưu tiên, đang chạy->realPriority);

màu = ĐỎ+đang chạy->pid;

swflag = 0;

int reschedule() // được gọi từ bên trong V() với IRQ bị tắt

if (readyQueue &&readyQueue->priority > Running->priority){

nếu (intnest==0){

printf("task%d TRƯỚC nhiệm vụ%d NGAY LẬP TỨC\n", ReadyQueue->pid,

đang chạy-> pid);

tswitch();

khác{

printf("task%d TRÌ HOÃN TRƯỚC nhiệm vụ%d ", ReadyQueue->pid,

đang chạy-> pid);

swflag = 1; // IRQ bị vô hiệu hóa nên không cần khóa/mở khóa

// kfork() tạo một tác vụ mới và nhập vào ReadyQueue

PROC *kfork(int func, int ưu tiên)

int tôi;

PROC *p = getproc(&freeList);

nếu (p==0){

kprintf("kfork thất bại\n");

trả về (PROC *)0;

p->ppid = đang chạy->pid;

p->cha mẹ = đang chạy;

p->trạng thái = SẴN SÀNG;

p->realPriority = p->priority = ưu tiên;


Machine Translated by Google

10.7 RTOS bộ xử lý đơn (UP_RTOS) 423

p->run_time = 0;

p->ready_time = 0;

// đặt kstack ở chế độ tiếp tục để thực thi func()

cho (i=1; i<15; i++)

p->kstack[SSIZE-i] = 0;

p->kstack[SSIZE-1] = (int)func;

p->ksp = &(p->kstack[SSIZE-14]);

enqueue(&readyQueue, p);

printf("task%d tạo một task con%d\n", Running->pid, p->pid);

sắp xếp lại();

trả lại p;

/********************* tập tin tc của C10.1 ****************/

// đặt T=5 tích tắc hẹn giờ, thời gian thực hiện nhiệm vụ = 8*T

#xác định T 5

#xác định chu kỳ 8*T

#include "type.h"

#include "string.c"

#include "queue.c"

#include "pv.c"

#include "uart.c"

#include "vid.c"

#include "ngoại lệ.c"

#include "kernel.c"

#include "timer.c"

// toàn cầu

cấu trúc semaphore ss[5]; // các ngữ nghĩa cho tác vụ cần chặn

u32 gtime dễ bay hơi; // giờ toàn cầu

UART *up0, *up1;

int tcount = 0; // số lần ngắt hẹn giờ ở IRQ thấp

void copy_vectors(void) { // giống như trước }

int enterint() // cho các IRQ lồng nhau: xóa nguồn ngắt

trạng thái int, usstatus, scode;

trạng thái = *((int *)(VIC_BASE_ADDR)); // đọc thanh ghi trạng thái

if (trạng thái & (1<<4)){ // hẹn giờ0 tại IRQ 4

tcount++;

if (trạng thái & (1<<12)){ // uart0 tại IRQ 12

ustatus = *(up0->base + UDS); // đọc thanh ghi UDS

if (trạng thái & (1<<13)){ // uart1 tại IRQ 13

ustatus = *(up1->base + UDS); // đọc thanh ghi UDS

int endIRQ() { printf("cho đến KẾT THÚC IRQ\n"); }

int int_end(){ printf("chuyển tác vụ ở cuối IRQ\n"); }

// sử dụng các ngắt có vectơ của PL190

bộ đếm thời gian trống0_handler()

bộ đếm thời gian_handler(0);

}
Machine Translated by Google

424 10 hệ điều hành nhúng thời gian thực

void uart0_handler()

uart_handler(&uart[0]);

void uart1_handler()

uart_handler(&uart[1]);

int vectorInt_init()

printf("vectorInterrupt_init()\n");

*((int *)(VIC_BASE_ADDR+0x100)) = (int)timer0_handler;

*((int *)(VIC_BASE_ADDR+0x104)) = (int)uart0_handler;

*((int *)(VIC_BASE_ADDR+0x108)) = (int)uart1_handler; //(2). ghi vào intControlRegs

= E=1|IRQ# = 1xxxxx *((int *)(VIC_BASE_ADDR+0x200)) = 0x24; //0100100 tại IRQ 4

*((int *)(VIC_BASE_ADDR+0x204)) = 0x2C; //0101100 tại IRQ 12

*((int *)(VIC_BASE_ADDR+0x208)) = 0x2D; //0101101 tại IRQ 13

// ghi 32-bit 0 vào IntSelectReg để tạo ra các ngắt IRQ

*((int *)(VIC_BASE_ADDR+0x0C)) = 0;

khoảng trống irq_chandler()

int (*f)(); // f là con trỏ hàm

f =(void *)*((int *)(VIC_BASE_ADDR+0x30)); // lấy địa chỉ ISR

f(); // gọi hàm ISR

*((int *)(VIC_BASE_ADDR+0x30)) = 1; // ghi vào vectorAddr dưới dạng EOI

độ trễ int (int pid) // vòng lặp trễ: mô phỏng thời gian tính toán tác vụ

int tôi, j;

cho (i=0; i<1000; i++){

// có thể sử dụng pid cho thời gian trễ tác vụ khác nhau

cho (j=0; j<1000; j++); // thay đổi dòng này cho các giao dịch khác nhau

/************ Nhiệm vụ định kỳ tĩnh ***********/

int taskCode()

int pid = đang chạy->pid;

u32 time1, time2, t, Ready_time, Complete_time;

trong khi(1){

kho a(); // đặt lại nhiệm vụ semaphore và Ready_time

ss[running->pid].value = 0;

ss[running->pid].queue = 0;

đang chạy->start_time = 0;

mở khóa();

// chặn trên semaphore trên mỗi proc cho đến khi được tính thời gian

P(&ss[running->pid]);

Ready_time = đang chạy->start_time;

printf("%dready=%d", pid,ready_time); // tới màn hình LCD

uprintf("%dready=%d", pid,ready_time); // tới UART0

thời gian1 = g thời gian;


Machine Translated by Google

10.7 RTOS bộ xử lý đơn (UP_RTOS) 425

printf("bắt đầu=%d", time1);

uprintf("start=%d", time1);

độ trễ (pid);

time2 = gtime;

t = thời gian2 - thời gian1;

Complete_time = time2 – Ready_time;

printf("end=%d%d[%d %d]", time2, t, Ready_time, Complete_time);

uprintf("end=%d%d[%d %d]", time2, t, Ready_time, Complete_time);

if (complete_time > Period){ // nếu nhiệm vụ bị trễ thời hạn

printf(" Hoảng loạn: lỡ thời hạn!\n");

uprintf(" PANIC: lỡ thời hạn!\n");

khác{

printf("Được\n");

uprintf("OK\n");

int chính()

int tôi;

PROC *p;

màu = TRẮNG;

fbuf_init();

uart_init();

up0 = &uart[0];

up1 = &uart[1];

kprintf("Chào mừng đến với UP_RTOS trong ARM\n");

/* kích hoạt ngắt hẹn giờ0,1, uart0,1 */

VIC_INTENABLE = 0;

VIC_INTENABLE |= (1<<4); // bộ đếm thời gian0,1 ở bit4

VIC_INTENABLE |= (1<<12); // UART0 tại bit12

VIC_INTENABLE |= (1<<13); // UART2 tại bit13

vectorInt_init();

bộ đếm thời gian_init();

bộ đếm thời gian_start(0);

kernel_init();

printf("P0 kfork task\n"); // tạo 4 tác vụ

cho (i=1; i<=4; i++){

ss[i].value = 0; // khởi tạo các ngữ nghĩa tác vụ

ss[i].queue = (PROC *)0;

kfork((int)taskCode, 1); // tất cả mức độ ưu tiên CÙNG = 1

mở khóa();

trong khi(1){ // vòng lặp nhiệm vụ nhàn rỗi P0

nếu (sẵn sàng)

tswitch();

Phân tích thời hạn của hệ thống C10.1: Với 4 nhiệm vụ có thời gian bằng nhau, theo điều kiện lập lịch của RMS, tất cả
các nhiệm vụ sẽ có thể đáp ứng được thời hạn nếu

ð C1 þ C2 þ C3 þ C4 = chu kỳ\4 ð 2
QUẦN QUÈ ð 0:7568
Þ¼ Þ 1=4 1 ð10:1Þ
Machine Translated by Google

426 10 hệ điều hành nhúng thời gian thực

Hình 10.1 UP_RTOS với lập lịch quay vòng

Nếu chúng ta giả định thời gian tính toán của tất cả các tác vụ Ci=C là như nhau thì điều kiện (10.1) trở thành

C=thời gian\0:189 ð10:2Þ

Hình 10.1 cho thấy kết quả đầu ra mẫu khi chạy chương trình ví dụ C10.1. Hình vẽ cho thấy thời gian tính toán từng tác vụ riêng lẻ thay đổi từ 3 đến 5

tích tắc hẹn giờ. Thời gian tính toán tác vụ bao gồm chi phí do xử lý gián đoạn bởi cả ngắt hẹn giờ và ngắt I/O, cũng như thời gian chuyển đổi và lập lịch

tác vụ. Trong chương trình thử nghiệm C10.1, chúng tôi đặt tất cả thời gian tính toán tác vụ thành C = 5 tích tắc hẹn giờ và khoảng thời gian tác vụ = 8*C,

sao cho C/chu kỳ = 1/8=0,125, nằm trong giới hạn khả năng lập lịch của RMS. Trong trường hợp này, tất cả các nhiệm vụ thực sự có thể đáp ứng được thời hạn,

như Hình 10.1 cho thấy.

Điều kiện (10.2) gợi ý rằng chúng ta kỳ vọng tất cả các nhiệm vụ sẽ hoàn thành đúng thời hạn nếu C/thời gian < 0,189. Tuy nhiên, kết quả thử nghiệm lại

cho thấy điều ngược lại. Ví dụ: nếu chúng ta đặt khoảng thời gian của nhiệm vụ thành 7*C (C/chu kỳ = 0,1428), vẫn nằm trong giới hạn RMS, một số nhiệm vụ

sẽ bắt đầu trễ thời hạn, như được hiển thị trong Hình 10.2.

Trên thực tế, kết quả thử nghiệm cho thấy, với chu kỳ = 6*C (C/chu kỳ = 0,167), hầu như tất cả các công việc sẽ bị trễ thời hạn.

Ví dụ cho thấy rằng, mặc dù dễ dàng thiết kế và triển khai hạt nhân ROTS để hỗ trợ các tác vụ định kỳ nhưng không có gì đảm bảo rằng tác vụ đó có thể đáp

ứng đúng thời hạn ngay cả khi hệ thống tuân thủ nghiêm ngặt các nguyên tắc của RMS.

10.7.8 UP_RTOS với các tác vụ định kỳ tĩnh và lập kế hoạch ưu tiên

Ví dụ thứ hai về UP_RTOS, ký hiệu là C10.2, thể hiện các tác vụ định kỳ tĩnh với việc lập lịch tác vụ ưu tiên.

Khi hệ thống khởi động, nó sẽ tạo 4 tác vụ tĩnh với các khoảng thời gian khác nhau. Giống như trong RMS, mỗi tác vụ được gán mức ưu tiên tĩnh tỷ lệ nghịch

với chu kỳ của nó. Cụ thể, các khoảng thời gian và mức độ ưu tiên của nhiệm vụ được chỉ định như sau, trong đó các khoảng thời gian của nhiệm vụ được

tính bằng đơn vị T tích tắc của bộ đếm thời gian.


Machine Translated by Google

10.7 RTOS bộ xử lý đơn (UP_RTOS) 427

Hình 10.2 Minh họa các nhiệm vụ thiếu thời hạn

Nhiệm vụ Giai đoạn Sự ưu tiên


------- --------- ---------

P1 5T 1

P2 4T 2

P3 3T 3

P4 2T 4
-------------------------------

Mỗi tác vụ thực thi một vòng lặp vô hạn, trong đó trước tiên nó tự chặn chính nó trên một semaphore duy nhất được khởi tạo bằng 0. Mỗi tác vụ bị chặn

sẽ được V-ed up theo định kỳ theo thời gian thực hiện nhiệm vụ. Như trong chương trình C10.1, thời gian sẵn sàng của một tác vụ là

thời điểm tác vụ sẵn sàng chạy, tức là khi nó được kích hoạt bởi bộ đếm thời gian hoặc khi nó được nhập vào hàng đợi sẵn sàng.

Khi một tác vụ chạy, nó sẽ lấy thời gian bắt đầu làm thời gian chung. Sau đó, nó thực hiện một số tính toán, được mô phỏng bằng độ trễ

vòng. Khi kết thúc vòng thực thi, mỗi tác vụ sẽ nhận được thời gian kết thúc và kiểm tra xem nó có đáp ứng đúng thời hạn hay không (bằng với nhiệm vụ

Giai đoạn). Sau đó nó lặp lại vòng lặp một lần nữa. Sau đây là mã của chương trình C10.2. Để cho ngắn gọn, chúng tôi chỉ
hiển thị mã sửa đổi của C10.2.

(1). ts.s của C10.2. Tương tự như trong C10.1.

(2). Tệp uart.c: Trình điều khiển UART: chỉ dành cho đầu ra bằng các ngắt TX

(3). file vid.c: Trình điều khiển LCD: CÙNG như trước ngoại trừ bộ đệm khung ở mức 4MB

(4). Tệp hẹn giờ.c: Trong trình xử lý ngắt hẹn giờ, các tác vụ được sắp xếp theo chu kỳ tác vụ. Nó cũng đặt thời gian sẵn sàng của mỗi tác vụ

khi tác vụ được nhập vào hàng đợi sẵn sàng.

/** tập tin time.c của C10.2 **/

#define TLOAD 0x0


Machine Translated by Google

428 10 hệ điều hành nhúng thời gian thực

#define TVALUE 0x1

#define TCNTL 0x2

#define TINTCLR 0x3

#xác định TRIS 0x4

#define TMIS 0x5

#define TBGLOAD 0x6

bộ đếm thời gian cấu trúc typedef {

u32 *cơ sở; // địa chỉ cơ sở của bộ định thời

int tích tắc, hh, mm, ss; // mỗi vùng dữ liệu hẹn giờ

đồng hồ char[16];

}Đồng hồ hẹn giờ;

Hẹn giờ hẹn giờ [4]; // 4 bộ định thời, chỉ sử dụng bộ định thời0

làm trống bộ đếm thời gian_init()

int tôi;

THỜI GIAN *tp;

printf("timer_init(): ");

gtime = 0;

for (i=0; i<4; i++){ // 4 bộ định thời nhưng chỉ sử dụng bộ định thời0

tp = &timer[i];

if (i==0) tp->base = (u32 *)0x101E2000;

if (i==1) tp->base = (u32 *)0x101E2020;

if (i==2) tp->base = (u32 *)0x101E3000;

if (i==3) tp->base = (u32 *)0x101E3020;

*(tp->base+TLOAD) = 0x0; // cài lại

*(tp->base+TVALUE)= 0xFFFFFFFF;

*(tp->base+TRIS) = 0x0;

*(tp->base+TMIS) = 0x0;

*(tp->base+TLOAD) = 0x100; //0x62=|

011-0000=|NOTEn|Pe|IntE|-|scal=00|32-bit|0=wrap| *(tp->base+TCNTL) = 0x62; *(tp-

>base+TBGLOAD) = 0xF00; // đếm thời

gian tp->tick = tp->hh = tp->mm = tp->ss = 0; strcpy((char *)tp-

>clock, "00:00:00");

void time_handler(int n)

int tôi;

THỜI GIAN *t = &timer[n];

gtime++; // tăng thời gian toàn cầu

t->đánh dấu++; // cho đồng hồ treo tường địa phương

nếu (t->tick >= 64){

t->đánh dấu=0; t->ss++;

nếu (t->ss == 60){

t->ss=0; t->mm++;

nếu (t->mm==60){

t->mm=0; t->hh++;

if (t->tick == 0){ // hiển thị đồng hồ treo tường

cho (i=0; i<8; i++){

unkpchar(t->clock[i], 0, 70+i);

}
Machine Translated by Google

10.7 RTOS bộ xử lý đơn (UP_RTOS) 429

t->clock[7]='0'+(t->ss%10); t->clock[6]='0'+(t->ss/10);

t->clock[4]='0'+(t->mm%10); t->đồng hồ[3]='0'+(t->mm/10);

t->clock[1]='0'+(t->hh%10); t->clock[0]='0'+(t->hh/10);

cho (i=0; i<8; i++){

kpchar(t->clock[i], 0, 70+i);

if ((gtime % (2*T))==0){ // kích hoạt P4 cứ sau 2T

V(&ss[4]);

proc[4].ready_time = gtime;

if ((gtime % (3*T))==0){ // kích hoạt P3 cứ sau 3T

V(&ss[3]);

proc[3].ready_time = gtime;

if ((gtime % (4*T))==0){ // kích hoạt P2 cứ sau 4T

V(&ss[2]);

proc[2].ready_time = 0;

if ((gtime % (5*T))==0){ // kích hoạt P1 cứ sau 5T

V(&ss[1]);

proc[1].ready_time = gtime;

hẹn giờ_clearInterrupt(n);

void time_start(int n) // time_start(0), 1, v.v.

THỜI GIAN *tp = &timer[n];

printf("timer_start %d\n", n); *(tp-

>base+TCNTL) |= 0x80; // đặt bit kích hoạt 7

int time_clearInterrupt(int n) // time_start(0), 1, v.v.

THỜI GIAN *tp = &timer[n]; *(tp-

>base+TINTCLR) = 0xFFFFFFFF;

(5). File kernel của C10.2: Tương tự như trong C10.1

(6). tập tin tc của C10.2

/****************** tệp tc của C10.2 ****************/

#xác định T 32

#include "type.h"

#include "string.c"

#include "queue.c"

#include "pv.c"

#include "uart.c"

#include "vid.c"

#include "ngoại lệ.c"

#include "kernel.c"

#include "timer.c"

// toàn cầu
Machine Translated by Google

430 10 hệ điều hành nhúng thời gian thực

cấu trúc semaphore ss[5]; // các ngữ nghĩa cho tác vụ cần chặn

u32 gtime dễ bay hơi; // giờ toàn cầu

UART *up0, *up1;

int tcount = 0; void // số lần ngắt hẹn giờ ở IRQ thấp

copy_vectors(void) { // giống như trước }

int enterint() // cho các IRQ lồng nhau: xóa nguồn ngắt

trạng thái int, usstatus, scode;

trạng thái = *((int *)(VIC_BASE_ADDR)); // đọc thanh ghi trạng thái

if (trạng thái & (1<<4)){ // hẹn giờ0 tại IRQ 4

tcount++;

if (trạng thái & (1<<12)){ // uart0 tại IRQ 12

ustatus = *(up0->base + UDS); // đọc thanh ghi UDS

if (trạng thái & (1<<13)){ // uart1 tại IRQ 13

ustatus = *(up1->base + UDS); // đọc thanh ghi UDS

int endIRQ() { printf("cho đến KẾT THÚC IRQ\n"); }

int int_end(){ printf("chuyển tác vụ ở cuối IRQ\n"); }

// sử dụng các ngắt có vectơ của PL190

bộ đếm thời gian trống0_handler()

bộ đếm thời gian_handler(0);

void uart0_handler()

uart_handler(&uart[0]);

void uart1_handler()

uart_handler(&uart[1]);

int vectorInt_init()

printf("vectorInterrupt_init()\n");

*((int *)(VIC_BASE_ADDR+0x100)) = (int)timer0_handler;

*((int *)(VIC_BASE_ADDR+0x104)) = (int)uart0_handler;

*((int *)(VIC_BASE_ADDR+0x108)) = (int)uart1_handler;

//(2). ghi vào intControlRegs = E=1|IRQ# = 1xxxxx

*((int *)(VIC_BASE_ADDR+0x200)) = 0x24; //0100100 tại IRQ 4

*((int *)(VIC_BASE_ADDR+0x204)) = 0x2C; //0101100 tại IRQ 12

*((int *)(VIC_BASE_ADDR+0x208)) = 0x2D; //0101101 tại IRQ 13

// ghi 32-bit 0 vào IntSelectReg để tạo ra các ngắt IRQ

*((int *)(VIC_BASE_ADDR+0x0C)) = 0;

khoảng trống irq_chandler()

int (*f)(); // f là con trỏ hàm

f =(void *)*((int *)(VIC_BASE_ADDR+0x30)); // lấy địa chỉ ISR

f(); // gọi hàm ISR

*((int *)(VIC_BASE_ADDR+0x30)) = 1; // ghi vào vectorAddr dưới dạng EOI

}
Machine Translated by Google

10.7 RTOS bộ xử lý đơn (UP_RTOS) 431

int delay(int pid) // vòng lặp trễ: mô phỏng thời gian tính toán tác vụ

int tôi, j;

cho (i=0; i<1000; i++){

cho (j=0; j<1000; j++); // thay đổi dòng này cho các giao dịch khác nhau

int taskCode()

int pid = đang chạy->pid;

u32 time1, time2, t, chu kỳ, thời gian sẵn sàng, ctime;

khoảng thời gian = (6 - đang chạy->pid)*T; // thời gian thực hiện nhiệm vụ=5,4,3,2

trong khi(1){

kho a();

ss[running->pid].value = 0;

ss[running->pid].queue = 0;

proc[pid].ready_time = 0;

mở khóa();

P(&ss[running->pid]); // chặn trên semaphore; Ved up theo thời kỳ

thời gian sẵn sàng = đang chạy->ready_time;

thời gian1 = g thời gian;

printf("P%dready=%dstart=%d", pid, thời gian sẵn sàng, time1);

uprintf("P%dready=%dstart=%d", pid, thời gian sẵn sàng, time1);

độ trễ (pid); // mô phỏng thời gian tính toán tác vụ

time2 = gtime;

t = thời gian2 - thời gian1;

ctime = time2 - thời gian sẵn sàng; // thời gian hoàn thành nhiệm vụ

printf("end=%d%d[%d%d]", time2, t, Period, ctime);

uprintf("end=%d%d[%d%d]", time2, t, chu kỳ, ctime);

nếu (dtime > dấu chấm){

printf(" Hoảng loạn! lỡ thời hạn!\n"); // tới màn hình LCD

uprintf(" Hoảng loạn! lỡ thời hạn!\n"); // tới UART0

khác{

printf("Được\n");

uprintf("OK\n");

int chính()

int tôi;

PROC *p;

fbuf_init();

uart_init();

up0 = &uart[0];

up1 = &uart[1];

kprintf("Chào mừng đến với UP_RTOS trong ARM\n");

/* kích hoạt ngắt hẹn giờ0,1, uart0,1 SIC */

VIC_INTENABLE = 0;

VIC_INTENABLE |= (1<<4); // bộ đếm thời gian0,1 ở bit4

VIC_INTENABLE |= (1<<5); // bộ đếm thời gian2,3

VIC_INTENABLE |= (1<<12); // UART0 tại bit12


Machine Translated by Google

432 10 hệ điều hành nhúng thời gian thực

VIC_INTENABLE |= (1<<13); // UART2 tại bit13

VIC_INTENABLE |= (1<<31); // SIC tới IRQ31 vectorInt_init() của VIC;

bộ đếm thời gian_init();

bộ đếm thời gian_start(0);

kernel_init();

kprintf("P0 nhiệm vụ kfork\n");

cho (i=1; i<=4; i++){ // tạo 4 tác vụ tĩnh

ss[i].value = 0;

ss[i].queue = (PROC *)0;

kfork((int)taskCode, i); // mức độ ưu tiên = 1,2,3,4

printQ(readyQueue);

mở khóa();

while(1){ // vòng lặp P0

nếu (sẵn sàng)

tswitch();

Phân tích thời hạn của hệ thống C10.2: Với 4 nhiệm vụ và khoảng thời gian nhiệm vụ từ 2T đến 5T, chúng ta có thể rút ra giới hạn trên về thời gian

tính toán nhiệm vụ như sau. Giả sử Ci là thời gian tính toán của taski. Theo điều kiện về khả năng lập lịch trình của RMS, chúng tôi hy vọng tất cả các

nhiệm vụ có thể đáp ứng thời hạn nếu

C1/5T + C2/4T + C3/3T + C4/2T < 4(2**(1/4) – 1)

hoặc 12C1 + 15C2 + 20C3 + 30C4 < 45,41T

Điều kiện này có thể được sử dụng để ước tính thời gian tính toán tối đa của các nhiệm vụ mà không bị lỡ thời hạn. Ví dụ: nếu chúng ta giả định

rằng thời gian tính toán của tất cả các tác vụ là như nhau, tức là C=Ci với tất cả i, thì giới hạn trên của thời gian tính toán của từng tác vụ là

C\ð Þ 45:41=77 T ¼ 0:59T ð10:3Þ

Tuy nhiên, nếu chúng ta tính đến chi phí do quá trình xử lý bị gián đoạn, thời gian chặn tác vụ và thời gian chuyển đổi tác vụ, v.v. thì thời gian

tính toán tác vụ cho phép phải nhỏ hơn nhiều. Thay vì cố gắng điều chỉnh thời gian tính toán của từng tác vụ riêng lẻ, chúng tôi chỉ cần sử dụng các

giá trị khác nhau của T trong chương trình thử nghiệm và quan sát hành vi của tác vụ trong thời gian chạy. Trong chương trình thử nghiệm, đơn vị của

khoảng thời gian nhiệm vụ T ban đầu được đặt thành 32 tích tắc hẹn giờ.

Hình 10.3 cho thấy kết quả đầu ra mẫu khi chạy chương trình C10.2. Hình vẽ cho thấy cả 4 task đều được kích hoạt để chạy định kỳ. Thời gian tính

toán tác vụ trung bình là khoảng 5 tích tắc hẹn giờ hoặc 0,156T, thấp hơn đáng kể so với giới hạn lý thuyết là 0,59T. Trong trường hợp này, tất cả các

nhiệm vụ thực sự có thể đáp ứng được thời hạn. Tuy nhiên, nếu chúng ta rút ngắn T xuống giá trị nhỏ hơn, một số nhiệm vụ sẽ bắt đầu trễ thời hạn. Ví

dụ: với các dấu tích hẹn giờ T=16, C=0,31T, vẫn thấp hơn nhiều so với giới hạn RMS là 0,59T, chỉ tác vụ4 sẽ chạy vì nó được kích hoạt quá nhanh nên đây

luôn là tác vụ sẵn sàng có mức ưu tiên cao nhất.

Trong trường hợp này, task1 đến taks3 sẽ luôn trễ thời hạn vì chúng không bao giờ có thể được lên lịch để chạy. Ví dụ cho thấy rằng, với việc lập lịch

tác vụ ưu tiên, việc xác định liệu các tác vụ có thể đáp ứng thời hạn dựa trên các mô hình lý thuyết của RTOS thậm chí còn khó hơn hay không. Đây là

một lĩnh vực RTOS chắc chắn cần được nghiên cứu nhiều hơn, cả về mặt phân tích và thực nghiệm.

10.7.9 UP_RTOS với tài nguyên chia sẻ tác vụ động

Ví dụ thứ ba về UP_RTOS, ký hiệu là C10.3, thể hiện các tác vụ động chia sẻ tài nguyên. Tài nguyên được mô phỏng bằng khóa mutex, hỗ trợ kế thừa ưu

tiên để ngăn chặn việc đảo ngược mức ưu tiên. Trong hệ thống C10.3, các tác vụ được tạo động và có thể chấm dứt sau khi hoàn thành công việc. Lập lịch

tác vụ được thực hiện theo mức độ ưu tiên của tác vụ ưu tiên (tĩnh). Sau đây liệt kê mã triển khai của chương trình C10.3.
Machine Translated by Google

10.7 RTOS bộ xử lý đơn (UP_RTOS) 433

Hình 10.3 UP_RTOS với các tác vụ định kỳ và lập lịch ưu tiên

(1). file ts.s của C10.3: file ts.s giống hệt file của C10.1 (2).
Các tập tin kernel C của C10.3

Để ngắn gọn, chúng tôi chỉ hiển thị các hàm kernel đã sửa đổi để hỗ trợ ưu tiên nhiệm vụ. Chúng bao gồm thao tác V trên các ẩn
dụ và mutex_unlock, có thể khiến tác vụ bị chặn sẵn sàng chạy và thay đổi hàng đợi sẵn sàng. Ngoài ra, kfork có thể tạo một tác vụ
mới có mức độ ưu tiên cao hơn tác vụ đang chạy hiện tại. Tất cả các hàm này đều gọi reschedule(), có thể chuyển đổi nhiệm vụ ngay
lập tức hoặc trì hoãn việc chuyển đổi nhiệm vụ cho đến khi kết thúc quá trình xử lý ngắt IRQ.

int sắp xếp lại()

int SR = int_off();

int pid = đang chạy->pid;

if (readyQueue &&readyQueue->priority > Running->priority){

if (intnest==0){ // không có trong trình xử lý IRQ: ưu tiên ngay lập tức

printf("%d TRƯỚC %d NGAY LẬP TỨC\n", ReadyQueue->pid, pid);

tswitch();

khác{ // vẫn còn trong trình xử lý IRQ: trì hoãn quyền ưu tiên

printf("%d TRÌ HOÃN TRƯỚC %d\n", ReadyQueue->pid, Running->pid);

swflag = 1; // đặt nhu cầu chuyển cờ nhiệm vụ

int_on(SR);

}
Machine Translated by Google

434 10 hệ điều hành nhúng thời gian thực

int P(struct semaphore *s)

int SR = int_off();

s->giá trị--;

nếu (s->giá trị < 0){ // chặn tác vụ đang chạy

đang chạy->trạng thái = KHỐI; enqueue(&s-

>queue, đang chạy); đang chạy->trạng thái =

KHỐI; enqueue(&s->queue, đang chạy); //

kế thừa ưu tiên if (running->priority > s->owner-

>priority){

s->chủ sở hữu->ưu tiên = đang chạy->ưu tiên; // tăng mức độ ưu tiên của chủ sở hữu

sắp xếp lại_readyQueue(); // sắp xếp lại hàng đợi sẵn sàng

tswitch();

s->chủ sở hữu = đang chạy; // là chủ sở hữu của semaphore

int_on(SR);

int V(struct semaphore *s)

PROC *p; int cpsr;

int SR = int_off();

s->giá trị++;

s->chủ sở hữu = 0; // xóa trường chủ sở hữu

nếu (s->giá trị <= 0){ // hàng đợi có người phục vụ

p = dequeue(&s->queue);

p->trạng thái = SẴN SÀNG;

s->chủ sở hữu = p; // chủ sở hữu mới

enqueue(&readyQueue, p); đang chạy-

>ưu tiên = đang chạy->ưu tiên; // khôi phục mức độ ưu tiên của chủ sở hữu printf("timer: V up task%d

pri=%d; đang chạy pri=%d\n",

p->pid, p->ưu tiên, chạy->ưu tiên);

sắp xếp lại();

int_on(SR);

int mutex_lock(MUTEX *s)

PROC *p;

int SR = int_off();

printf("task%d khóa mutex %x\n", Running->pid, s);

if (s->lock==0){ // mutex ở trạng thái mở khóa

s->khóa = 1;

s->chủ sở hữu = đang chạy;

else{ // mutex đã bị khóa: người gọi BLOCK trên mutex

đang chạy->trạng thái = KHỐI; enqueue(&s-

>queue, đang chạy); // kế thừa ưu tiên if

(running->priority > s->owner->priority){

s->chủ sở hữu->ưu tiên = đang chạy->ưu tiên; // tăng mức độ ưu tiên của chủ sở hữu

reorder_readyQueue(); // sắp xếp lại hàng đợi sẵn sàng

tswitch(); // chuyển nhiệm vụ

}
Machine Translated by Google

10.7 RTOS bộ xử lý đơn (UP_RTOS) 435

int_on(SR);

int mutex_unlock(MUTEX *s)

PROC *p;

int SR = int_off();

printf("task%d đang mở khóa mutex\n", đang chạy->pid); if (s->lock==0 || s-

>owner != đang chạy){ // lỗi mở khóa

int_on(SR); trả về -1;

// mutex bị khóa và tác vụ đang chạy là chủ sở hữu if (s->queue == 0)

{ // mutex không có người phục vụ

s->khóa = 0; // xóa khóa

s->chủ sở hữu = 0; // xóa chủ sở hữu

else{ // mutex có người phục vụ: bỏ chặn một người với tư cách là chủ sở hữu mới

p = dequeue(&s->queue);

p->trạng thái = SẴN SÀNG;

s->chủ sở hữu = p;

enqueue(&readyQueue, p);

đang chạy->ưu tiên = đang chạy->ưu tiên; // khôi phục quyền ưu tiên của chủ sở hữu

sắp xếp lại();

int_on(SR);

trả về 1;

PROC *kfork(int func, int ưu tiên)

// tạo tác vụ mới p với mức độ ưu tiên như trước p->rpriority = p-

>priority = Priority; // với các tác vụ động, ReadyQueue

phải được bảo vệ bởi mutex mutex_lock(&readyQueuelock);

enqueue(&readyQueue, p); // nhập tác vụ mới vào hàng đợi sẵn sàng

mutex_unlock(&readyQueuelock);

sắp xếp lại();

trả lại p;

tập tin tc của C10.3

/****************** tệp tc của C10.3 ****************/

#include "type.h"

MUTEX *mp; // mutex toàn cầu

cấu trúc ngữ nghĩa s1; // ngữ nghĩa toàn cục

#include "queue.c"

#include "mutex.c" // các hoạt động mutex được sửa đổi

#include "pv.c" // các hoạt động P/V được sửa đổi

#include "kbd.c"

#include "uart.c"

#include "vid.c"

#include "ngoại lệ.c"

#include "kernel.c"

#include "timer.c"

#include "sdc.c" // trình điều khiển SDC

#include "mesg.c" // thông qua


Machine Translated by Google

436 10 hệ điều hành nhúng thời gian thực

int klog(char *line){ send(line, 2) } ;// gửi tin nhắn tới log task #2

int endIRQ(){ printf("until END of IRQ\n") } void copy_vectors(void)

{ // giống như trước }

// sử dụng các ngắt có vectơ của PL190

int time0_handler(){timer_handler(0) }

int uart0_handler() { uart_handler(&uart[0]) }

int uart1_handler() { uart_handler(&uart[1]) }

int v31_handler()

int sicstatus = *(int *)(SIC_BASE_ADDR+0);

if (sicstatus & (1<<3)) { kbd_handler() }

if (sicstatus & (1<<22)){ sdc_handler() }

int vectorInt_init() // thêm các ngắt KBD và SDC

printf("vectorInt_init() ");

/***** ghi vào vectorAddr regs ****************************/

*((int *)(VIC_BASE_ADDR+0x100)) = (int)timer0_handler;

*((int *)(VIC_BASE_ADDR+0x104)) = (int)uart0_handler;

*((int *)(VIC_BASE_ADDR+0x108)) = (int)uart1_handler;

*((int *)(VIC_BASE_ADDR+0x10C)) = (int)kbd_handler;

*((int *)(VIC_BASE_ADDR+0x10C)) = (int)v31_handler;

*((int *)(VIC_BASE_ADDR+0x110)) = (int)sdc_handler;

/***** ghi vào intControlRegs: E=1|IRQ# = 1xxxxx *********/

*((int *)(VIC_BASE_ADDR+0x200)) = 0x24; //0100100 tại IRQ 4

*((int *)(VIC_BASE_ADDR+0x204)) = 0x2C; //0101100 tại IRQ 12

*((int *)(VIC_BASE_ADDR+0x208)) = 0x2D; //0101101 tại IRQ 13

*((int *)(VIC_BASE_ADDR+0x20C)) = 0x3F; //0111111 tại IRQ 31

*((int *)(VIC_BASE_ADDR+0x210)) = 0x36; //0110110 tại IRQ 22

/***** ghi 0 vào IntSelectReg để tạo các ngắt IRQ */

*((int *)(VIC_BASE_ADDR+0x0C)) = 0;

int irq_chandler()

int (*f)(); // f là con trỏ hàm

f =(void *)*((int *)(VIC_BASE_ADDR+0x30)); // đọc reg vectorAddr

f(); // gọi hàm ISR

*((int *)(VIC_BASE_ADDR+0x30)) = 1; // ghi vào vectorAddr dưới dạng EOI

int delay(){ // vòng lặp trễ để mô phỏng tính toán tác vụ }

nhiệm vụ int5()

printf("task%d đang chạy: ", Running->pid);

klog("bắt đầu");

mutex_lock(mp);

printf("task%d bên trong CR\n", Running->pid);

klog("bên trong CR");

mutex_unlock(mp);

klog("chấm dứt");

kexit(0);

}
Machine Translated by Google

10.7 RTOS bộ xử lý đơn (UP_RTOS) 437

nhiệm vụ int4()

klog("tạo task5");

kfork((int)task5, 5); // tạo P5 với mức độ ưu tiên=5

trì hoãn();

printf("task%d terminte\n", Running->pid);

klog("chấm dứt");

kexit(0);

nhiệm vụ int3() // Được cập nhật theo định kỳ theo bộ đếm thời gian

int pid, trạng thái;

trong khi(1){

printf("task%d chờ sự kiện hẹn giờ\n", Running->pid);

P(&s1);

klog("đang chạy");

mutex_lock(mp);

printf("task%d bên trong CR\n", Running->pid);

klog("tạo task4");

nhiệm vụ kfork((int), 4); // tạo P4 trong khi giữ khóa mutex

trì hoãn();

mutex_unlock(mp);

pid = kwait(&status);

int task2() // tác vụ ghi nhật ký ở mức ưu tiên 1

int blk = 0;

dòng char[1024]; // dòng nhật ký 1KB

printf("task%d: ", Running->pid);

trong khi(1){

//printf("task%d:recved line=%s\n", Running->pid, line);

r = recv(dòng);

put_block(blk, line); // ghi dòng nhật ký vào SDC

blk++;

uprintf("%s\n", dòng); // hiển thị lên UART0 trực tuyến

printf("log: ");

nhiệm vụ int1()

trạng thái int, pid;

pid = kfork((int)task2, 2);

pid = kfork((int)task3, 3);

while(1) // task1 chờ bất kỳ ZOMBIE con nào

pid = kwait(&status);

int chính()

fbuf_init(); // LCD

uart_init(); // UART

kbd_init(); // KBD

printf("Chào mừng đến với UP_RTOS trong ARM\n");


Machine Translated by Google

438 10 hệ điều hành nhúng thời gian thực

/* cấu hình VIC và SIC cho các ngắt */

VIC_INTENABLE = 0;

VIC_INTENABLE |= (1<<4); // bộ đếm thời gian0,1 ở bit4

VIC_INTENABLE |= (1<<12); // UART0 tại bit12

VIC_INTENABLE |= (1<<13); // UART2 tại bit13

VIC_INTENABLE |= (1<<31); // SIC tới IRQ31 của VIC

UART0_IMSC = 1<<4; // kích hoạt ngắt UART0 RXIM

SIC_INTENABLE = (1<<3); // KBD int=bit3 trên SIC

SIC_INTENABLE |= (1<<22); // SDC int=bit22 trên SIC

SIC_ENSET = (1<<3); // KBD int=3 trên SIC

SIC_ENSET |= 1<<22; // SDC int=22 trên SIC

vectorInt_init(); // khởi tạo các ngắt có vector

bộ đếm thời gian_init();

bộ đếm thời gian_start(0); // bộ đếm thời gian0

sdc_init(); // khởi tạo trình điều khiển SDC

tin nhắn_init(); // khởi tạo bộ đệm tin nhắn

mp = mutex_create(); // mutex và semaphore

s1.giá trị = 0; s1.queue = 0;

kernel_init(); // khởi tạo kernel, chạy P0

kfork((int)task1, 1); // tạo P1 với mức độ ưu tiên=1

trong khi(1){ // Vòng lặp P0

nếu (sẵn sàng)

tswitch();

Khi hệ thống C10.3 khởi động, nó tạo và chạy tác vụ ban đầu P0, có mức ưu tiên thấp nhất là 0. P0 tạo một tác vụ mới P1 với mức ưu tiên =

2. Vì P1 có mức độ ưu tiên cao hơn nên nó trực tiếp ưu tiên P0, điều này thể hiện việc ưu tiên nhiệm vụ ngay lập tức mà không có bất kỳ sự

chậm trễ nào. Khi P1 chạy, nó tạo ra một tác vụ con P2 với mức độ ưu tiên=1 làm tác vụ ghi nhật ký. Trong trường hợp này, nó không chuyển đổi

tiến trình. Sau đó, nó tạo một tác vụ con khác P3 với mức độ ưu tiên = 3. Vì P3 có mức độ ưu tiên cao hơn nên nó ngay lập tức chiếm ưu thế

trước P1. Khi P1 chạy lại, nó thực thi trong một vòng lặp, chờ đợi bất kỳ con ZOMBIE nào và giải phóng PROC con để sử dụng lại.

Khi P3 chạy, nó chờ một sự kiện hẹn giờ, sự kiện này sẽ được bộ hẹn giờ V-ed up định kỳ. Khi P3 chạy lại, nó khóa một mutex và tạo P4 với mức

độ ưu tiên = 4, ngay lập tức ưu tiên P3. P4 tạo ra một P5 con với mức độ ưu tiên 5, ngay lập tức ưu tiên P4. Khi P5 chạy, nó cố gắng lấy cùng

một khóa mutex mà P3 vẫn giữ. Vì vậy, P5 bị chặn trên mutex nhưng nó tăng mức độ ưu tiên của P3 lên 5, do đó ngăn chặn việc đảo ngược mức độ

ưu tiên. Khi P3 mở khóa mutex, nó sẽ mở khóa P5 và khôi phục mức ưu tiên của chính nó về 3, cho phép P4 chạy tiếp theo thay vì P3. Những điều

này thể hiện sự kế thừa ưu tiên trong UP_RTOS. Sau khi mở khóa mutex, P3 có hai lựa chọn. Nó có thể lặp lại vòng lặp hoặc kết thúc. Nếu P3 lặp

lại vòng lặp, nó phải đợi con của nó kết thúc và giải phóng cấu trúc PROC con để sử dụng lại. Nếu không, số lượng cấu trúc PROC có sẵn sẽ ít

hơn một lần mỗi lần P3 chạy. Ngoài ra, P3 có thể chấm dứt. Trong trường hợp đó, P1 sẽ loại bỏ tất cả các PROC đã kết thúc và tạo lại một phiên

bản P3 mới. Trong hệ thống trình diễn C10.3, nhiệm vụ P2 là nhiệm vụ ghi nhật ký. Nó chạy với mức ưu tiên thấp thứ hai để không cản trở việc

thực thi các tác vụ khác. P2 liên tục cố gắng nhận các tin nhắn đang ghi nhật ký yêu cầu từ các tác vụ khác. Đối với mỗi yêu cầu khao khát, P2

định dạng yêu cầu nhật ký thành biểu mẫu

dấu thời gian: pid:thông tin nhật ký

và ghi dòng này vào khối 1KB trong SDC bằng cách gọi trực tiếp trình điều khiển SDC. Hình 10.4 cho thấy các kết quả đầu ra mẫu khi chạy hệ thống

UP_RTOS C10.3.

Phần trên cùng của Hình 10.4 hiển thị thông tin nhật ký trực tuyến được hiển thị bởi tác vụ ghi nhật ký tới cổng UART. Thông tin ghi nhật

ký cũng được lưu vào SDC dưới dạng bản ghi vĩnh viễn. Trong chương trình trình diễn, SDC là một SD ảo, không phải là một tệp thông thường.

Nội dung của nó có thể được đọc bằng thao tác get_block() của trình điều khiển SDC để kiểm tra.

Phân tích khả năng lập lịch và thời hạn: Khả năng lập lịch trình của RTOS bao gồm các tài nguyên chia sẻ nhiệm vụ đã được nhiều người

nghiên cứu. Với việc chia sẻ tài nguyên, các ưu tiên của nhiệm vụ không còn cố định nữa. Vì vậy, những phân tích như vậy không áp dụng cho mô

hình RMS đơn giản nhưng chúng có thể áp dụng cho cả mô hình EDF và DMS. Sau đây là phân tích về khả năng lập kế hoạch (được đơn giản hóa) dựa

trên công trình của (Audsley và cộng sự 1993). Đầu tiên, chúng ta định nghĩa các thuật ngữ sau.
Machine Translated by Google

10.7 RTOS bộ xử lý đơn (UP_RTOS) 439

Hình 10.4 UP_RTOS với tài nguyên chia sẻ tác vụ động

Ci = thời gian tính toán trong trường hợp xấu nhất của nhiệm vụ i trên mỗi bản

phát hành Ti = giới hạn dưới về thời gian giữa các lần thực hiện nhiệm vụ i liên tiếp. Đối với các nhiệm vụ định kỳ, Ti bằng khoảng thời gian của nhiệm vụ Di

= yêu cầu về thời hạn của nhiệm vụ i, được đo tương ứng với thời gian giải phóng nhất định của nhiệm vụ i Bi = nhiệm

vụ có thời gian chặn trong trường hợp xấu nhất mà tôi có thể gặp phải do kế thừa mức độ ưu tiên. Bi thường bằng phần quan trọng dài nhất của các nhiệm vụ có

mức độ ưu tiên thấp hơn khi truy cập các ẩn dụ có mức trần cao hơn hoặc bằng mức độ ưu tiên của nhiệm vụ i Ii = trường hợp nhiễu xấu nhất mà

một nhiệm vụ tôi có thể gặp phải từ các nhiệm vụ khác. Sự can thiệp vào nhiệm vụ i được định nghĩa là thời gian các nhiệm vụ có mức ưu tiên cao hơn có thể

chiếm ưu thế và thực thi nhiệm vụ i, do đó ngăn cản nhiệm vụ i thực thi Ri = thời gian phản hồi trong

trường hợp xấu nhất đối với nhiệm vụ tôi được đo từ thời điểm nhiệm vụ được giải phóng. Đối với một nhiệm vụ có thể lập lịch trình, Ri < Di hoặc Ri < Ti, nếu

thời hạn Di không được chỉ định hp(i) = tập hợp

các nhiệm vụ có mức ưu tiên cơ bản cao hơn mức ưu tiên cơ sở của nhiệm vụ i, tức là các nhiệm vụ có thể ưu tiên nhiệm vụ i

Dựa trên các thuật ngữ được xác định ở trên, thời gian đáp ứng của nhiệm vụ thứ i có thể được biểu thị bằng

Ri ¼ Ci þ Bi þ Ii ð10:4Þ

Thời gian tính toán trong trường hợp xấu nhất Ci có thể được xác định bằng cách chạy từng tác vụ trong môi trường biệt lập. Thời gian chặn trong trường

hợp xấu nhất Bi bằng với phần quan trọng dài nhất của bất kỳ tác vụ có mức độ ưu tiên thấp hơn nào truy cập vào một semaphore hoặc mutex. Trường hợp xấu nhất

của nhiệm vụ i bị ảnh hưởng bởi nhiệm vụ j là [Ri/Tj]Cj, chiếm thời gian nhiệm vụ i bị chặn bởi nhiệm vụ j do được ưu tiên thực hiện nhiệm vụ. Tổng mức can

thiệp của nhiệm vụ i từ tất cả các nhiệm vụ j khác được đưa ra bởi
Machine Translated by Google

440 10 hệ điều hành nhúng thời gian thực

Ii ¼ TỔNG g ½ Ri=Tj Cj ; ð10:5Þ


j in hp ið Þf

Kết hợp các phương trình (10.4) và (10.5) thu được phương trình (tái diễn)

Ri nð Þ¼ þ 1 Ci þ Bi þ SUM ½ Ri nð Þ=Tj Cj g ð10:6Þ


j in hp ið Þf

Phương trình truy hồi có thể được giải bằng phép lặp. Quá trình lặp bắt đầu với Ri(0) = 0 và kết thúc khi Ri(n+1) = Ri

(N). Việc lặp lại có thể kết thúc sớm hơn nếu Ri(n+1) > Di hoặc Ri(n+1) > Ti, chỉ ra rằng taski sẽ không đạt thời hạn.

Hơn nữa, nó đã được chứng minh (Joseph và Pandya 1986) rằng phép lặp được đảm bảo hội tụ nếu việc sử dụng CPU

là <100%. Sau khi tính toán thời gian phản hồi của tất cả các nhiệm vụ, việc kiểm tra xem chúng có thể đáp ứng được yêu cầu hay không là một vấn đề đơn giản.
thời hạn. Tất cả các nhiệm vụ có thể đáp ứng thời hạn nếu

Ri\Di hoặc Ri\Ti cho các


ð nhiệm vụ định kỳ Þcho tất cả i

Các kết quả phân tích có thể được sử dụng làm hướng dẫn khi thiết kế RTOS với tính năng chia sẻ tài nguyên. Để xác định liệu

các nhiệm vụ trong RTOS thực có thể đáp ứng thời hạn vẫn phải được xác minh bằng thực nghiệm. Đối với ví dụ hệ thống RTOS C10.3, chúng tôi

hãy để việc phân tích khả năng lập kế hoạch và xác minh thời hạn thực hiện nhiệm vụ như một bài tập trong phần Vấn đề.

10.8 RTOS đa bộ xử lý (SMP_RTOS)

Trong chương. 9, chúng tôi đã thảo luận chi tiết về hoạt động SMP cho kiến trúc ARM MPcore. Trong phần này, chúng tôi sẽ chỉ ra cách

thiết kế và triển khai RTOS cho hệ thống SMP. Mặc dù nguyên tắc thiết kế mang tính chung chung và có thể áp dụng cho mọi SMP

hệ thống, chúng tôi sẽ sử dụng kiến trúc ARM A9-MPcore làm nền tảng để triển khai và thử nghiệm. Còn hơn là

trình bày một hệ thống hoàn chỉnh trong một bước, chúng tôi sẽ phát triển hệ thống SMP_RTOS theo các bước tăng dần. Trong mỗi bước, chúng tôi

sẽ tập trung vào một vấn đề duy nhất đối với các hoạt động thời gian thực trong môi trường SMP và trình bày giải pháp bằng một

hệ thống mẫu. Các bước là:

(1). Phát triển hạt nhân SMP để quản lý tác vụ với khả năng xử lý đồng thời được cải thiện

(2). Điều chỉnh UP_RTOS cho SMP

(3). Triển khai lập lịch tác vụ ưu tiên theo mức độ ưu tiên trong SMP

(4). Thực hiện các ngắt lồng nhau trong SMP

(5). Triển khai kế thừa ưu tiên trong SMP

(6). Tích hợp các kết quả vào một SMP_RTOS hoàn chỉnh

10.8.1 Hạt nhân SMP_RTOS để quản lý tác vụ

Đầu tiên, chúng tôi trình bày thiết kế và triển khai hạt nhân SMP_ROTS ban đầu để quản lý tác vụ. SMP_RTOS ban đầu

kernel chỉ hỗ trợ tạo tác vụ động, chấm dứt tác vụ và đồng bộ hóa tác vụ. Nó nên khai thác song song

khả năng xử lý của nhiều CPU để cải thiện khả năng xử lý đồng thời. Để bắt đầu, chúng ta sẽ giả sử hoạt động sau

môi trường thực hiện nhiệm vụ.

. Mỗi nhiệm vụ là một thực thể thực thi duy nhất. Không có chủ đề bổ sung trong một nhiệm vụ.

. Tất cả các tác vụ đều thực thi trong cùng một không gian địa chỉ, do đó không cần ánh xạ địa chỉ ảo.

. Các tác vụ không sử dụng tín hiệu để liên lạc giữa các tác vụ và không cần hỗ trợ hệ thống tệp, vì vậy chúng không có bất kỳ tín hiệu nào.

tài nguyên, như trong một hệ điều hành có mục đích chung.

Đây là những giả định hợp lý cho các ứng dụng thời gian thực, giúp đơn giản hóa cấu trúc PROC cho các ứng dụng thời gian thực.

nhiệm vụ. Phần sau đây trình bày cấu trúc PROC đơn giản hóa trong nhân SMP_RTOS.
Machine Translated by Google

10.8 RTOS đa bộ xử lý (SMP_RTOS) 441

10.8.1.1 Cấu trúc PROC trong SMP_RTOS

#xác định SSIZE 1024

quy trình cấu trúc typedef{

struct proc *next;

int *ksp; // con trỏ ngăn xếp đã lưu

int trạng thái; // MIỄN PHÍ|SẴN SÀNG|BLOCK|ZOMBIE, v.v.

int sự ưu tiên; // mức độ ưu tiên hiệu quả

int pid;

int ppid;

struct proc *parent;

int mã thoát; // mã thoát

int sẵn sàng_thời gian;

struct mbuf *mqueue; // Hàng đợi tin nhắn

cấu trúc mutex mlock; // khóa mutex hàng đợi tin nhắn

cấu trúc semaphore nmsg; // chờ tin nhắn semaphore

int cpuid; // Proc này đang chạy trên CPU nào

int sự ưu tiên; // dành cho kế thừa ưu tiên

int khoảng thời gian; // để lập lịch theo thời gian

struct semaphore wchild; // chờ các con ZOMBIE

int kstack[SSIZE];

}THỦ TỤC;

Trong cấu trúc PROC đơn giản hóa, các trường không gian địa chỉ ảo, bối cảnh và tài nguyên chế độ người dùng, ví dụ: bộ mô tả tệp

và tín hiệu đều bị loại bỏ. Để rõ ràng, các trường bổ sung cho hoạt động thời gian thực được hiển thị bằng các đường nét đậm.

10.8.1.2 Cấu trúc dữ liệu hạt nhân: Cấu trúc dữ liệu hạt nhân được định nghĩa như sau.

#xác định NCPU 4

#define NPROC 256

Mỗi nhiệm vụ được thể hiện bằng một cấu trúc PROC. Tổng số cấu trúc PROC, NPROC, là một cấu hình

tham số, có thể được thiết lập cho phù hợp với nhu cầu hệ thống thực tế. Ban đầu, tất cả cấu trúc PROC đều nằm trong một Danh sách miễn phí. Khi tạo

một nhiệm vụ, chúng tôi phân bổ một PROC miễn phí từ Danh sách miễn phí. Khi một tác vụ kết thúc, cấu trúc PROC của nó cuối cùng sẽ được giải phóng trở lại

freeList để tái sử dụng. Danh sách miễn phí được bảo vệ bằng khóa xoay trong quá trình phân bổ/hủy phân bổ các PROC miễn phí.

PROC *danh sách miễn phí; // chứa tất cả PROC miễn phí

int khóa tự do = 0; // spinlock cho freeList

Trong nhân UP, chỉ cần sử dụng một con trỏ đang chạy duy nhất trỏ vào PROC đang thực thi hiện tại là đủ. Trong nhân SMP, chúng ta

sẽ xác định các tác vụ thực thi trên các CPU khác nhau bằng các phương tiện sau.

#define đang chạy run[get_cpuid()]

PROC *chạy[NCPU]; // con trỏ tới PROC đang chạy trên mỗi CPU

Trong nhân UP, chỉ cần sử dụng một hàng đợi duy nhất để lập lịch tác vụ là đủ. Trong nhân SMP, một hàng đợi lập lịch đơn có thể là một

tắc nghẽn, hạn chế nghiêm trọng sự đồng thời của hệ thống. Để cải thiện cả tính đồng thời và hiệu quả, chúng tôi giả định

rằng mỗi CPU có một hàng đợi lập lịch riêng được bảo vệ bởi một spinlock.
Machine Translated by Google

442 10 hệ điều hành nhúng thời gian thực

PROC *readyQueue[NCPU]; // mỗi hàng đợi lập lịch tác vụ của CPU

int sẵn sàng [NCPU]; // spinlocks cho ReadyQueue[cpuid]

Mỗi CPU luôn cố gắng chạy các tác vụ từ ReadyQueue[cpuid] của chính nó. Nếu hàng đợi sẵn sàng của CPU trống, nó sẽ chạy một tác vụ nhàn rỗi đặc

biệt, đặt CPU ở trạng thái tiết kiệm năng lượng, chờ các sự kiện hoặc gián đoạn. Khi hệ thống khởi động, mỗi CPU sẽ chạy một tác vụ ban đầu đặc biệt,

đây cũng là tác vụ nhàn rỗi trên CPU.

PROC iproc[NCPU]; // mỗi tác vụ ban đầu/không hoạt động của CPU

Các tác vụ thường chạy ở chế độ Giám sát (SVC), chế độ này sử dụng ngăn xếp trong cấu trúc PROC. Một tác vụ có thể vào chế độ IRQ để xử lý các

ngắt hoặc chế độ ABT để xử lý các ngoại lệ hủy bỏ dữ liệu, v.v. Mỗi CPU phải có các ngăn xếp riêng biệt để xử lý các ngắt và ngoại lệ. Do đó, chúng

tôi xác định IRQ trên mỗi CPU và ngăn xếp chế độ ngoại lệ là

int irq_stack[NCPU][1024]; // mỗi ngăn xếp chế độ IRQ của CPU

int abt_stack[NCPU][1024]; // mỗi ngăn xếp chế độ ABT của CPU

int und_stack[NCPU][1024]; // mỗi ngăn xếp chế độ UND của CPU

Trong nhân ROTS, việc lập lịch tác vụ ưu tiên và các ngắt lồng nhau là rất cần thiết. Với việc lập lịch tác vụ ưu tiên, mỗi CPU cần có một cờ

chuyển tác vụ. Với các ngắt lồng nhau, mỗi CPU phải theo dõi mức độ lồng nhau của ngắt. Chuyển đổi tác vụ không được phép trong khi CPU đang thực thi

bên trong bất kỳ trình xử lý ngắt nào. Chuyển đổi tác vụ chỉ có thể xảy ra nếu tất cả quá trình xử lý ngắt lồng nhau đã kết thúc. Vì vậy, chúng tôi

xác định

int intnest dễ bay hơi [NCPU]; // mỗi bộ đếm lồng nhau ngắt CPU

dễ bay hơi int swflag[NCPU]; // mỗi cờ tác vụ chuyển đổi CPU

Vì mỗi biến này chỉ được truy cập bởi một CPU duy nhất, từ một tác vụ chạy trên CPU hoặc bằng một ngắt

xử lý trên cùng một CPU, chỉ cần bảo vệ chúng bằng các ngắt kích hoạt/vô hiệu hóa thông thường của CPU là đủ.

10.8.1.3 Bảo vệ cấu trúc dữ liệu hạt nhân Mỗi cấu trúc dữ liệu hạt

nhân được bảo vệ bởi một spinlock. Yêu cầu về spinlocks chủ yếu là ở cấp độ đơn. Khi các tác vụ phải có nhiều spinlock, chúng tôi phải đảm bảo rằng

thứ tự khóa luôn là một chiều để không thể xảy ra deadlock.

10.8.1.4 Công cụ đồng bộ hóa Hạt nhân SMP_RTOS

hỗ trợ các công cụ sau để đồng bộ hóa tác vụ và liên lạc giữa các tác vụ.

Spinlocks: spinlocks được sử dụng để bảo vệ các vùng quan trọng (CR) trong thời gian ngắn và cả trong bộ xử lý ngắt thiết bị.

Mutexes: mutexes dùng để bảo vệ CR trong thời gian dài. Chúng được triển khai với tính kế thừa ưu tiên để ngăn chặn sự đảo ngược mức độ ưu tiên.

Semaphores: semaphores chỉ được sử dụng để hợp tác quy trình. Chúng cũng được thực hiện với sự kế thừa ưu tiên.

Bộ nhớ dùng chung: các tác vụ thực thi trong cùng một không gian địa chỉ, do đó chúng có thể sử dụng bộ nhớ dùng chung để liên lạc giữa các tác vụ.

Mỗi vùng bộ nhớ dùng chung được bảo vệ bằng khóa mutex để đảm bảo quyền truy cập độc quyền.

Tin nhắn: tác vụ có thể gửi/nhận tin nhắn thông qua hàng đợi tin nhắn trên mỗi tác vụ.

10.8.1.5 Các tác vụ Quản lý tác vụ có

thể được tạo động để thực thi một chức năng có mức ưu tiên tĩnh trên một CPU cụ thể. Tạo tác vụ bằng API

int pid = kfork((int)func, int ưu tiên, int cpu)

Để nhất quán với nhân Unix/Linux truyền thống, các tác vụ tuân theo mối quan hệ cha-con thông thường. Khi một nhiệm vụ có

đã hoàn thành công việc của mình, nó có thể chấm dứt bởi API

void kexit(int exitValue)


Machine Translated by Google

10.8 RTOS đa bộ xử lý (SMP_RTOS) 443

Khi một tác vụ có các tác vụ con kết thúc, trước tiên, nó sẽ giải phóng các tác vụ con đã bị chấm dứt của chính nó, nếu có, và gửi các

tác vụ con khác đến một nhiệm vụ đặc biệt P1, hoạt động giống như quy trình INIT trong nhân Unix/Linux. Tác vụ bị chấm dứt sẽ trở thành

ZOMBIE và thông báo cho cha mẹ của nó, có thể là cha mẹ ban đầu hoặc cha mẹ nuôi P1. Nhiệm vụ cha mẹ có thể đợi API kết thúc con

int kwait(int *trạng thái)

trả về pid con đã kết thúc, trạng thái thoát của nó và giải phóng PROC con ZOMBIE để sử dụng lại. Đồng bộ hóa giữa nhiệm vụ gốc và nhiệm

vụ con được thực hiện bằng một semaphore trong PROC gốc, điều này ngăn cản tình trạng chạy đua giữa các tác vụ gốc và nhiệm vụ con ngay cả

khi chúng chạy song song trên các CPU khác nhau.

Tiếp theo, chúng tôi trình bày cách triển khai phần quản lý tác vụ của nhân SMP_RTOS. Như thường lệ, mã hạt nhân bao gồm một tệp ts.s

và một tập hợp các tệp trong C. Các tệp hạt nhân được liên kết biên dịch với một tệp thực thi nhị phân t.bin, được chạy trực tiếp trên máy

ảo realview-pbx-a9 được mô phỏng trong QEMU bằng tập lệnh sh.

arm-none-eabi-as -mcpu=cortex-a9 ts.s -o ts.o

arm-none-eabi-gcc -c -mcpu=cortex-a9 tc -o to

arm-none-eabi-ld -T t.ld ts.o to -Ttext=0x10000 -o t.elf

arm-none-eabi-objcopy -O nhị phân t.elf t.bin

qemu-system-arm -M realview-pbx-a9 -smp 4 -m 512M -sd ../sdc \

-kernel t.bin -serial mon:stdio

Ngoài ra, chúng tôi cũng có thể ghi t.bin dưới dạng ảnh hạt nhân vào thư mục boot/ trong hệ thống tệp SDC và sử dụng một tệp riêng biệt.

booter để khởi động ảnh kernel từ SDC.

(1). Trình tự khởi động: Khi máy ảo realview-pbx-a9 khởi động, CPU0 là bộ xử lý khởi động và tất cả các CPU phụ (AP) khác được giữ ở trạng

thái WFI, chờ SGI từ bộ xử lý khởi động thực sự khởi động. Bộ xử lý khởi động bắt đầu thực thi reset_handler trong tệp ts.s. Các hoạt động
của CPU0 là

. Đặt con trỏ ngăn xếp chế độ SVC thành đầu cao của iproc[0], làm cho iproc[0].kstack làm ngăn xếp ban đầu.

. Đặt con trỏ ngăn xếp chế độ IRQ ở đầu cao nhất của irq_stack[0], làm cho irq_stack[0] làm ngăn xếp chế độ IRQ của nó. Tương tự, nó thiết

lập ngăn xếp chế độ ABT và UND.

. Sao chép bảng vectơ vào địa chỉ 0, sau đó gọi main() trong C để khởi tạo kernel.

Phần sau đây hiển thị mã reset_handler của CPU0 và mã khởi động của các AP.

(2). Mã Reset_handler và apStart trong hội

.global reset_handler, apStart // CPU0, mã AP

.global iproc, procsize // toàn cầu

.global irq_stack, abt_stack, und_stack // toàn cầu

.global tswitch, bộ lập lịch

.global int_on, int_off, khóa, mở khóa

.global get_cpsr, get_spsr;

.global slock, sunlock, get_cpuid

.vector toàn cầu_bắt đầu, vectơ_end

reset_handler:

// CPU0: đặt ngăn xếp SVC thành CAO CẤP của iproc[0].kstack[] // r0 trỏ tới proc's

ldr r0, =iproc ldr

r1, =procsize // r1 -> procsize


Machine Translated by Google

444 10 hệ điều hành nhúng thời gian thực

ldr r2, [r1, #0] // r2 = xử lý

thêm r0, r0, r2

mov sp, r0 // SVC sp -> cao cấp của iproc[0]

// CPU0: vào chế độ IRQ để thiết lập ngăn xếp IRQ

msr cpsr, #0x92

ldr sp, =irq_stack // trỏ vào int irq_stack[0][1024]

thêm sp, sp, #4096 // cấp cao nhất của irq_stack[0]

// CPU0: vào chế độ ABT để thiết lập ngăn xếp ABT

msr cpsr, #0x97

ldr sp, =abt_stack

thêm sp, sp, #4096

// CPU0: vào chế độ UND để thiết lập ngăn xếp UND

msr cpsr, #0x9B

ldr sp, =und_stack

thêm sp, sp, #4096

// CPU0: quay lại chế độ SVC để đặt SPSR thành chế độ SVC khi bật IRQ

msr cpsr, #0x93

msr spsr, #0x13

// CPU0: sao chép bảng vector vào địa chỉ 0

bl copy_vectors

bl chính // gọi hàm main() trong C


b. // treo nếu main() quay trở lại

apStart: // Điểm vào AP

LDR r0, =iproc // r0 trỏ tới iproc

LDR r1, =procsize // r1 -> procsize

LDR r2, [r1, #0] // r2 = xử lý

// lấy ID CPU

MRC p15, 0, r1, c0, c0, 5 // đọc thanh ghi ID CPU tới r1

VÀ r1, r1, #0x03 // chỉ che dấu các bit ID CPU

mul r3, r2, r1 // procsize*CPUID: r3->high end của iproc[CPUID]

thêm r0, r0, r3 // r0 += r3

mov sp, r0 // sp->iproc[CPUID] cao cấp

//AP: chuyển sang chế độ IRQ để đặt ngăn xếp IRQ

MSR cpsr, #0x92

ldr r0, =irq_stack // int irq_stack[4][1024] trong C

lsl r2, r1, #12 // r2 = r1*4096

CỘNG r0, r0, r2 // r0 -> svcstack[cpuid] cao cấp

MOV sp, r0 // IRQ sp của ID CPU -> phần cao nhất của irq[ID][1024]

//AP: chuyển sang chế độ ABT để đặt ngăn xếp ABT

MSR cpsr, #0x97

LDR sp, =abt_stack

lsl r2, r1, #12 // r2 = r1*4096

CỘNG r0, r0, r2 // r0 -> svcstack[cpuid] cao cấp

MOV sp, r0 // IRQ sp của ID CPU -> phần cao nhất của irq[ID][4096]

//AP: vào chế độ UND để đặt ngăn xếp UND

MSR cpsr, #0x9B

LDR sp, =und_stack

lsl r2, r1, #12 // r2 = r1*4096

CỘNG r0, r0, r2 // r0 -> svcstack[cpuid] cao cấp

MOV sp, r0 // IRQ sp của ID CPU -> phần cao nhất của irq[ID][4096]

//AP: quay lại chế độ SVC để đặt SPSR

MSR cpsr, #0x93

MSR spsr, #0x13


Machine Translated by Google

10.8 RTOS đa bộ xử lý (SMP_RTOS) 445

//AP: gọi APstart() với IRQ bật

MSR cpsr, #0x13

bl bắt đầu // gọi APstart() trong C

(3). Hàm main() và APstart() trong C

Trong main(), CPU0 trước tiên khởi tạo trình điều khiển thiết bị, định cấu hình GIC cho các ngắt, khởi tạo cấu trúc dữ liệu kernel

và chạy tác vụ ban đầu với pid=1000. Sau đó nó phát ra SGI để kích hoạt các AP. Sau khi tất cả các AP đã sẵn sàng, nó sẽ tạo P0 làm tác

vụ ghi nhật ký và tác vụ INIT P1. Sau đó, nó thực thi run_task(), cố gắng chạy các tác vụ dưới dạng hàng đợi sẵn sàng của cùng một CPU.

Khi nhận được SGI từ CPU0, mỗi AP bắt đầu thực thi mã apStart trong tập hợp. Các hành động của mỗi AP tương tự như của CPU0, tức là

nó đặt con trỏ ngăn xếp chế độ SVC ở mức cao nhất của iproc[cpuid] và IRQ cũng như các ngăn xếp chế độ khác theo cpuid. Sau đó, nó gọi

APstart() trong C. Trong APstart(), mỗi AP định cấu hình và khởi động bộ đếm thời gian cục bộ của nó, cập nhật biến toàn cục ncpu để

đồng bộ hóa với CPU0. Sau đó nó cũng gọi run_task(). Tại thời điểm này, hàng đợi sẵn sàng của các AP vẫn trống.

Vì vậy, mỗi AP chạy một tác vụ nhàn rỗi với pid=1000+cpuid và chuyển sang trạng thái WFE. Sau khi tạo nhiệm vụ trên các AP, có hai cách

để các AP tiếp tục. CPU0 có thể gửi SGI đến tất cả các AP, khiến chúng thoát khỏi trạng thái WFE hoặc mỗi AP sẽ khởi động khi bộ đếm

thời gian cục bộ của nó bị gián đoạn. Phần sau đây hiển thị các đoạn mã của run_task(), APstart() và main().

int run_task() // mỗi CPU cố gắng chạy tác vụ từ ReadyQ của chính nó

int cpuid = get_cpuid();

trong khi(1){

slock(&readylock[cpuid]);

if (readyQueue[cpuid] == 0){ // kiểm tra ReadyQueue của chính mình

sunlock(&readylock[cpuid]);

asm("WFE"); // chờ sự kiện/ngắt

khác{

sunlock(&readylock[cpuid]);

ttswitch(); // chuyển tác vụ trên CPU

int APstart()

int cpuid = get_cpuid();

slock(&aplock);

màu = VÀNG;

printf("CPU%d trong APstart … ", cpuid); config_int(29,

cpuid); // cấu hình bộ đếm thời gian cục bộ ptimer_init(); ptimer_start(); // khởi

động bộ đếm thời gian cục bộ

ncpu++;

đang chạy = &iproc[cpuid]; printf("%d // chạy iproc ban đầu[cpuid]

chạy trên CPU%d\n", Running->pid, cpuid); khóa nắng(&aplock);

run_task();

int chính()

// khởi tạo trình điều khiển thiết bị

// cấu hình GIC cho các ngắt

kernel_init(); //khởi tạo hạt nhân

printf("AP khởi động CPU0: ");

int *APaddress = (int *)0x10000030;


Machine Translated by Google

446 10 hệ điều hành nhúng thời gian thực

*Địa chỉ AP = (int)apStart;

send_sgi(0x0, 0x0F, 0x01); // gửi SGI 0 tới tất cả các Aps

printf("CPU0 đợi AP sẵn sàng\n");

while(ncpu < 4);

printf("CPU0 tiếp tục\n");

kfork(int)logtask, 1, 0); // tạo P0 trên CPU0 làm tác vụ ghi nhật ký

kfork((int)f1, 1, 0); // tạo P1 trên CPU0 làm tác vụ INIT

run_task();

10.8.1.6 Trình diễn quản lý tác vụ trong SMP_RTOS


Chúng tôi chứng minh việc quản lý tác vụ trong nhân SMP_RTOS ban đầu bằng một chương trình mẫu, ký hiệu là C10.4. Quản lý tác vụ bao gồm tạo tác

vụ, kết thúc tác vụ và chờ kết thúc tác vụ con. Chương trình bao gồm những điều sau đây

các thành phần.

(1). tập tin ts.s của C10.4:

/*********** ts.s file của C10.4: SMP_RTOS kernel **********/

.chữ

.code 32

.global reset_handler, vectơ_start, vectơ_end

.global proc, procsize, apStart

.global tswitch, bộ lập lịch

.global int_on, int_off, khóa, mở khóa

.global get_fault_status, get_fault_addr, get_cpsr, get_spsr;

.global slock, sunlock, get_cpuid

reset_handler:

// giống như được hiển thị ở trên

apStart: // Điểm vào AP

// giống như được hiển thị ở trên

irq_handler: // IRQ ngắt điểm vào

phụ lr, lr, #4

stmfd sp!, {r0-r12, lr} // lưu ngữ cảnh

bl irq_chandler // gọi irq_handler()

ldmfd sp!, {r0-r12, pc}^ // trả về

data_handler:

phụ lr, lr, #4

stmfd sp!, {r0-r12, lr}

bl data_abort_handler

ldmfd sp!, {r0-r12, pc}^

chuyển đổi: // tswitch() ở chế độ SVC

msr cpsr, #0x93 // ngắt

stmfd sp!, {r0-r12, lr}

ldr r4, =chạy // r4=&chạy

mrc p15, 0, r5, c0, c0, 5 // đọc thanh ghi CPUID tới r5

và r5, r5, #0x3 // chỉ có ID CPU

di chuyển r6, #4 // r6 = 4

mul r6, r5 // r6 = 4*cpuid

thêm r4, r6 // r4 = &run[cpuid]

ldr r6, [r4, #0] // r6->chạy PROC

str sp, [r6, #4] // lưu sp vào PROC.ksp

lịch trình bl // gọi bộ lập lịch() trong C


Machine Translated by Google

10.8 RTOS đa bộ xử lý (SMP_RTOS) 447

ldr r4, =chạy // r4=&chạy

mrc p15, 0, r5, c0, c0, 5 // đọc thanh ghi CPUID tới r5

và r5, r5, #0x3 // chỉ có ID CPU

di chuyển r6, #4 // r6 = 4

mul r6, r5 // r6 = 4*cpuid

thêm r4, r6 // r4 = &run[cpuid]

ldr r6, [r4, #0] // r6->chạy PROC

ldr sp, [r6, #4] msr // khôi phục ksp

cpsr, #0x13 ldmfd sp!, // ngắt trên

{r0-r12, pc} // tiếp tục chạy mới

int_off: // int sr=int_off()

stmfd sp!, {r1}

bà r1, cpsr

chuyển động r0, r1 // r0 = r1

hoặc r1, r1, #0x80

msr cpsr, r1 // che dấu IRQ

ldmfd sp!, {r1}

di chuyển máy tính, lr // trả về cpsr gốc

int_on: // int_on(SR)

bà CPSR, r0

di chuyển máy tính, lr

kho a: // tạo ra IRQ trong cpsr

bà r0, cpsr

hoặc r0, r0, #0x80

msr cpsr, r0

di chuyển máy tính, lr

mở khóa: // mặt nạ trong IRQ trong cpsr

bà r0, cpsr

bi r0, r0, #0x80

msr cpsr, r0

di chuyển máy tính, lr

.global send_sgi

// void send_sgi(int ID, int target_list, int filter_list)

send_sgi: // send_sgi với danh sách CPUTarget

và r3, r0, #0x0F // Che giấu các bit ID không sử dụng tới r3

và r1, r1, #0x0F // Che giấu các bit không sử dụng của target_filter

và r2, r2, #0x0F // Che giấu các bit không sử dụng của filter_list

orr r3, r3, r1, LSL #16 // Kết hợp ID và target_filter

orr r3, r3, r2, LSL #24 // và bây giờ là danh sách bộ lọc

// Lấy địa chỉ của GIC

mrc p15, 4, r0, c15, c0, 0 // Đọc địa chỉ cơ sở ngoại vi

thêm r0, r0, #0x1F00 // Thêm offset của reg sgi_trigger

str r3, [r0] // Ghi vào thanh ghi SGI(ICDSGIR)

bx lr

slock: // int slock(&spin)

ldrex r1, [r0]

cmp r1, #0x0

WENE

bn slock

mov r1, #1

strex r2, r1, [r0]

cmp r2, #0x0


Machine Translated by Google

448 10 hệ điều hành nhúng thời gian thực

bn slock

DMB // rào chắn

bx lr

nắng: // sunlock(&spin)

mov r1, #0x0

DMB

str r1, [r0]

DSB

SEV

bx lr

get_fault_status: // đọc và trả về MMU reg 5

mrc p15,0,r0,c5,c0,0 // đọc DFSR

di chuyển máy tính, lr

get_fault_addr: // đọc và trả về MMU reg 6

mrc p15,0,r0,c6,c0,0 // đọc DFSR

di chuyển máy tính, lr

get_cpsr:

bà r0, cpsr

di chuyển máy tính, lr

get_spsr:

bà r0, spsr

di chuyển máy tính, lr

get_cpuid:

mrc p15, 0, r0, c0, c0, 5 // Đọc thanh ghi ID CPU

và r0, r0, #0x03 // Tắt mặt nạ, để lại trường ID CPU

bx lr

.global allow_scu

// void allow_scu(void): Kích hoạt SCU

kích hoạt_scu:

mrc p15, 4, r0, c15, c0, 0 // Đọc địa chỉ cơ sở ngoại vi

ldr r1, [r0, #0x0] // Đọc thanh ghi điều khiển SCU

hoặc r1, r1, #0x1 // Đặt bit 0 (Bit Kích hoạt)

str r1, [r0, #0x0] // Viết lại giá trị đã sửa đổi

bx lr

vectơ_start:

PC LDR, reset_handler_addr

Máy tính LDR, undef_handler_addr

Máy tính LDR, svc_handler_addr

PC LDR, tìm nạp trước_abort_handler_addr

PC LDR, dữ liệu_abort_handler_addr

B .

Máy tính LDR, irq_handler_addr


PC LDR, fiq_handler_addr

đặt lại_handler_addr: .word reset_handler

undef_handler_addr: .word undef_handler

svc_handler_addr: .word svc_entry

prefetch_abort_handler_addr: .word prefetch_abort_handler

dữ liệu_abort_handler_addr: .word data_handler

irq_handler_addr: .word irq_handler

fiq_handler_addr: .word fiq_handler

vectơ_end:

// các hàm tiện ích SMP khác: KHÔNG HIỂN THỊ


Machine Translated by Google

10.8 RTOS đa bộ xử lý (SMP_RTOS) 449

(2). Tệp kernel.c của SMP_RTOS

#xác định NCPU 4

#define NPROC 256

#xác định SSIZE 1024

#định nghĩa MIỄN PHÍ 0

#define SẴN SÀNG 1

#xác định KHỐI 2

#định nghĩa ZOMBIE 3

quy trình cấu trúc typedef{

struct proc *next;

int *ksp; // con trỏ ngăn xếp đã lưu

int trạng thái; // MIỄN PHÍ|SẴN SÀNG|BLOCK|ZOMBIE, v.v.

int sự ưu tiên; // mức độ ưu tiên thực sự

int pid;

int ppid;

struct proc *parent;

int mã thoát; // mã thoát

int sẵn sàng_thời gian;

struct mbuf *mqueue; // Hàng đợi tin nhắn

cấu trúc mutex mlock; // khóa mutex hàng đợi tin nhắn

cấu trúc semaphore nmsg; // chờ tin nhắn semaphore

int cpuid; // CPU này đang chạy trên CPU nào

int ưu tiên; // mức độ ưu tiên hiệu quả cho PI

int khoảng thời gian; // để lập lịch theo thời gian

struct semaphore wchild; // chờ các con ZOMBIE

int kstack[SSIZE];

}THỦ TỤC;

#define đang chạy run[get_cpuid()]

// cấu trúc dữ liệu kernel và spinlocks

PROC iproc[NCPU]; // mỗi PROC ban đầu/không hoạt động của CPU

PROC *chạy[NCPU]; // con trỏ tới PROC đang chạy trên mỗi CPU

Quy trình PROC[NPROC]; // Cấu trúc PROC

PROC *danh sách miễn phí; // danh sách miễn phí

PROC *readyQueue[NCPU]; // mỗi CPU hàng đợi sẵn sàng

int sẵn sàng [NCPU]; // mỗi CPU ReadyQueue spinlcok

int intnest[NCPU]; // mỗi mức lồng ghép IRQ của CPU

int swflag[NCPU]; // mỗi cờ tác vụ chuyển đổi CPU

int freeListlock = 0; // spinlock freeList

int procsize = sizeof(PROC); // Kích thước PROC

int irq_stack[NCPU][1024]; // mỗi ngăn xếp chế độ IRQ của CPU

int abt_stack[NCPU][1024]; // mỗi ngăn xếp chế độ ABT của CPU

int und_stack[NCPU][1024]; // mỗi ngăn xếp chế độ UND của CPU

int kernel_init()

int tôi;

PROC *p; char *cp;

printf("kernel_init(): init iprocs\n");

cho (i=0; i<NCPU; i++){ // khởi tạo PROC ban đầu cho mỗi CPU

p = &iproc[i];

p->pid = 1000 + i; // pid đặc biệt cho các PROC nhàn rỗi trên mỗi CPU

p->trạng thái = SẴN SÀNG;


Machine Translated by Google

450 10 hệ điều hành nhúng thời gian thực

p->ppid = p->pid;

chạy[i] = p; // run[i] trỏ tới iPROC[i] đang rảnh

ReadyQueue[i] = 0; // hàng đợi sẵn sàng

khóa sẵn sàng[i] = 0; // spinlocks

intnest[i] = 0; // Các mức lồng IRQ

swflag[i] = 0; // chuyển cờ nhiệm vụ

cho (i=0; i<NPROC; i++){

p = &proc[i];

p->pid = tôi;

p->trạng thái = MIỄN PHÍ;

p->ưu tiên = 0;

p->ppid = 0;

p->tiếp theo = p + 1;

proc[NPROC-1].next = 0; // Danh sách PROC miễn phí

freeList = &proc[0];

Danh sách ngủ = 0;

khóa tự do = 0;

đang chạy = chạy[0]; // tác vụ ban đầu trên CPU0

int ttswitch() // chuyển tác vụ trên CPUID

int cpuid = get_cpuid();

kho a();

slock(&readylock[cpuid]);

tswitch();

sunlock(&readylock[cpuid]);

mở khóa();

bộ lập lịch int()

int pid; PROC *cũ=đang chạy;

int cpuid = get_cpuid();

slock(&readylock[cpuid];

if (running->pid < 1000){ // các tác vụ thông thường

if (đang chạy->trạng thái==SẴN SÀNG)

enqueue(&readyQueue[cpuid], đang chạy);

đang chạy = dequeue(&readyQueue[cpuid]);

nếu (đang chạy == 0)

đang chạy = &iproc[cpuid];// chạy tác vụ nhàn rỗi trên CPU

swflag[cpuid] = 0;

sunlock(&readylock[cpuid]);

int kfork(int func, int ưu tiên, int cpu)

int cpuid = get_cpuid();

PROC *p = getproc(&freeList); // cấp phát một PROC miễn phí

if (p==0){ printf("kfork1 bị lỗi\n"); trở lại -1 }

p->ppid = đang chạy->pid;

p->cha mẹ = đang chạy;

p->trạng thái = SẴN SÀNG;


Machine Translated by Google

10.8 RTOS đa bộ xử lý (SMP_RTOS) 451

p->ưu tiên = ưu tiên;

p->cpuid = cpuid

cho (i=1; i<15; i++) // tất cả 14 mục = 0

p->kstack[SSIZE-i] = 0;

p->kstack[SSIZE-1] = (int)func; // tiếp tục hoạt động

p->ksp = &(p->kstack[SSIZE-14]); // đã lưu ksp

slock(&readylock[cpu]);

enqueue(&readyQueue[cpu], p); // nhập p vào rQ[cpu]

sunlock(&readylock[cpu]);

if (p->ưu tiên > đang chạy->ưu tiên){

nếu (cpuid == cpu) { // nếu trên cùng một CPU

printf("%d PREEMPT %d trên CPU%d\n", p->pid, Running->pid, cpuid);

ttswitch();

trả về p->pid;

int P(struct semaphore *s) // Thao tác P trong SMP

int SR = int_off();

slock(&s->lock);

s->giá trị--;

nếu (s->giá trị < 0){

đang chạy->trạng thái = KHỐI;

enqueue(&s->queue, đang chạy);

// chỉ ưu tiên kế thừa nếu trên cùng một CPU

if (đang chạy->ưu tiên > s->chủ sở hữu->ưu tiên){

if (s->owner->cpu == đang chạy->cpu){

s->chủ sở hữu->ưu tiên = đang chạy->ưu tiên; // tăng mức độ ưu tiên của chủ sở hữu

sắp xếp lại_readyQueue(); // sắp xếp lại hàng đợi sẵn sàng

khóa nắng(&s->lock);

ttswitch();

khác{

s->chủ sở hữu = đang chạy; // với tư cách là chủ sở hữu của s

khóa nắng(&s->lock);

int_on(SR);

int V(struct semaphore *s) // Thao tác V trong SMP

PROC *p;

int SR = int_off();

slock(&s->lock);

s->giá trị++;

s->chủ sở hữu = 0; // xóa trường chủ sở hữu

nếu (s->giá trị <= 0){ // hàng đợi có người phục vụ

p = dequeue(&s->queue);

p->trạng thái = SẴN SÀNG;

s->chủ sở hữu = p; // chủ sở hữu mới


Machine Translated by Google

452 10 hệ điều hành nhúng thời gian thực

slock(&readylock[p->cpuid]); // rQ của CPU gốc

enqueue(&readyQueue[p->cpuid], p);

sunlock(&readylock[p->cpuid]);

đang chạy->ưu tiên = đang chạy->ưu tiên; // khôi phục quyền ưu tiên của chủ sở hữu

sắp xếp lại();

khóa nắng(&s->lock);

int_on(SR);

int kexit(int value) // kết thúc tác vụ

int tôi; PROC *p; if

(running->pid==1){ return -1 } // P1 không bao giờ chết

slock(&proclock); // lấy proclock

cho (i=2; i<NPROC; i++){

p = &proc[i];

if ((p->status != FREE) && (p->ppid == đang chạy->pid)){

if (p->status==ZOMBIE){

p->trạng thái = MIỄN PHÍ;

putproc(&freeList, p); // giải phóng bất kỳ con ZOMBIE nào

khác{

printf("gửi con %d tới P1\n", p->pid);

p->ppid = 1;

p->parent = &proc[1];

sunlock(&proclock); // phát hành proclock

đang chạy->exitCode = value;

đang chạy->trạng thái = ZOMBIE;

V(&running->parent->wchild);

ttswitch();

int kwait(int *trạng thái) // đợi bất kỳ con ZOMBIE nào

int i, nChild = 0;

PROC *p;

slock(&proclock); // lấy proclock

cho (i=2; i<NPROC; i++){

p = &proc[i];

if (p->status != FREE && p->ppid == đang chạy->pid)

nChild++;

sunlock(&proclock);

nếu (!nChild){ // không có lỗi con

trả về -1;

P(&running->wchild); // chờ một con ZOMBIE

slock(&proclock);

cho (i=2; i<NPROC; i++){

p = &proc[i];

if ((p->status==ZOMBIE) && (p->ppid == đang chạy->pid)){

*trạng thái = p->mã thoát;


Machine Translated by Google

10.8 RTOS đa bộ xử lý (SMP_RTOS) 453

p->trạng thái = MIỄN PHÍ;

putproc(&freeList, p);

sunlock(&proclock);

trả về p->pid;

(3). tập tin tc của SMP_RTOS

/*************** tệp tc của SMP_RTOS ***********/

#include "type.h"

int aplock = 0;

int ncpu = 1;

#include "string.c"

#include "queue.c"

#include "pv.c"

#include "uart.c"

#include "kbd.c"

#include "ptimer.c"

#include "vid.c"

#include "ngoại lệ.c"

#include "kernel.c"

#include "sdc.c"

#include "tin nhắn.c"

int copy_vectors(){ // giống như trước } #define GIC_BASE

0x1F000000

int config_gic()

// thiết lập thanh ghi mặt nạ ưu tiên int

*(int *)(GIC_BASE + 0x104) = 0xFFFF;

// thiết lập thanh ghi điều khiển giao diện CPU: cho phép ngắt tín hiệu

*(int *)(GIC_BASE + 0x100) = 1;

// thanh ghi điều khiển nhà phân phối để gửi các ngắt đang chờ xử lý tới CPU

*(int *)(GIC_BASE + 0x1000) = 1;

config_int(29, 0); // ptimer lúc 29 đến CPU0

config_int(44, 1); // UAR0 tới CPU1

config_int(49, 2); // SDC tới CPU0

config_int(52, 3); // KBD tới CPU1

int config_int(int N, int targetCPU)

int reg_offset, chỉ mục, giá trị, địa chỉ;

reg_offset = (N>>3)&0xFFFFFFFC;

chỉ số = N & 0x1F;

giá trị = 0x1 << chỉ số;

địa chỉ = (GIC_BASE + 0x1100) + reg_offset;

*(int *)địa chỉ |= giá trị;

reg_offset = (N & 0xFFFFFFFC);

chỉ số = N & 0x3;

địa chỉ = (GIC_BASE + 0x1800) + reg_offset + chỉ mục;

*(char *)address = (char)(1 << targetCPU);

địa chỉ = (GIC_BASE + 0x1400) + reg_offset + chỉ mục;

*(char *)địa chỉ = (char)0x88; // mức độ ưu tiên=8

}
Machine Translated by Google

454 10 hệ điều hành nhúng thời gian thực

int irq_chandler()

// đọc ICCIAR của giao diện CPU trong GIC

int intID = *(int *)(GIC_BASE + 0x10C);

if (intID == 29){ // bộ đếm thời gian cục bộ của CPU

ptimer_handler();

if (intID == 44){ // uart0

uart_handler(0);

if (intID == 49){ // SDC

sdc_handler();

nếu (intID == 52){ // KBD

kbd_handler();

*(int *)(GIC_BASE + 0x110) = intID; // phát hành EOI

int kdelay(int d){ // vòng lặp trễ }

int taskCode()

int cpuid = get_cpuid();

int pid = đang chạy->pid;

trong khi(1){

printf("TASK%d TRÊN CPU%d: ", pid, cpuid);

kdelay(pid*100000); // mô phỏng tính toán nhiệm vụ

ttswitch();

int INIT_task() // INIT nhiệm vụ P1

int i, pid, trạng thái;

// tạo 2 tác vụ trên các CPU khác nhau

cho (i=0; i<2; i++){

kfork((int)taskCode, 1, 0);

kfork((int)taskCode, 1, 1);

kfork((int)taskcode, 1, 2);

kfork((int)taskCode, 1, 3);

while(1){ // task1 chờ ZOMBIE con

pid = kwait(&status);

int logtask() // tác vụ ghi nhật ký P0

int pid;

int cpuid = get_cpuid();

thông điệp char[128];

printf("LOGtask%d khởi động trên CPU%d\n", Running->pid, cpuid);

while(1){ // task0 chờ tin nhắn

pid = recv(tin nhắn);

}
Machine Translated by Google

10.8 RTOS đa bộ xử lý (SMP_RTOS) 455

int run_task() // mỗi CPU cố gắng chạy tác vụ từ ReadyQ của chính nó

int cpuid = get_cpuid();

trong khi(1){

slock(&readylock[cpuid]);

if (readyQueue[cpuid] == 0){ // kiểm tra ReadyQueue của chính mình

sunlock(&readylock[cpuid]);

asm("WFE"); // chờ sự kiện/ngắt

khác{

sunlock(&readylock[cpuid]);

ttswitch(); // chuyển tác vụ trên CPU

int APstart()

int cpuid = get_cpuid();

slock(&aplock);

màu = VÀNG;

printf("CPU%d trong APstart … ", cpuid);

config_int(29, cpuid); // mỗi bộ đếm thời gian cục bộ của CPU

ptimer_init();

ptimer_start();

ncpu++;

đang chạy = &iproc[cpuid]; // chạy trên mỗi tác vụ ban đầu của CPU

printf("%d chạy trên CPU%d\n", Running->pid, cpuid);

khóa nắng(&aplock);

run_task();

int chính()

int cpuid = get_cpuid();

Enable_scu();

fbuf_init();

printf("Chào mừng đến với SMP_RTOS trong ARM\n");

sdc_init();

kbd_init();

uart_init();

ptimer_init();

ptimer_start();

config_gic();

printf("CPU0 khởi tạo kernel\n");

kernel_init();

printf("AP khởi động CPU0: ");

int *APaddr = (int *)0x10000030;

*APaddr = (int)apStart;

send_sgi(0x0, 0x0F, 0x01);

printf("CPU0 đợi AP sẵn sàng\n");

while(ncpu < 4);

printf("CPU0 tiếp tục\n");

kfork(int)logtask, 1, 0); // P0 trên CPU0 làm tác vụ đăng nhập

kfork((int)INIT_task, 1, 0); // P1 trên CPU0 dưới dạng tác vụ INIT

run_task();

}
Machine Translated by Google

456 10 hệ điều hành nhúng thời gian thực

Hình 10.5 Quản lý tác vụ trong SMP_RTOS

Mục tiêu của hạt nhân SMP_RTOS ban đầu là chứng minh rằng hệ thống có thể tạo và chạy các tác vụ trên các CPU khác nhau.

Khi hệ thống khởi động, CPU0 tạo và chạy tác vụ ban đầu P1000, gọi hàm main(). Trong main(), P1000 khởi tạo trình điều khiển thiết bị và nhân

hệ thống. Sau đó, nó kích hoạt các CPU thứ cấp (AP). Mỗi AP tự khởi tạo và chạy một tác vụ ban đầu P1000+cpuid, gọi run_task(), cố gắng chạy

các tác vụ từ ReadyQueue[cpuid] của chính nó. Sau khi kích hoạt các AP, P1000 tạo tác vụ ghi nhật ký P0 và tác vụ INIT P1 trên CPU0. Sau đó

P1000 chuyển tác vụ để chạy tác vụ INIT P1. P1 tạo hai tác vụ con trên mỗi CPU và đợi bất kỳ tác vụ con nào kết thúc. Mỗi tác vụ thực thi

một vòng lặp, trong đó nó trì hoãn một thời gian để mô phỏng tính toán tác vụ và gọi ttswitch() để chuyển đổi tác vụ. Trong hệ thống mẫu, các

tác vụ chạy vòng lặp liên tục.

Người đọc có thể kiểm tra hoạt động kwait() bằng cách sửa đổi hàm taskCode() để cho phép các tác vụ kết thúc. Hình 10.5 cho thấy các kết quả

đầu ra mẫu khi chạy chương trình C10.4, thể hiện việc quản lý tác vụ trong nhân SMP_RTOS.

10.8.2 Thích ứng UP_RTOS với SMP

Cách đơn giản nhất để phát triển RTOS cho bộ đa xử lý là điều chỉnh UP_RTOS cho các hoạt động MP. Theo cách tiếp cận này, hệ thống bao gồm

nhiều CPU. Mỗi CPU thực thi một tập hợp các tác vụ được liên kết với CPU và hoạt động trong môi trường cục bộ của CPU. Ưu điểm chính của

phương pháp này nằm ở sự đơn giản của nó. Cần rất ít thao tác để chuyển đổi UP_ROTS thành MP RTOS. Điểm bất lợi là hệ thống thu được chỉ

là hệ thống MP bất đối xứng chứ không phải hệ thống SMP. Chúng tôi minh họa một hệ thống như vậy, ký hiệu là MP_RTOS, bằng chương trình mẫu

C10.5. Chương trình bao gồm các thành phần sau.

(1). Tệp Ts.s: giống như trong UP_RTOS, ngoại trừ irq_hanler được đơn giản hóa, hỗ trợ chuyển đổi tác vụ nhưng không có các ngắt lồng nhau.

irq_handler: // IRQ ngắt điểm vào

phụ lr, lr, #4

stmfd sp!, {r0-r12, lr} // lưu tất cả các reg Umode trong kstack

bà r0, spsr

stmfd sp!, {r0} bl

irq_chandler
Machine Translated by Google

10.8 RTOS đa bộ xử lý (SMP_RTOS) 457

// kiểm tra giá trị trả về: 0=nornal, 1=swflag đã được đặt => tác vụ swtich

cmp r0, #0

bgt doswitch

ldmfd sp!, {r0}

msr spsr, r0

ldmfd sp!, {r0-r12, pc}^

doswitch:

msr cpsr, #0x93 stmfd

sp!, {r0-r3, lr}

bl ttswitch

msr cpsr, #0x93 ldmfd

sp!, {r0-r3, lr} msr cpsr, #0x92

ldmfd sp!, {r0}

msr spsr, r0

ldmfd sp!, {r0-r12, pc}^

(2). Tệp kernel.c: giống như trong UP_RTOS để quản lý tác vụ, ngoại trừ việc chúng tôi xác định

cấu trúc semaphore sem[NCPU];

và khởi tạo mỗi sem.value = 0. Mỗi semaphore được liên kết với một CPU, chỉ có thể được truy cập bởi các tác vụ thực thi trên cùng một CPU.

Nếu một tác vụ bị chặn trên một đèn hiệu, nó sẽ được bộ đếm thời gian cục bộ của CPU bỏ chặn theo định kỳ.

khoảng trống ptimer_handler()

int cpuid = id = get_cpuid(); struct ttt *tp

= &tt[cpuid]; slock(&plock); // cập nhật

tick, ss, mm, hh:

giống như trước if (tp->tick==0){ // mỗi giây: hiển thị đồng hồ

treo tường

// hiển thị đồng hồ treo tường: giống như trước

if ((tp->ss % 5)==0) // cứ sau 5 giây, hãy kích hoạt một tác vụ

V(&sem[cpuid]); // V để bỏ chặn một tác vụ đang chờ

khóa nắng(&plock);

ptimer_clearInterrupt(); // xóa ngắt hẹn giờ

(3). tc: Tương tự như trong UP_RTOS để quản lý tác vụ, ngoại trừ những sửa đổi sau. Thay vì tạo tác vụ trên các CPU khác nhau, tác vụ ban đầu

của mỗi CPU sẽ tạo một tác vụ để thực thi INIT_task() trên cùng một CPU. Mỗi tác vụ tạo một tác vụ khác để thực thi taskCode(), cũng trên cùng

một CPU. Tất cả các tác vụ đều có cùng mức độ ưu tiên 1. Mỗi tác vụ thực thi một vòng lặp vô hạn, trong đó nó trì hoãn một thời gian để mô phỏng

quá trình tính toán tác vụ và sau đó tự chặn nó trên một semaphore được liên kết với CPU. Bộ định thời cục bộ của mỗi CPU định kỳ gọi V để bỏ

chặn các tác vụ, cho phép chúng tiếp tục. Bất cứ khi nào một tác vụ sẵn sàng được đưa vào hàng đợi lập lịch cục bộ của CPU, nó có thể ưu tiên

tác vụ đang chạy trên cùng một CPU.

/************ tệp tc của MP_RTOS C10.5 *************/

int run_task(){ // giống như trước

int kdelay(int d){ // độ trễ }

int taskCode()

int cpuid = get_cpuid();

trong khi(1){
Machine Translated by Google

458 10 hệ điều hành nhúng thời gian thực

printf("TASK%d TRÊN CPU%d\n", đang chạy->pid, cpuid);

kdelay(đang chạy->pid*10000);

reset_sem(&sem[cpuid]); // đặt lại sem.value về 0

P(&sem[cpuid]);

int INIT_task()

int cpuid = get_cpuid();

kfork((int)taskCode, 1, cpuid);

trong khi(1){

printf("task%d trên CPU%d\n", Running->pid, cpuid);

kdelay(đang chạy->pid*10000);

reset_sem(&sem[cpuid]); // đặt lại sem.value về 0

P(&sem[cpuid]);

int APstart()

int cpuid = get_cpuid();

slock(&aplock);

printf("CPU%d trong APstart: ", cpuid);

config_int(29, cpuid); // cần cái này cho mỗi CPU

ptimer_init(); ptimer_start();

ncpu++;

đang chạy = &iproc[cpuid];

printf("%d chạy trên CPU%d\n", Running->pid, cpuid);

khóa nắng(&aplock);

kfork((int)INIT_task, 1, cpuid);

run_task();

int chính()

// khởi tạo trình điều khiển thiết bị, GIC và kernel: giống như trước

kprintf("CPU0 tiếp tục\n");

kfork((int)f1, 1, 0);

run_task();

10.8.2.1 Trình diễn MP_RTOS Hình 10.6 cho thấy

kết quả đầu ra khi chạy hệ thống MP_RTOS.

10.8.3 Các ngắt lồng nhau trong SMP_RTOS

Trong hạt nhân UP RTOS, chỉ có một CPU xử lý tất cả các ngắt. Để đảm bảo phản hồi nhanh với các ngắt, điều quan trọng là phải cho phép

các ngắt lồng nhau. Trong nhân SMP RTOS, các ngắt thường được định tuyến đến các CPU khác nhau để cân bằng tải xử lý ngắt.

Mặc dù vậy, vẫn cần phải cho phép các ngắt lồng nhau. Chẳng hạn, mỗi CPU có thể sử dụng bộ định thời cục bộ, bộ định thời này tạo ra

các ngắt định thời có mức độ ưu tiên cao. Nếu không có các ngắt lồng nhau, trình điều khiển thiết bị có mức độ ưu tiên thấp có thể

chặn các ngắt của bộ hẹn giờ, khiến bộ hẹn giờ bị mất dấu tích. Trong phần này, chúng tôi sẽ trình bày cách triển khai các ngắt lồng

nhau trong nhân SMP_RTOS bằng chương trình mẫu C10.6. Nguyên tắc giống như trong UP_RTOS, ngoại trừ điểm khác biệt nhỏ sau.

Tất cả các bộ xử lý ARM MPcore đều sử dụng GIC để điều khiển ngắt. Để hỗ trợ các ngắt lồng nhau, trình xử lý ngắt phải đọc thanh

ghi ID ngắt của GIC để xác nhận ngắt hiện tại trước khi kích hoạt các ngắt. Thanh ghi ID ngắt của GIC
Machine Translated by Google

10.8 RTOS đa bộ xử lý (SMP_RTOS) 459

Hình 10.6 Trình diễn hệ thống MP_RTOS

chỉ có thể được đọc một lần. Trình xử lý ngắt phải sử dụng ID ngắt để gọi irq_chandler(int intID) trong C. Đối với SGI đặc biệt
các ngắt (0-15), chúng phải được xử lý riêng vì chúng có mức độ ưu tiên ngắt cao và không cần bất kỳ sự lồng ghép nào.

.set GICreg, 0x1F00010C // Đăng ký GIC intID

.set GICeoi, 0x1F000110 // đăng ký GIC EOI

irq_handler:

phụ lr, lr, #4

stmfd sp!, {r0-r12, lr}

bà r0, spsr

stmfd sp!, {r0} // đẩy SPSR

// đọc GICreg để lấy dòng ngắt intID và ACK

ldr r1, =GICreg

ldr r0, [r1]

// xử lý trực tiếp intID SGI

cmp r0, #15 // SGI ngắt

lồng bgt

ldr r1, =GICeoi

str r0, [r1] // phát hành EOI


b trở lại

làm tổ:

// sang chế độ SVC

msr cpsr, #0x93 // Chế độ SVC tắt IRQ

stmfd sp!, {r0-r3, lr}

msr cpsr, #0x13 // Chế độ SVC có ngắt IRQ

bl irq_chandler // gọi irq_chandler(intID)


Machine Translated by Google

460 10 hệ điều hành nhúng thời gian thực

msr cpsr, #0x93 // Chế độ SVC tắt IRQ

ldmfd sp!, {r0-r3, lr} //

msr cpsr, #0x92 // Chế độ IRQ có ngắt

trở lại:

ldmfd sp!, {r0}

msr spsr, r0

ldmfd sp!, {r0-r12, pc}^

int irq_chandler(int intID) // được gọi với ID ngắt

int cpuid = get_cpuid();

int cpsr = get_cpsr() & 0x9F; // CPSR với |IRQ|chế độ| mặt nạ

intnest[cpuid]++; // intsest by 1

nếu (intID != 29)

printf("IRQ%d trên CPU%d ở chế độ=%x\n", intID, cpuid, cpsr);

nếu (intID == 29){ // bộ đếm thời gian cục bộ của CPU

ptimer_handler();

}
nếu (intID == 44){ // ngắt UART0

uart_handler(0);

}
nếu (intID == 49){ // ngắt SDC

sdc_handler();

}
nếu (intID == 52){ // ngắt KBD

kbd_handler();

}
*(int *)(GIC_BASE + 0x110) = intID; // phát hành EOF

intnest[cpuid]--; // giảm số nguyên bằng 1

Hình 10.7 cho thấy kết quả đầu ra khi chạy chương trình C10.6, minh họa SMP_RTOS với các ngắt lồng nhau.
Như hình minh họa, tất cả các ngắt được xử lý ở chế độ SVC.

Hình 10.7 Các ngắt lồng nhau trong SMP_RTOS


Machine Translated by Google

10.8 RTOS đa bộ xử lý (SMP_RTOS) 461

10.8.4 Lập lịch tác vụ ưu tiên trong SMP_RTOS

Để đáp ứng thời hạn nhiệm vụ, mọi ROTS phải hỗ trợ lập kế hoạch nhiệm vụ ưu tiên để đảm bảo các nhiệm vụ có mức độ ưu tiên cao có thể được thực thi càng sớm

càng tốt. Trong phần này, chúng tôi sẽ trình bày cách triển khai lập lịch tác vụ ưu tiên trong nhân SMP_RTOS và thể hiện tính năng lập lịch tác vụ ưu tiên bằng

chương trình mẫu C10.7. Vấn đề ưu tiên nhiệm vụ có thể được phân thành hai trường hợp chính.

Trường hợp 1: Khi một tác vụ đang chạy khiến một tác vụ khác có mức độ ưu tiên cao hơn sẵn sàng chạy, ví dụ: khi tạo một tác vụ mới với

mức độ ưu tiên cao hơn hoặc bỏ chặn một nhiệm vụ có mức độ ưu tiên cao hơn.

Trường hợp 2: Khi trình xử lý ngắt làm cho một tác vụ có thể chạy được, ví dụ: khi bỏ chặn một tác vụ có mức độ ưu tiên cao hơn hoặc khi tác vụ đang chạy

hiện tại đã cạn kiệt lượng thời gian của nó trong lập lịch vòng tròn theo thời gian.

Trong kernel UP, việc thực hiện ưu tiên nhiệm vụ rất đơn giản. Trong trường hợp đầu tiên, bất cứ khi nào một tác vụ khiến một tác vụ khác sẵn sàng chạy,

nó chỉ cần kiểm tra xem tác vụ mới có mức độ ưu tiên cao hơn hay không. Nếu vậy, nó sẽ yêu cầu chuyển đổi tác vụ để tự ưu tiên ngay lập tức. Đối với trường

hợp thứ hai, tình hình chỉ phức tạp hơn một chút. Với các ngắt lồng nhau, trình xử lý ngắt phải kiểm tra xem tất cả quá trình xử lý ngắt lồng nhau đã kết thúc

chưa. Nếu vậy, nó có thể gọi chuyển đổi tác vụ để ưu tiên tác vụ đang chạy. Mặt khác, nó trì hoãn việc chuyển đổi tác vụ cho đến khi kết thúc quá trình xử lý

ngắt lồng nhau.

Ở SMP, tình hình rất khác. Trong nhân SMP, một thực thể thực thi, tức là một tác vụ hoặc một trình xử lý ngắt, việc thực thi trên một CPU có thể khiến một

tác vụ khác sẵn sàng chạy trên một CPU khác. Nếu vậy, nó phải thông báo cho CPU khác sắp xếp lại các nhiệm vụ để có thể được ưu tiên thực hiện nhiệm vụ. Trong

trường hợp này, nó phải đưa ra một SGI, ví dụ với ngắt ID=2, tới CPU khác. Đáp lại ngắt SGI_2, CPU mục tiêu có thể thực thi trình xử lý ngắt SGI_2 để sắp xếp

lại các tác vụ. Chúng tôi minh họa việc chuyển đổi tác vụ dựa trên SGI bằng chương trình mẫu C10.7. Để cho ngắn gọn, chúng tôi chỉ hiển thị các đoạn mã có liên

quan.

10.8.5 Chuyển tác vụ bằng SGI

/************ irq_handler() trong tệp ts.s *************/

irq_handler: // IRQ ngắt điểm vào

phụ lr, lr, #4

stmfd sp!, {r0-r12, lr} // lưu tất cả các reg Umode trong kstack

bl irq_chandler // gọi irq_handler() trong C trong tệp svc.c // return value=1 có nghĩa là swflag

được đặt => tác vụ swtich

cmp r0, #0

lợi ích của việc chuyển đổi

ldmfd sp!, {r0-r12, pc}^ // trả về

doswitch:

msr cpsr, #0x93 stmfd sp!,

{r0-r3, lr}

bl ttswitch // chuyển tác vụ ở chế độ SVC

ldmfd sp!, {r0-r3, lr}

msr cpsr, #0x92 // trả về theo ngữ cảnh trong ngăn xếp IRQ

ldmfd sp!, {r0-r12, pc}^

/*************** tập tin tc ****************************/

int send_sgi(int ID, int targetCPU, int filter)

int target = (1 << targetCPU); // đặt bit ID CPU

if (targetCPU > 3) // > 3 có nghĩa là tất cả các CPU

mục tiêu = 0xF; // bật cả 4 bit CPU ID

int *sgi_reg = (int *)(GIC_BASE + 0x1F00);

*sgi_reg = (lọc << 24) | (mục tiêu<<16) | NHẬN DẠNG;

int SGI2_handler()

int cpuid = get_cpuid(); printf("%d

trên CPU%d có SGI_2: set swflag\n", Running->pid, cpuid);


Machine Translated by Google

462 10 hệ điều hành nhúng thời gian thực

swflag[cpuid] = 1;

int irq_chandler()

int intID = *(int *)(GIC_BASE + 0x10C); int cpuid = get_cpuid();

nếu (intID == 2){

SGI2_handler();

// các trường hợp intID khác: giống như trước

*(int *)(GIC_BASE + 0x110) = (cpuid<<10)|intID; // phát hành EOI

if (swflag[cpuid]){ // trả về 1 cho chuyển đổi tác vụ trong irq_handler

swflag[cpuid] = 0;

trả về 1;

trả về 0;

int kdelay(int d){ // độ trễ để mô phỏng tính toán tác vụ }

int f2()

int cpuid = get_cpu_id();

trong khi(1){

printf("TASK%d ĐANG CHẠY TRÊN CPU%d: ", Running->pid, cpuid);

kdelay(đang chạy->pid*10000); // mô phỏng thời gian tính toán tác vụ

int f1()

int cpuid = get_cpu_id();

trong khi(1){

printf("task%d chạy trên CPU%d: ", Running->pid, cpuid); kdelay(đang chạy->pid*100000); //

mô phỏng thời gian tính toán tác vụ

int chính()

// khởi động AP: giống như trước

printf("CPU0 tiếp tục\n");

kfork((int)f1, 1, 1); // tạo tác vụ trên CPU1

kfork((int)f2, 1, 1);

kfork((int)f1, 1, 2); // tạo tác vụ trên CPU2

kfork((int)f2, 1, 2);

trong khi(1){

printf("nhập một dòng: ");

kgetline(dòng); printf("\n");

send_sgi(2, 1, 0); // SGI_2 tới CPU1

send_sgi(2, 2, 0); // SGI_2 tới CPU2

Trong chương trình mẫu C10.7, sau khi khởi động các AP, chương trình chính thực thi trên CPU0 sẽ tạo ra hai nhóm tác vụ
trên cả CPU1 và CPU2, tất cả đều có cùng mức ưu tiên 1. Tất cả các tác vụ đều thực hiện một vòng lặp vô hạn. Nếu không có đầu
vào bên ngoài, mỗi CPU sẽ tiếp tục thực hiện cùng một nhiệm vụ mãi mãi. Sau khi tạo các tác vụ trên các CPU khác nhau, chương
trình chính sẽ nhắc nhập dòng đầu vào từ bàn phím. Sau đó, nó gửi một SGI có intID=2 đến các CPU khác, khiến chúng thực thi
SGI2_handler. Trong SGI2_handler, mỗi CPU bật cờ tác vụ chuyển đổi của nó và trả về giá trị 1. Trong mã irq_handler, nó sẽ kiểm tra
Machine Translated by Google

10.8 RTOS đa bộ xử lý (SMP_RTOS) 463

Hình 10.8 Chuyển đổi tác vụ bằng SGI trong SMP_RTOS

giá trị trả về. Nếu giá trị trả về khác 0, biểu thị nhu cầu chuyển đổi tác vụ, nó sẽ chuyển sang chế độ SVC và gọi ttswitch() để chuyển

tác vụ. Khi tác vụ đã tắt tiếp tục, nó sẽ quay trở lại điểm bị gián đoạn ban đầu theo ngữ cảnh đã lưu trong ngăn xếp IRQ. Hình 10.8 cho

thấy kết quả đầu ra của việc chuyển đổi tác vụ bằng SGI. Như hình minh họa, mỗi dòng đầu vào sẽ gửi SGI-2 đến cả CPU1 và CPU2, khiến

chúng chuyển đổi nhiệm vụ.

10.8.6 Lập lịch tác vụ theo thời gian trong SMP_RTOS

Trong RTOS, các tác vụ có thể có cùng mức độ ưu tiên. Thay vì chờ đợi các tác vụ như vậy tự nguyện từ bỏ CPU, có thể mong muốn hoặc thậm

chí cần thiết lập lịch trình cho chúng theo vòng tròn với các khoảng thời gian. Trong sơ đồ này, khi một tác vụ được lên lịch để chạy,

nó sẽ được cấp một khoảng thời gian là khoảng thời gian tối đa mà tác vụ được phép chạy. Trong trình xử lý ngắt hẹn giờ, nó giảm khoảng

thời gian của tác vụ đang chạy theo định kỳ. Khi khoảng thời gian của tác vụ đang chạy đạt đến 0, nó sẽ được ưu tiên chạy một tác vụ khác.

Chúng tôi minh họa việc lập lịch tác vụ theo khoảng thời gian trong khung SMP_RTOS bằng một ví dụ. Chương trình ví dụ, C10.8, giống như

C10.7 ngoại trừ những sửa đổi sau.

/************ file kernel.c của C10.8 ***********/

bộ lập lịch int()

int pid; PROC *cũ=đang chạy;


int cpuid = get_cpuid();
if (running->pid < 1000){ // chỉ các tác vụ thông thường
if (đang chạy->trạng thái==SẴN SÀNG)

enqueue(&readyQueue[cpuid], đang chạy);


Machine Translated by Google

464 10 hệ điều hành nhúng thời gian thực

đang chạy = dequeue(&readyQueue[cpuid]); nếu (đang chạy ==

0) // nếu hàng đợi sẵn sàng của CPU trống

đang chạy = &iproc[cpuid]; // chạy tác vụ nhàn rỗi

đang chạy->thời gian = 4; // 4 giây thời gian

swflag[cpuid] = 0;

sunlock(&readylock[cpuid]);

/********* file ptimer.c của C10.8 ****************/ void ptimer_handler()

// trình xử lý hẹn giờ cục bộ

int cpuid = get_cpuid(); // cập nhật

tích tắc, hiển thị đồng hồ treo tường: giống như trước

ptimer_clearInterrupt(); // xóa ngắt hẹn giờ

nếu (tp->tick == 0){ // môi giây

if (running->pid < 1000){// chỉ các tác vụ thông thường

đang chạy->thời gian--;

printf("%dtime=%d ", Running->pid, Running->timeslice);

if (running->timeslice <= 0){

printf("%d trên CPU%d hết thời gian\n", đang chạy->pid, cpuid);

swflag[cpuid] = 1; // đặt swflag o chuyển tác vụ ở cuối IRQ

/*************** tệp tc của C10.8 ****************/

int f3()

int cpuid = get_cpuid();

trong khi(1){

printf("TASK%d TRÊN CPU%d\n", đang chạy->pid, cpuid); kdelay(đang chạy->pid *

100000); // mô phỏng tính toán

int f2()

int cpuid = get_cpuid();

trong khi(1){

printf("task%d trên cpu%d\n", Running->pid, cpuid);

kdelay(đang chạy->pid * 100000); // mô phỏng tính toán

int f1()

int pid, trạng thái;

kfork((int)f2, 1, 1); // tạo tác vụ trên CPU1

kfork((int)f3, 1, 1); trong khi(1) // tạo tác vụ trên CPU1

pid = kwait(&status); // P1 đợi ZOMBIE con

int run_task(){ // giống như trước }

int chính()

// khởi động AP" giống như trước


Machine Translated by Google

10.8 RTOS đa bộ xử lý (SMP_RTOS) 465

Hình 10.9 Lập lịch tác vụ theo thời gian trong SMP_RTOS

printf("CPU0 tiếp tục\n");

kfork((int)f1, 1, 0); // tạo tác vụ 1 trên CPU0

run_task();

Trong chương trình mẫu C10.8, tác vụ ban đầu của CPU0 tạo task1 để thực thi f1() trên CPU0. Task1 tạo task2 và task3 trên
CPU1 với mức độ ưu tiên như nhau. Sau đó, task1 đợi bất kỳ tác vụ con nào kết thúc. Vì cả task2 và task3 đều có cùng mức độ ưu
tiên nên nếu không cắt thời gian, CPU1 sẽ tiếp tục chạy task2 mãi mãi. Với tính năng chia thời gian, task2 và task3 sẽ lần lượt
chạy, mỗi lần chạy trong một khoảng thời gian = 4 giây. Hình 10.9 cho thấy kết quả đầu ra của việc chạy chương trình C10.8, thể
hiện việc lập kế hoạch tác vụ theo thời gian.

10.8.7 Lập lịch tác vụ ưu tiên trong SMP_RTOS

Dựa trên các cuộc thảo luận ở trên, giờ đây chúng tôi trình bày cách triển khai lập lịch tác vụ ưu tiên trong hạt nhân SMP_ROTS.

10.8.7.1 Chức năng lập lịch lại cho ưu tiên nhiệm vụ

int sắp xếp lại (PROC *p)

int cpuid = get_cpuid(); // thực thi CPU int targetCPU = p-

>cpuid; // CPU mục tiêu if (cpuid == targetCPU){ // nếu cùng

một CPU

if (readyQueue[cpuid]->priority > Running->priority){ if (intnest[cpuid]==0){ //

ƯU ĐÃI ngay lập tức

ttswitch();
Machine Translated by Google

466 10 hệ điều hành nhúng thời gian thực

khác{ // trì hoãn cho đến khi kết thúc các IRQ lồng nhau

swflag[cpuid] = 1;

khác{ // CPU khác nhau

send_sgi(intID=2, CPUID=targetCPU, filter=0);

10.8.7.2 Tạo nhiệm vụ với quyền ưu tiên

int kfork(int func, int ưu tiên, int cpu)

int cpuid = get_cpuid(); // thực thi CPU // tạo một tác vụ mới

p, nhập vào ReadyQueue[cpuid] như trước reschedule(p); trả về p->pid;

// gọi lại lịch trình lại

10.8.7.3 Semaphore có quyền ưu tiên

int V(struct semaphore *s)

PROC *p = 0;

int SR = int_off();

slock(&s->lock);

s->giá trị++;

nếu (s->giá trị <= 0){

p = dequeue(&s->queue);

p->trạng thái = SẴN SÀNG;

slock(&readylock[p->cpuid]);

enqueue(&readyQueue[p->cpuid], p);

sunlock(&readylock[p->cpuid]);

khóa nắng(&s->lock);

int_on(SR);

Nếu p) // nếu đã bỏ chặn người phục vụ

sắp xếp lại();

10.8.7.4 Mutex có quyền ưu tiên

int mutex_unlock(MUTEX *s)

PROC *p = 0;

int SR = int_off();

slock(&s->lock) // thu được spinlock

// ĐÁNH GIÁ: mutex có người phục vụ: bỏ chặn một người là chủ sở hữu mới

p = dequeue(&s->queue);
Machine Translated by Google

10.8 RTOS đa bộ xử lý (SMP_RTOS) 467

p->trạng thái = SẴN SÀNG;

s->chủ sở hữu = p;

slock(&readylock[p->cpuid]); enqueue(&readyQueue[p-

>cpuid], p);

sunlock(&readylock[p->cpuid]); khóa nắng(&s-

>lock); // giải phóng spinlock

int_on(SR);

Nếu p) // nếu đã bỏ chặn người phục vụ

sắp xếp lại();

10.8.7.5 IRQ lồng nhau ngắt với quyền ưu tiên

int SGI2_handler()

int cpuid = get_cpuid();

PROC *p = ReadyQueue[cpuid]; if (p && p->ưu

tiên > đang chạy->ưu tiên){

swflag[cpuid] = 1; // đặt cờ chuyển đổi tác vụ

int irq_chandler(int intID) // được gọi với ID ngắt

int cpuid = get_cpuid();

intnest[cpuid]++; // tăng số lần đếm lên 1

nếu (intID == 2){ // ngắt SGI_2

SGI2_handler();

// các trình xử lý ngắt IRQ khác: giống như trước

*(int *)(GIC_BASE + 0x110) = (cpuID<<10)|intID; // phát hành EOI

intnest[cpuid]--; // số intnest tháng 12 tăng thêm 1

int checkIRQnesting()// trả về 1 nếu kết thúc lồng IRQ && swflag được đặt

int cpuid = get_cpuid(); if

(intnest[cpuid] == 0 && swflag[cpuid])

trả về 1;

trả về 0;

/*** irq_handler với các ngắt lồng nhau VÀ SGI_2 ****/ .set GICreg, 0x1F00010C // GIC intID

register .set GICeoi, 0x1F000110 // GIC EOI register

irq_handler:

phụ lr, lr, #4

stmfd sp!, {r0-r12, lr}

bà r0, spsr

stmfd sp!, {r0} // đẩy SPSR

// đọc GICreg để lấy intID

ldr r1, =GICreg

ldr r0, [r1]

// xử lý trực tiếp SGI intID=0

cmp r0, #0 // SGI 0 được sử dụng khi khởi động


lồng bgt

ldr r1, =GICeoi


Machine Translated by Google

468 10 hệ điều hành nhúng thời gian thực

str r0, [r1] // phát hành EOI


b trở lại

làm tổ: // sang chế độ SVC

msr cpsr, #0x93 // Chế độ SVC tắt IRQ

stmfd sp!, {r0-r3, lr}

msr cpsr, #0x13 // Chế độ SVC có bật ngắt

bl irq_chandler msr // irq_chandler return 1: chuyển đổi tác vụ

cpsr, #0x93 // Chế độ SVC tắt IRQ

bl checkIRQnesting // trả về 1 nếu OK để chuyển tác vụ

cmp r0, #0

bgt doswitch

ldmfd sp!, {r0-r3, lr}

b trở lại

doswitch:

stmfd sp!, {r0-r3, lr}

bl ttswitch // tác vụ chuyển sang chế độ SVC

ldmfd sp!, {r0-r3, lr}

trở lại:

msr cpsr, #0x92 // Chế độ IRQ có ngắt

ldmfd sp!, {r0}

msr spsr, r0

ldmfd sp!, {r0-r12, pc}^

/************ kết thúc irq_handler *******************/

10.8.8 Kế thừa ưu tiên

Trong nhân SMP_RTOS, spinlock, mutexes và semaphores được sử dụng để đồng bộ hóa quy trình. Spinlocks là

non-blocking, tức là chúng không gây ra hiện tượng chuyển ngữ cảnh nên không cần ưu tiên kế thừa. Mutexes và semaphores là

chặn, có thể gây ra chuyển đổi nhiệm vụ, vì vậy chúng phải hỗ trợ kế thừa ưu tiên. Thực hiện kế thừa ưu tiên trong

SMP tương tự như UP_RTOS, ngoại trừ những khác biệt sau. Khi một tác vụ sắp bị chặn trên một

mutex hoặc semaphore, nó có thể phải tăng mức độ ưu tiên của chủ sở hữu mutex hoặc semaphore, có thể đang chạy trên một

CPU khác nhau. Nếu chủ sở hữu mutex hoặc semaphore và tác vụ hiện tại nằm trên cùng một CPU thì tình huống cũng giống như trong

UP_RTOS. Ngược lại, tác vụ hiện tại phải gửi SGI tới CPU của chủ sở hữu mutex hoặc semaphore, khiến nó phải điều chỉnh

ưu tiên nhiệm vụ và sắp xếp lại các nhiệm vụ trên CPU mục tiêu. Đề án có thể được thực hiện như sau. Khi một nhiệm vụ sắp hoàn thành

bị chặn trên mutex hoặc semaphore:

(1). Viết [pid, ưu tiên] vào bộ nhớ dùng chung chuyên dụng, trong đó pid là PID của chủ sở hữu mutex/semaphore và mức độ ưu tiên là

ưu tiên mới.

(2). Cấp SGI_3 cho CPU của tác vụ chủ sở hữu mutex/semaphore

(3). SGI_3_handler trên CPU mục tiêu:

Nhận [pid, ưu tiên] từ bộ nhớ dùng chung;

Tăng mức độ ưu tiên của nhiệm vụ mục tiêu và sắp xếp lại nhiệm vụ

Phần sau đây trình bày cách triển khai kế thừa ưu tiên cho các mutex trong SMP_ROTS. Việc thực hiện ưu tiên

kế thừa cho các semaphores là tương tự.

biến động int PID[NCPU]; // bộ nhớ dùng chung cho bộ xử lý SGI_3

int mutex_lock(MUTEX *s)

{
PROC *p;

int cpuid = get_cpuid();

int SR = int_off();
Machine Translated by Google

10.8 RTOS đa bộ xử lý (SMP_RTOS) 469

slock(&s->lock); // khóa quay

if (s->state==UNLOCKED{ // mutex ở trạng thái mở khóa

s->trạng thái = KHÓA;

s->chủ sở hữu = đang chạy;

khóa nắng(&s->lock);

int_on(SR);

trả về 1;

/************** mutex đã bị khóa ******************/ if (s->owner == đang chạy){ // đã bị

khóa bởi tác vụ này printf("mutex đã bị bạn khóa!\n");

khóa nắng(&s->lock);

int_on(SR);

trả về 1;

printf("TASK%d KHỐI TRÊN MUTEX: ", đang chạy->pid);

đang chạy->trạng thái = KHỐI;

enqueue(&s->queue, đang chạy);

// tăng mức độ ưu tiên của chủ sở hữu lên mức độ ưu tiên của việc chạy

if (đang chạy->ưu tiên > s->chủ sở hữu->ưu tiên){

if (s->owner->cpuid == cpuid){ // cùng một CPU

s->chủ sở hữu->ưu tiên = đang chạy->ưu tiên; // tăng mức độ ưu tiên của chủ sở hữu

else{ // không cùng CPU

PID[s->owner-cpuid] = s->owner->pid; // pid của chủ sở hữu send_sgi(3, s->owner-

>cpuid, 0); // gửi SGI tới nhiệm vụ nhận lại

khóa nắng(&s->lock);

tswitch(); // chuyển nhiệm vụ

int_on(SR);

trả về 1;

int mutex_unlock(MUTEX *s)

PROC *p;

int cpuid = get_cpuid();

int SR = int_off();

slock(&s->lock); // khóa quay

if (s->state==UNLOCKED || s->owner != đang chạy){

printf("%d mutex_unlock error\n", Running->pid);

khóa nắng(&s->lock);

int_on(SR);

trả về 0;

// người gọi là chủ sở hữu và mutex đã bị khóa

nếu (s->hàng đợi == 0){ // mutex không có người phục vụ

s->state = MỞ KHÓA; // xóa trạng thái khóa

s->chủ sở hữu = 0; // xóa chủ sở hữu

đang chạy->ưu tiên = đang chạy->ưu tiên;

else{ // mutex có người phục vụ: bỏ chặn một người với tư cách là chủ sở hữu mới

p = dequeue(&s->queue);

p->trạng thái = SẴN SÀNG;

s->chủ sở hữu = p;
Machine Translated by Google

470 10 hệ điều hành nhúng thời gian thực

slock(&readylock[p->cpuid]);

enqueue(&readyQueue[p->cpuid], p);

sunlock(&readylock[p->cpuid]);

đang chạy->ưu tiên = đang chạy->realPriority; // khôi phục mức độ ưu tiên

if (p->cpuid == cpuid){ // cùng một CPU

if (p->ưu tiên > đang chạy->ưu tiên){

nếu (intnest==0) // không nằm trong bộ xử lý IRQ

ttswitch(); // quyền ưu tiên ngay bây giờ

khác{

swflag[cpuid] = 1; // trì hoãn việc ưu tiên cho đến khi kết thúc IRQ

else // người gọi và p trên các CPU khác nhau => SGI tới CPU của p

send_sgi(2, p->cpuid, 0); // gửi SGI để sắp xếp lại nhiệm vụ

khóa nắng(&s->lock);

int_on(SR);

trả về 1;

10.8.9 Trình diễn hệ thống SMP_RTOS

Hệ thống SMP_RTOS tích hợp tất cả các chức năng được thảo luận ở trên để cung cấp cho hệ thống những tính năng sau:

khả năng.

(1). Hạt nhân SMP để quản lý tác vụ. Các tác vụ được phân phối tới các CPU khác nhau để cải thiện tính đồng thời trong SMP.

(2). Lập kế hoạch nhiệm vụ ưu tiên theo mức độ ưu tiên và thời gian

(3). Hỗ trợ các ngắt lồng nhau

(4). Ưu tiên kế thừa trong các hoạt động mutex và semaphore

(5). Giao tiếp tác vụ bằng bộ nhớ và tin nhắn dùng chung

(6). Đồng bộ hóa bộ xử lý bằng SGI

(7). Tác vụ ghi nhật ký để ghi lại các hoạt động của tác vụ vào SDC

Chúng tôi trình diễn hệ thống SMP_RTOS bằng chương trình mẫu C10.9 Để cho ngắn gọn, chúng tôi chỉ hiển thị tệp tc

của hệ thống.

/******** tc của bản demo SMP_RTOS. hệ thống C10.9 ********/

#include "type.h" // kiểu hệ thống và hằng số

#include "string.c"

cấu trúc ngữ nghĩa s0, s1; // để đồng bộ hóa tác vụ

int irq_stack[4] [1024], abt_stack [4] [1024], und_stack [4] [1024];

#include "queue.c" // hàm enqueue/dequeue

#include "pv.c" // các ẩn dụ có ưu tiên kế thừa

#include "mutex.c" // mutex với quyền kế thừa ưu tiên

#include "uart.c" // trình điều khiển UART

#include "kbd.c" // trình điều khiển KBD

#include "ptimer.c" // trình điều khiển hẹn giờ cục bộ

#include "vid.c" // Trình điều khiển màn hình LCD

#include "Exceptions.c" // trình xử lý ngoại lệ

#include "kernel.c" // kernel init và bộ lập lịch tác vụ

#include "chờ.c" // Hàm kexit() và kwait()

#include "fork.c" // hàm kfork()

#include "sdc.c" // trình điều khiển SDC


Machine Translated by Google

10.8 RTOS đa bộ xử lý (SMP_RTOS) 471

#include "tin nhắn.c" // thông qua

int copy_vectors(){ // giống như trước }

int config_gic() { // giống như trước }

int config_int(int N, int targetCPU){// giống như trước }

int irq_chandler(){ // giống như trước }

int APstart() { // giống như trước }

int SGI_handler() // trình xử lý sgi

int cpuid = get_cpuid();

printf("%d trên CPU%d nhận được SGI-2: ", Running->pid, cpuid);

if (readyQueue[cpuid]){ // if readQ không trống

printf("đặt swflag\n");

swflag[cpuid] = 1;

else{ // trống sẵn sàngQ

printf("KHÔNG CÓ HÀNH ĐỘNG\n");

swflag[cpuid] = 0;

int klog(char *line){ send(line, 2) }

char *logmsg = "thông tin nhật ký";

nhiệm vụ int5()

printf("task%d đang chạy: ", Running->pid);

klog("bắt đầu");

mutex_lock(mp);

printf("task%d bên trong CR\n", Running->pid);

klog("bên trong CR");

mutex_unlock(mp);

klog("chấm dứt");

kexit(0);

nhiệm vụ int4()

int cpuid = get_cpuid();

trong khi(1){

printf("%d trên CPU%d: ", Running->pid, cpuid);

kfork((int)task5, 5, 2); // tạo tác vụ task5 trên CPU2

kdelay(đang chạy->pid*10000);

kexit(0);

nhiệm vụ int3()

int cpuid = get_cpuid();

trong khi(1){

printf("%d trên CPU%d: ", Running->pid, cpuid);

klog(logmsg);

mutex_lock(mp); // khóa mutex

pid = kfork((int)task4, 4, 2); // tạo task4 trên CPU2

kdelay(đang chạy->pid*10000);

mutex_unlock(mp);
Machine Translated by Google

472 10 hệ điều hành nhúng thời gian thực

pid = kwait(&status);

int task2() // ghi nhật ký tác vụ

int r, blk;

dòng char[1024];

printf("nhật ký tác vụ %d start\n", Running->pid);

blk = 0; // bắt đầu ghi vào khối 0 của SDC

trong khi(1){

printf("LOGtask%d: đang nhận\n", đang chạy->pid);

r = recv(dòng); // nhận một dòng nhật ký

put_block(blk, line); // ghi nhật ký vào khối SDC (1KB)

blk++;

uprintf("%s", line); // hiển thị nhật ký tới UART0 trực tuyến

printf("log: ");

nhiệm vụ int1()

trạng thái int, pid, p3;

int cpuid = get_cpuid();

fs_init();

kfork((int)task2, 2, 1);

V(&s0); // khởi động logtask

kfork((int)task3, 3, 2);

trong khi(1)

pid = kwait(&status);

int run_task(){ // giống như trước }

int chính()

int cpuid = get_cpuid();

Enable_scu();

fbuf_init();

kprintf("Chào mừng đến với SMP_RTOS trong ARM\n");

sdc_init();

kbd_init();

uart_init();

ptimer_init();

ptimer_start();

tin nhắn_init();

config_gic();

kprintf("CPU0 khởi tạo kernel\n");

kernel_init();

mp = mutex_create();

s0.lock = s1.lock = s0.value = s1.value = 0;

s0.queue = s1.queue = = 0;

printf("AP khởi động CPU0: ");

int *APaddr = (int *)0x10000030;

*APaddr = (int)apStart;

send_sgi(0x0, 0x0F, 0x01);


Machine Translated by Google

10.8 RTOS đa bộ xử lý (SMP_RTOS) 473

Hình 10.10 Trình diễn SMP_RTOS

printf("CPU0 đợi AP sẵn sàng\n");

while(ncpu < 4);

kfork((int)task1, 1, 0);

run_task();

Hình 10.10 thể hiện hệ thống SMP_RTOS đang chạy. Phần trên cùng của Hình 10.10 hiển thị thông tin nhật ký trực tuyến
được tạo ra bởi tác vụ ghi nhật ký. Phần dưới cùng của Hình 10.10 hiển thị màn hình khởi động của SMP_RTOS.
Trong hàm main() của hệ thống SMP_RTOS, quy trình ban đầu trên CPU0 tạo ra tác vụ P1, tác vụ này hoạt động như quy trình
INIT. P1 tạo task2 làm tác vụ ghi nhật ký trên CPU1 và task3 trên CPU2. Sau đó P1 thực hiện một vòng lặp để chờ bất kỳ con ZOMBIE
nào. Nhiệm vụ 3 đầu tiên khóa một mutex. Sau đó, nó tạo task4, task5, tất cả đều trên CPU2 nhưng có mức độ ưu tiên khác nhau.
Khi task5 chạy, nó sẽ cố gắng lấy cùng một khóa mutex, khóa này vẫn được giữ bởi task3. Trong trường hợp này, task5 sẽ bị chặn
trên mutex nhưng nó nâng mức độ ưu tiên thực tế của task3 lên 5, điều này thể hiện sự kế thừa mức độ ưu tiên. Ngoài ra, hệ
thống còn hiển thị quyền ưu tiên tác vụ và đồng bộ hóa giữa các bộ xử lý bằng SGI. Tất cả các tác vụ có thể gửi thông tin nhật ký dưới dạng
Machine Translated by Google

474 10 hệ điều hành nhúng thời gian thực

thông báo tới tác vụ ghi nhật ký, tác vụ này sẽ gọi trực tiếp trình điều khiển SDC để ghi thông tin nhật ký vào khối (1KB) của SDC. Nó cũng

ghi thông tin nhật ký vào cổng UART để hiển thị trực tuyến. Biến thể của sơ đồ ghi nhật ký được liệt kê dưới dạng dự án lập trình trong phần

Sự cố.

10.9 Tóm tắt

Chương này đề cập đến các hệ điều hành thời gian thực nhúng (RTOS). Nó giới thiệu các khái niệm và yêu cầu của hệ thống thời gian thực. Nó

bao gồm các loại thuật toán lập lịch tác vụ khác nhau trong RTOS, bao gồm RMS, EDF và DMS. Nó giải thích vấn đề đảo ngược mức độ ưu tiên do

lập kế hoạch nhiệm vụ ưu tiên và chỉ ra cách xử lý việc đảo ngược mức độ ưu tiên và ưu tiên nhiệm vụ. Nó bao gồm các nghiên cứu điển hình

về một số hệ điều hành thời gian thực phổ biến và trình bày một bộ hướng dẫn chung về thiết kế RTOS. Nó cho thấy thiết kế và triển khai

UP_RTOS cho các hệ thống bộ xử lý đơn (UP). Sau đó, nó mở rộng UP_RTOS thành SMP_RTOS, hỗ trợ các ngắt lồng nhau, lập lịch tác vụ ưu tiên, kế

thừa ưu tiên và đồng bộ hóa giữa các bộ xử lý bằng SGI.

Danh sách các chương trình mẫu

C10.1. UP_RTOS với các nhiệm vụ định kỳ và lập kế hoạch luân chuyển C10.2.

UP_RTOS với các tác vụ định kỳ và lập lịch ưu tiên C10.3. UP_RTOS với các

tác vụ động C10.4. Hạt nhân SMP_RTOS để quản

lý tác vụ C10.5. MP_RTOS, một hệ thống SMP được bản địa

hóa.

C10.6. Các ngắt lồng nhau trong SMP_RTOS C10.7.

Ưu tiên nhiệm vụ của SGI trong SMP_RTOS C10.8. Lập kế

hoạch tác vụ theo thời gian trong SMP_RTOS C10.9. Trình

diễn SMP_RTOS

Các vấn đề

1. Trong hệ thống UP_RTOS và SMP_RTOS, tác vụ ghi nhật ký sẽ lưu thông tin nhật ký trực tiếp vào SDC, bỏ qua hệ thống tệp. Sửa đổi chương

trình C10.1 và C10.8 để cho phép tác vụ ghi nhật ký lưu thông tin nhật ký vào một tệp trong hệ thống tệp (EXT2/3) trên SDC. Thảo luận về những

lợi thế và bất lợi của việc sử dụng các tập tin nhật ký.

2. Trong kernel UP_RTOS, ưu tiên kế thừa chỉ ở một cấp độ. Triển khai tính kế thừa ưu tiên trong chuỗi các yêu cầu lồng nhau cho khóa mutex

hoặc semaphore.

3. Đối với ví dụ về hệ thống UP_ROTS C10.3, hãy thực hiện phân tích thời gian phản hồi để xác định xem các tác vụ có đáp ứng thời hạn hay

không. Xác minh xem nhiệm vụ có thể đáp ứng thời hạn theo kinh nghiệm hay không.

4. Trong hệ thống MP_RTOS, các tác vụ được phân chia thành các CPU riêng biệt để xử lý song song. Đưa ra cách hỗ trợ di chuyển tác vụ, cho

phép các tác vụ được phân phối đến các CPU khác nhau để cân bằng tải xử lý.

5. Trong hệ thống SMP_RTOS, các tác vụ chạy trên các CPU khác nhau có thể gây nhiễu lẫn nhau vì tất cả chúng đều thực thi trong cùng một không

gian địa chỉ. Sử dụng MMU với ánh xạ VA đến PA không đồng nhất để cung cấp cho mỗi CPU một không gian VA riêng biệt.

6. Trong SMP RTOS, các ngắt có thể được xử lý trực tiếp bởi ISR hoặc bằng các tác vụ giả. Thiết kế và triển khai các hệ thống RTOS sử dụng các

sơ đồ xử lý ngắt này và so sánh hiệu suất của chúng.

Người giới thiệu

Audsley, NC: "Lập kế hoạch đơn điệu thời hạn", Khoa Khoa học Máy tính, Đại học York, 1990 Audsley, N. C,
Burns, A., Richardson, MF, Tindell, K., Wellings, AJ: "Áp dụng lý thuyết lập kế hoạch mới cho lập lịch ưu tiên ưu tiên tĩnh.Tạp chí Kỹ thuật
Phần mềm, 8(5):284–292, 1993.
Buttazzo, GC: Tuyên bố của Counter RMS Tỷ lệ Monotonic so với EDF: Ngày phán xét, Hệ thống thời gian thực, 29, 5–26,
2005 Dietrich, S., Walker, D., "Sự phát triển của Linux thời gian thực", http: //www.cse.nd.edu/courses/cse60463/www/amatta2.pdf,
DNX 2015: DNX RTOS, http://www.dnx-rtos.org,
FreeROTS 2015: FreeRTOS, http://www.freertos.org, 2016
Machine Translated by Google

Người giới thiệu 475

Hagen, W. "Cải thiện hiệu suất và thời gian thực trong hạt nhân Linux 2.6", tạp chí Linux, 2005 Josheph,
M và Pandya, P., "Tìm thời gian phản hồi trong hệ thống thời gian thực", Comput. J., 1986 , 29, (5). trang 390-395
Jones, MB (16 tháng 12 năm 1997). "Điều gì thực sự đã xảy ra trên sao Hỏa?". Microsoft.com.
1997 Labrosse, J.: Micro/OS-II, R&D Books,
1999 Leung, JY T., Merrill, M. L: "Lưu ý về việc lập kế hoạch ưu tiên cho các nhiệm vụ theo thời gian thực, định kỳ. Thư xử lý thông tin,
11(3):115–118, 1982 Linux: "Giới thiệu về Linux thời gian thực dành cho nhà phát triển nhúng", https://www.linux.com/blog/intro-real-time-
linux-embedded- nhà phát triển Lưu, CL; Layland, J. "Các thuật toán lập kế hoạch cho đa chương trình trong môi trường thời gian thực cứng", Tạp
chí ACM 20 (1): 46–61, 1973 Micrium: Micro/OS-III, https://
www.micrium.com, 2016 Nutt, G. NuttX, Hệ điều hành thời gian
thực, nuttx.org, 2016 POSIX 1003.1b: https://en.wikipedia.org/
wiki/POSIX, QNX 2016: QNX Neutrino RTOS, http://www.qnx.com/products/neutrino-
rtos, 2015 Reeves, GE: "Điều gì thực sự đã xảy ra trên sao Hỏa? - Tài khoản có thẩm quyền". Microsoft.com. 1997.
RTLinux: RTLinux, https://en.wikipedia.org/wiki/RTLinux Sha,
L., Rajkumar,R., Lehoczky,JP: (tháng 9 năm 1990). "Các giao thức kế thừa ưu tiên: Cách tiếp cận đồng bộ hóa thời gian thực", IEEE
Giao dịch trên máy tính,Tập 39, trang1175–1185, 1990
VxWorks: VxWorks: http://windriver.com, 2016.
Yodaiken, V.:"Tuyên ngôn RTLinux", Kỷ yếu của Hội nghị Linux lần thứ 5, 1999 Wang, KC:
"Thiết kế và triển khai hệ điều hành MTX", Springer Publishing International AG, 2015
Machine Translated by Google

Mục lục

MỘT Định dạng tệp BMP, 34

Kiểu dữ liệu trừu tượng (ADT), 142 Hình ảnh hạt nhân khởi động từ SDC, 253
Điều chỉnh thuật toán UP cho SMP, 395 Bộ xử lý khởi động (BSP), 329
Android, 1, 265 Hướng dẫn chi nhánh, 11

Bộ xử lý ứng dụng (AP), 329 Lỗi vỡ đường ống, 143


Các phép toán số học, 12
ARM926EJ-S, 7
Kiến trúc ARM, 2, 3 C

Lập trình lắp ráp ARM, 17 Kết hợp bộ đệm, 329

Bộ xử lý ARM Cortex-A9 MPcore, 331 chdir-getcwd-stat, 307


Bộ xử lý ARM Cortex-A15 MPcore, 331 Xóa yêu cầu ngắt thiết bị, 55
Thanh ghi CPU ARM, 8 Tham số dòng lệnh, 222
Ngoại lệ ARM, 47 Các phép toán so sánh, 12
Hướng dẫn ARM, 2, 3 Khóa có điều kiện, 361
Hướng dẫn ARM LDREX/STREX, 2, 350 Mutex có điều kiện, 361

Rào cản bộ nhớ ARM, 351 Semaphore có điều kiện, 362


Đơn vị quản lý bộ nhớ ARM (MMU), 3, 193 Khóa xoay có điều kiện, 361
CÁNH TAY MPcore, 2, 4 Cờ tình trạng, 10
Kiến trúc ARM MPcore, 2 Biến điều kiện, 141
Chuỗi công cụ Arm-none-eabi, 16 Chuyển ngữ cảnh, 116
Giao diện UART ARM PL011, 67 Hướng dẫn bộ đồng xử lý, 15
Bộ điều khiển LCD màu ARM PL110, 33 Cortex A9-MPcore, 7

Bộ điều khiển ngắt ARM PL190/192, 52 Dòng Cortex-A, 7

Bộ điều khiển ngắt vectơ ARM PL190, 84 Dòng Cortex-M, 7

Chế độ xử lý ARM, 8, 47 Dòng Cortex-R, 7

Lập trình ARM, 17 Thanh ghi giao diện CPU, 332


Mô-đun hẹn giờ kép ARM SB804, 56 Vùng quan trọng, 67, 293, 349
Trình giả lập hệ thống ARM, 16 Phần quan trọng, 67
Chuỗi công cụ ARM, 2, 16 Trình biên dịch chéo, 16

ARM đa năng, 3, 16 Mã khởi động C, 226


Máy ảo ARM, 2, 3 Thanh ghi trạng thái chương trình hiện tại (CPSR), 9
Mã ASCII, 37

MP bất đối xứng (ASMP), 329, 331


Sự kiện không đồng bộ, 98 D

Truyền tin nhắn không đồng bộ, 148 Hoạt động di chuyển dữ liệu, 13
Thanh ghi điều khiển phụ trợ (ACTL), 331 Rào cản bộ nhớ dữ liệu (DMB), 351
Rào cản đồng bộ hóa dữ liệu (DSB), 351
Phân tích thời hạn, 425, 432
B Đảo ngược thời hạn, 403
Cần số thùng, 12 Lập kế hoạch thời hạn-đơn điệu (DMS), 403
Thanh ghi cơ sở, 14 Tránh bế tắc, 137
Tốc độ truyền, 29 Phát hiện và phục hồi bế tắc, 137
Phần dữ liệu nhị phân, 34 Phòng chống bế tắc, 137
Thực thi nhị phân, 18 Ngăn chặn bế tắc trong các thuật toán song song, 394
Tín hiệu nhị phân, 134 Ngăn chặn bế tắc trong SMP, 394
BIOS, 329 Xóa_dir_entry, 306
Chặn truyền dữ liệu, 14 Phương pháp thiết kế, 105

© Springer International Publishing AG 2017 KC 477

Wang, Hệ điều hành nhúng và thời gian thực, DOI


10.1007/978-3-319-51517-5
Machine Translated by Google

478 Mục lục

Nguyên tắc thiết kế của RTOS, 412 G

Trình điều khiển thiết bị và trình xử lý ngắt trong SMP, 384, 396 Đầu vào-đầu ra mục đích chung (GPIO), 27
Trình điều khiển thiết bị, 27, 391 Hệ điều hành mục đích chung (GPOS), 265
Bộ nhớ thiết bị, 362 Bộ điều khiển ngắt chung (GIC), hàm printf() chung 2,
Bàn chuyển đổi thiết bị, 315 4, 330, 332 , 3
Vô hiệu hóa MMU, 172 Lập trình GIC, 332
Hiển thị tập tin hình ảnh, 33 Bộ tính giờ toàn cầu, 357

Hiển thị văn bản, 37 GoUmode, 379

Đăng ký kiểm soát truy cập tên miền (DACR), 169, 173 Lập trình GPIO, 27
Phân trang 2 cấp động, 238
Phân trang động, 286
Ưu tiên động, 125 H

Tạo quy trình động, 3 Hệ thống thời gian thực cứng, 401

Quy trình động, 121 Trình tự ngắt phần cứng, 54


Tài nguyên chia sẻ tác vụ động, 432 Hài hòa, 402

Lập kế hoạch sớm nhất-thời hạn đầu tiên (EDF), 402 I Giá trị tức thời và Bộ dịch chuyển thùng,
Tệp thực thi ELF, 217 13 Biểu đồ hàm ý, 107
tập tin ELF, 18 Hình ảnh đĩa ram ban đầu,
Kích hoạt MMU, 171 213 chương trình
ENQ/DEQ, 141 INIT, 321 Lắp ráp nội
Các tập tin hạt nhân EOS, 267 tuyến, 26
Eoslib, 390 Insert_dir_entry, 305
Trạng thái tương đương, 107 Đường dẫn lệnh, 10 Rào cản đồng bộ hóa lệnh (ISB),
Phím thoát, 62 351 CPU Intel x86,
Mô hình hướng sự kiện, 97 203 Giao tiếp giữa các quá trình,
Hệ thống hướng sự kiện, 97 3 Ngắt giữa các bộ xử lý (IPI), 329, 330 Đồng
Hệ thống nhúng trình điều khiển sự kiện, 3 bộ hóa giữa các bộ xử lý, 2 Bộ điều
Cờ sự kiện, 140 khiển ngắt, 51 Đăng ký
Ưu tiên sự kiện, 103 nhà phân phối ngắt, 332 Trình điều
Biến sự kiện, 140 khiển thiết bị điều khiển ngắt, 2, 56
Xử lý ngoại lệ và tín hiệu, 288 Thiết kế trình điều khiển điều khiển
Trình xử lý ngoại lệ, 49, 203 ngắt, 62 Trình xử lý
Bảng vectơ ngoại lệ, 49 ngắt, 55 Độ trễ ngắt,
Thực thi, 3, 221 401 Mặt nạ ngắt, 54, 390
Hình ảnh thực hiện, 20 Ưu tiên ngắt, 52 Đảo
Hệ thống tập tin EXT2, 4 ngược ưu tiên ngắt, 88 Xử lý
ngắt, 51, 54 Ngắt, 2, 51, 329
Quy trình dịch vụ ngắt
F (ISR), 55 Thanh ghi cho phép thiết lập
Hệ thống tập tin FAT, 214 ngắt, 339 Định tuyến ngắt, 330 Đảo
Đăng ký địa chỉ lỗi, 174 ngược- Lập lịch thời hạn
Thanh ghi trạng thái lỗi, 173 (IDS), 403 IOAPIC, 329 giới hạn I/O so với
Thao tác I/O tệp, 296 giới hạn tính

Khóa tập tin, 318 toán, 124 quản lý bộ đệm I/O, 318, 396
Mức độ hoạt động của tập tin, 294 I/O bằng cách thăm dò, 29 chuyển
Bảo vệ tập tin, luồng hướng I/O, 4 trình
tập tin 317 , 297, 301 xử lý IRQ và ngoại
Máy trạng thái hữu hạn (FSM), 105 lệ, 378 Chế độ IRQ, 9
Chế độ FIQ, 9
Tệp thực thi nhị phân phẳng, tệp
phông chữ

217 , 37 fork(), 221 K

Fork-Exec trong Unix/Linux, 221 Hạt nhân được ánh xạ cao (KMH), 193
FreeBSD, 265 danh Hạt nhân được ánh xạ thấp (KML), 193
sách miễn phí, 121 Chế độ hạt nhân, 297
FreeRTOS, 404 Chế độ hạt nhân Pgdir và Bảng trang, 285
Giảm thiểu FSM, 108 Kexit, 122
Bảng trạng thái FSM, 106 Trình điều khiển bàn phím,

Đầy đủ ngăn xếp giảm dần, 15 61 kfork, 122

Quy ước gọi hàm trong C, 21 Ánh xạ bộ nhớ KMH, 241


Machine Translated by Google

Mục lục 479

Kmode, 194 ồ

kwait, 220 Đối tượng, 18


Objdump, 18
Phân trang một cấp, 178
L Phân trang một cấp với không gian VA cao, 184
Trình điều khiển màn hình LCD, 33 Mở-đóng-lseek, 310
Hướng dẫn LDRES/STRES, 4 Opendir-Readdir, 315
Các chức năng I/O của thư viện, 295 , 297 Hệ điều hành, 6
Các quy trình nhẹ, 231 tập lệnh
liên kết, 18 hủy
liên kết, 308 P

Linux, 1 P, 133
Hướng dẫn LOAD và lưu trữ, 14 Mô tả trang, 169
Bộ tính giờ địa phương, 357 Bộ mô tả bảng trang, 175
Chương trình đăng nhập, 322 Thuật toán song song, 2
Nhảy xa, 23 Tính toán song song, 368
Sự kiện định kỳ, 98
Địa chỉ cơ sở ngoại vi, 330
M Bộ hẹn giờ ngoại vi, 357
Tạo tập tin, 267 , 390 Ống, 142
Thuật toán của người đưa thư, 339 PL050 KMI, 17
Nhiệm vụ tìm đường dẫn sao Hỏa, 403 Bộ điều khiển LCD PL110, 17
Người mẫu Mealy, 105 Giao diện thẻ đa phương tiện PL181, 17
Rào cản trí nhớ, 2 ,4 PL011 UART, 17
Quản lý bộ nhớ trong SMP, 362 Bộ điều khiển ngắt vectơ PL190, 17
Đơn vị quản lý bộ nhớ (MMU), 2 , 169 PMTX, 266
Bản đồ trí nhớ, 27 Chuyển Linux sang ARM, 266
I/O được ánh xạ bộ nhớ, 27 API tuân thủ POSIX, 410
Tin nhắn được chuyển đi, 147, 283 Ưu tiên, 125
Vi điều khiển, 1 Lập lịch tác vụ ưu tiên trong SMP_RTOS, 461 , 465
MicroC/OS ( µC/OS), 405 Chuyển đổi nhiệm vụ ưu tiên, 401
Hạt nhân vi mô, 147, 407 Hạt nhân UP ưu tiên, 158
mkdir-creat-mknod, tập Bộ điều khiển ngắt sơ cấp và thứ cấp, 53
lệnh 306 mk, 18 Trần ưu tiên, 404
Số đăng ký MMU, 170 Ưu tiên kế thừa, 164 , 404 , 468
Màn hình, 142 Đảo ngược ưu tiên, 164 ,
403
Mô hình Moore, 105 Khái niệm quy trình, 113
mount_root, 305 Khối điều khiển quá trình (PCB), 113
Núi-umount, 316 Quá trình hợp tác, 135
Tệp hạt nhân MTX, 390 Quy trình với các tên miền riêng lẻ, 212
Hướng dẫn nhân, 14 Cây gia phả quy trình, 220
Đa xử lý, 329 Trình tải tệp hình ảnh xử lý, 216
Mô hình hệ thống đa bộ xử lý (MP), 105 Quản lý quy trình, 3
RTOS đa bộ xử lý (SMP_RTOS), 440 Quản lý quy trình trong EOS, 269
Truyền dữ liệu đa ngành, 82 Quản lý quy trình trong SMP_EOS, 391
Đa nhiệm, 3 , 113 Mô hình quy trình, 103
Đa nhiệm trong SMP, 371 Lập kế hoạch quy trình, 125

Hạt nhân đa nhiệm, 2 Lập kế hoạch quy trình, 124 ,125


Mutex, 135 Đồng bộ hóa quy trình, 126
Mutex trong SMP, 353 Chấm dứt quy trình, 218 219,
Cấu trúc PROC, 113 , 115
Vấn đề nhà sản xuất-người tiêu dùng, 135 , 136
N Hướng dẫn chuyển PSR, 15
Các đường ống được đặt tên trong Linux, 143 Pthread, 141
Ngắt lồng nhau, 87
Các ngắt lồng nhau trong SMP_RTOS, 458
Xử lý ngắt lồng nhau, 3 Q
NetBSD, 265 QEMU, 2
Trình xử lý ngắt không lồng nhau, 55 Lệnh giám sát QEMU, 19
Không được ưu tiên, 125 QNX, 407
Hạt nhân UP không ưu tiên, 152
Quyền ưu tiên không hạn chế, 158
Bộ nhớ không chia sẻ, 362 R

Không gian VA không đồng nhất, 365 Tình trạng cuộc đua, 349

NuttX, 405 Đĩa RAM, 213


Machine Translated by Google

480 Mục lục

Raspberry PI, 265 Hoạt động ngăn xếp, 14


Raspberry PI-2, 4 Tệp khởi động C chuẩn crt0, 224
Raspbian, 265 Trình tự khởi động của ARM MPcores, 340
Lập kế hoạch đơn điệu tốc độ (RMS), 402 Phần Mô hình StateChart, 110
dữ liệu thô, 213 Hệ thống Sơ đồ trạng thái, 109

phản ứng, 97 Readelf, Model máy trạng thái, 105


18 Vấn đề về Giảm thiểu bảng trạng thái, 107
trình đọc-ghi, 136 Read_pipe, , 137 Phân trang 2 cấp tĩnh, 236
144 Tệp đặc biệt Thanh ghi trạng thái, 9

đọc-ghi, 315 ReadyQueue, 121 395 Mô hình siêu vòng lặp, 95


uid thực và hiệu quả,, 317 Mô hình hệ thống siêu vòng lặp, 3
Linux thời gian thực, 407 Bản Khảo sát của RTOS, 404
vá Linux thời gian thực, SVC_entry, 379
408 Hệ điều hành thời gian thực Trình xử lý SVC, 202

(RTOS), 2 Mô hình hệ thống thời gian thực , 4 , 401 Chế độ SVC, 9

(RT), 105 Hệ thống thời gian thực, 1 Chế độ SVGA, 33

Realview -pbx-a9, 331 Hoán đổi, 350

Tính toán tập lệnh giảm Chuyển đổi PGdir, 286 liên , 379

(RISC), 7 Reset_handler, 378 Quản lý tài nguyên, 2 kết đọc-liên kết tượng trưng, 309

Thời gian phản hồi so Đa xử lý đối xứng (SMP), 2


với thông lượng, 125 Quyền Truyền tin nhắn đồng bộ, 148
ưu tiên hạn chế, 158 Rmdir, 308 Vòng Syscall, 194
tròn, 125 RTLinux, 408 Sử dụng Đăng ký SYS_FLAGSSET, 330
ngăn xếp 194 ,
Cuộc gọi hệ thống, 2, 295

thời gian chạy, 2 Nhập và thoát cuộc gọi hệ thống, 202

Thanh ghi CPU mục tiêu, 339


S Khối điều khiển tác vụ (TCB), 113 , 404

Thanh ghi trạng thái chương trình đã lưu (SPSR), 9 Hạn chót nhiệm vụ, 401

Quét mã, 62 Thuật toán lập lịch tác vụ, 2


Khả năng lập kế hoạch, 402 Lập lịch tác vụ trong RTOS, 401
Lịch trình và thời hạn, 438 Mạng Telnet TCP/IP, 33

, 401
Người lập kế hoạch, 115 Kiểm tra và thiết lập (TS), 349

Trình điều khiển thẻ SD, 74 Chủ đề, 3 , 231


Giao thức thẻ SD, 74 Đồng bộ hóa chủ đề, 234
Trình điều khiển SDC trong SMP, 384 Trình điều khiển hẹn giờ, 56

Hệ thống tệp SDC, 248 Hàng đợi yêu cầu hẹn giờ, 292 , 293

Phần mô tả, 175 Chia sẻ thời gian, 125

Thẻ kỹ thuật số an toàn (SD), 73 Lập lịch tác vụ theo thời gian trong SMP_RTOS, 463
Ngữ nghĩa, 133 , 359 Bộ đệm tra cứu bản dịch (TLB), 169
Gửi EOI, 55 Bản dịch tài liệu tham khảo trang lớn, 178
I/O nối tiếp, 29 Bản dịch tài liệu tham khảo trang, 176
Giao diện ngoại vi nối tiếp (SPI), 74 Bản dịch tài liệu tham khảo phần, 176
SEV (Gửi sự kiện), 351 Bản dịch tài liệu tham khảo trang nhỏ, 177
Thanh ghi SGI (GICD_SGIR), 330 Bảng dịch, 175

Bộ nhớ dùng chung, 3 ,


142 , 362 Thanh ghi cơ sở bảng dịch (TTBR), 173
Chương trình Sh, 322 Bảng dịch logic đi bộ, 169
Bộ thu tín hiệu, 288 , 289 , 291 , 325 Khung chồng bẫy, 291
Xử lý tín hiệu trong hạt nhân EOS, 290 Tswitch, 378

Xử lý tín hiệu, 2 Phân trang hai cấp, 183


,
Tín Hiệu, 147 288 ,
289 Sơ đồ phân trang hai cấp, 169
Ngủ và thức, 126 Phân trang hai cấp với không gian VA cao, 189
Ngủ/thức trong SMP, 392 Khởi động hai giai đoạn, 260
Khởi động SMP trên máy ảo, 341
SMP_EOS, 389
SMP_RTOS, 2

Bộ điều khiển rình mò (SCU), 332 Trình điều khiển U , 67

Hệ thống thời gian thực mềm, 401 UART, 29 Ubuntu (15.10)

Ngắt do phần mềm tạo ra (SGI), 2 , 330 Linux, 2

Ngắt phần mềm (SWI), 15 Umode, 194 Đảo ngược ưu tiên không , 403

Spinlocks, 390 , 391 giới hạn, chế


Ngăn xếp và chương trình con, 15 độ 164 UND, 9 Không gian VA
Khung xếp chồng, 23 thống nhất, 363 Bộ xử lý RTOS (UP_RTOS),
Con trỏ khung ngăn xếp, 23 413 Mô hình hạt nhân Bộ xử lý đơn (UP), 3, 104
Machine Translated by Google

Mục lục 481

Hệ thống bộ đơn xử lý (UP), 2 Máy ảo, 2


UP_RTOS, 2 VxWorks, 406
UP_RTOS với các tác vụ định kỳ tĩnh, 416 , 426
Lệnh người dùng, 295
Giao diện người dùng, 2, 321 W

Hình ảnh chế độ người dùng, 194 Sự kiện chờ (WFE), 351
Bảng trang chế độ người dùng, 286 Chờ ngắt (WFI), 351
Quy trình chế độ người dùng, 193 Write_pipe, 144
Viết tập tin thông thường, 313

V.
V, 133 X

Ngắt theo vectơ, 84 Hoạt động XCHG/SWAP, 350


Bộ điều khiển ngắt Vector (VIC), 3
Vfork, 3 ,228
Chế độ VGA, 33 Z

Ánh xạ không gian địa chỉ ảo, 193 Con ZOMBIE, 220 , 270
Dịch địa chỉ ảo, 174

You might also like