Tác vụ và truyền thông giữa các tác vụ hệ thống nhúng

Các tác vụ (Task)

Một hệ thống thời gian thực được gọi là “điều khiển sự kiện” có nghĩa là hệ thống đó phải có chức năng chính là phản ứng lại các sự kiện xảy ra trong môi trường của hệ thống. Vậy thì hệ thống phản ứng lại các sự kiện như thế nào? Một giải pháp đưa ra có tên Đa nhiệm. Giải pháp này đã được chứng minh là một mô hình chuẩn cho các hệ thống điều khiển sự kiện và hệ thống sử dụng ngắt. Ý tưởng cơ bản của giải pháp này là chúng ta có thể phân chia một vấn đề lớn thành các nhánh nhỏ và đơn giản hơn để giải quyết. Mỗi một vấn đề con (sub-problem) trở thành một tác vụ - task. Mỗi một tác vụ chỉ làm một việc đơn giản. Sau đó, chúng ta giả thiết rằng các tác vụ này chạy song song với nhau. Trên thực tế, các tác vụ không bao giờ chạy song song nếu chúng ta không có một hệ thống đa vi xử lý. Trong trường hợp đang xét, các tác vụ sẽ chia sẻ một bộ vi xử lý.

Cũng giống như các chương trình khác, một tác vụ bao gồm mã lệnh để thực hiện các chức năng tác vụ phải thực hiện (do người lập trình đã thiết kế). Mã lệnh được chứa trong một hàm tương tự như hàm main() trong ngôn ngữ lập trình C. Điều làm nên sự khác biệt của tác vụ chính là ngữ cảnh (context) chứa trong ngăn xếp (stack) của nó.

Hình 7.1: Task là gì?

Mỗi một tác vụ bao gồm :

  • Mã nguồn chứa các chức năng của tác vụ.
  • Một ngăn xếp để chứa ngữ cảnh của tác vụ.
  • Một hộp thư (mail box) (tùy chọn) để phục vụ cho việc truyền thông với các tác vụ khác.

Chú ý rằng, đôi khi (nhiều khi khá hữu dụng) ta có thể tạo ra nhiều tác vụ từ một hàm chung. Như đã nói, điều làm cho một tác vụ có thể tách biệt và khác biệt với các tác vụ khác chính là ngăn xếp của nó. Thực tế đây chính là lập trình hướng đối tượng kiểu cổ điển. Ta có thể nghĩ rằng hàm tác vụ chính là việc định nghĩa một class. Và một tác vụ tạo ra từ hàm đó chính là một ví dụ về class.

Mặc dù có thể thấy các tác vụ là khá độc lập, nhưng về cơ bản thì chúng cũng cần phải hợp tác với nhau để thực hiện một mục đích chung đã được thiết kế sẵn cho hệ thống. Vì vậy, mỗi một tác vụ cần phải có một cơ chế truyền thông mà thông qua đó, chúng có thể kết nối, đồng bộ với các tác vụ khác. Trong trường hợp này, ta gọi cơ chế đó là Hộp thư – mail box.

Hình 7.2 miêu tả cấu trúc mã nguồn của một tác vụ. Đối số data dùng để tham số hóa một tác vụ. Vai trò của nó cũng giống với các đối số argv và argc trong hàm main() với ngôn ngữ C. Đối số này thực sự quan trọng trong trường hợp nhiều tác vụ cùng được tạo ra từ một hàm. Sự duy nhất của tác vụ được thể hiện bởi giá trị của đối số này.

Hình 7.2: Cấu trúc thông thường của một tác vụ

Một tác vụ có thể được khởi động với một vài khởi tạo (có thể bao gồm khởi tạo đối số data). Sau đó, thông thường, tác vụ sẽ đi vào một vòng lặp không giới hạn. Tại một vài điểm trong vòng lặp, nó sẽ đợi "Một sự kiện nào đó xảy ra", có thể, sự kiện đó là một bản tin được gửi tới mail box, hoặc đơn giản là tràn bộ định thời. Trong khi chờ sự kiện, tác vụ sẽ không làm gì cả và không sử dụng bộ vi xử lý. Một vài tác vụ khác nếu đã sẵn sàng hoạt động hoặc đang hoạt động sẽ xử dụng bộ vi xử lý.

Khi sự kiện mà tác vụ đang chờ xảy ra, tác vụ sẽ "thức dậy" và: nhận lấy bản tin, giải mã bản tin và hoạt động theo các yêu cầu đặt sẵn dựa trên một hệ thống các yêu cầu được phân định bởi câu lệnh switch(). Sau khi thực hiện xong yêu cầu, tác vụ lại quay trở lại trạng thái chờ sự kiện.

Có thể thấy rằng, tất cả các tá vụ đều giành phần lớn thời gian cho việc chờ một sự kiện nào đó xảy ra. Đây cũng chính là lí do để đa nhiệm hoạt động.

Truyền thông và đồng bộ giữa các tác vụ

Mặc dù các tác vu ̣ được xem như độc lập với nhau nhưng nhiệm vu ̣ tổng quát của hệ thống yêu cầu các tác vu ̣phải có sự liên hê ̣ với nhau, hợp tác với nhau. Do đó, thành phần cốt yếu của bất cứ hệ điều hành thời gian thực là tập hợp các dịch vụ truyền thông và đồng bộ.

Có một vài cơ chế đồng bộ và truyền thông hay được sử dụng, bao gồm:

  • Semaphore: Sử dụng cho việc đồng bộ hóa tín hiệu và khả năng tận dụng tài nguyên.
  • Monitor: Điều khiển việc truy cập vào vùng dữ liệu chia sẻ trong hoạt động của hệ thống.

Semaphore

Hãy xét 2 tác vụ, mỗi tác vu ̣ có nhiệm vụ in một bản tin có nội dung “I am task n” (n là số thứ tự của tác vụ) bằng một máy in chia sẻ như trong hình dưới. Nếu chúng ta không sử dụng bấ t cứ một cơ chế đồng bộ nào, kết quả có được từ máy in sẽ có thể là “II a amm tatasskk 12”.

 

 

Hình 7.3: Chia sẻ tài nguyên

Điều cần thiết ở đây là phải có một cơ chế nào đó để với cơ chế này, máy in chỉ có thể được sử dụng bởi 1 tác vụ tại một thời điểm xác định.

Semaphore hoạt động giống như một chiếc chìa khóa cho việc truy cập tới tài nguyên. Chỉ có tác vụ có chìa khóa này mới có quyền sử dụng tài nguyên. Để có thể sử dụng tài nguyên (là chiếc máy in trong trường hợp này), tác vu ̣ cần yêu cầu (acquire) chìa khóa (semaphore) bằng cách gọi tới một dịch vụ thích hợp như trong hình 7.4. Nếu chìa khóa ở trạng thái sẵn sàng, tức̀ là tài nguyên (máy in) hiện tại không được sử dụng bởi bất kỳ một tác vụ nào, tác vụ đó có thể được cho phép sử dụng tài nguyên. Sau khi sử dụng xong, tác vụ đó phải trả lại (release) semaphore cho các tác vụ khác có thể sử dụng.

Hình 7.4: Chia sẻ tài nguyên với Semaphone

Tuy nhiên, nếu máy in đang được sử dụng, tác vụ đó sẽ bị khóa cho tới khi tác vụ đang sử dụng máy in trả lại semaphore. Cùng một lúc có thể có nhiều tác vụ yêu cầu semaphore trong khi máy in đang hoạt động. Tất cả các tác vụ đó đều sẽ bị khóa. Các tác vụ bị khóa sẽ được xếp hàng theo kiểu hàng đợi theo thứ tự về mặt ưu tiên hoặc theo thứ tự thời gian mà chúng yêu cầu semaphore theo lệnh acquireSem. Cách thức sắp xếp thứ tự hàng đợi cho các tác vụ có thể được xây dựng trong kernel hoặc cũng có thể được cấu hình khi mà semaphore được tạo ra

Lệnh acquireSem hoạt động như sau:

  • Giảm giá trị của semaphore.
  • Nếu kết quả giá trị lớn hơn hay bằng 0, tức là tài nguyên là sẵn sàng, tác vụ có thể sử dụng tài nguyên ngay lập tức. Ngược lai kết quả nhỏ hơn 0, tác vị sẽ bị khóa và chờ đến khi tác vị đang sử dụng tài nguyên sử dụng lệnh releaseSem.

Lệnh releaseSem tăng giá trị của semaphore, Nếu kết quả trả về bé hơn hoặc bằng 0, điều đó có nghĩa là có ít nhất một tác vụ đang đợi semaphore, do đó tác vụ sẽ được chuyển vào trạng thái sẵn sàng.

Trong trường hợp máy in này, semaphore sẽ được gán mặc định ban đầu là 1 trong trường hợp hệ thống chỉ có một máy in được quản lý. Trường hợp này thông thường được gọi là semaphore nhị phân (binary semaphore) để phân biệt với các trường hợp tổng quát hơn (counting semaphore), trong đó semaphore được mặc định là một số bất kỳ nguyên và không âm.

Xét một bộ định địa chỉ bộ nhớ động để quản lý bộ nhớ đệm cố định như trên hình 7.5. Ở đây chúng ta khởi tạo cho semaphore một số lượng bộ nhớ đệm đang còn trống tại thời điểm ban đầu. Khi câu lệnh bufReq được gọi đến, nó trước tiên dành lấy semaphore, sau đó định địa chỉ cho bộ nhớ đệm. Trong 10 lần gọi lệnh bufReq đầu tiên, semaphore vẫn còn không âm, điều này làm cho các tác vụ yêu cầu semaphore vẫn có thể hoạt động được. Đến lần thực thi lệnh bufReq lần thứ 11, tác vụ yêu cầu sẽ bị khóa và chờ đến khi có một tác vụ khác gọi lệnh bufReq để giải phóng semaphore.

 

 

Hình 7.5: Chia sẻ hệ thống đa tài nguyên

Một số kenel sử dụng cả hai loại binary semaphore và couting semaphore vì trong một một số trường hợp, binary semaphore có hiệu quả hơn. Binary semaphone đôi khi còn được gọi là mutex có nghĩa là loại trừ lẫn nhau (mutual exclusion) .

Một semaphore đôi khi cũng có thể được sử dụng để tạo tín hiệu cho sự xuất hiện của một sự kiện như trong hình 7.6. Lấy vụ dụ làm thế nào mà hệ thống có thể nhận biết được sự xuất hiện của một ngắt? tác vụ cần thông tin về sự xuất hiện của ngắt sẽ treo (pend) semaphore lên. Chương trình con dịch vụ ngắt (Internet sevice Routine) sẽ phục vụ ngắt và sau đó gửi (post) semaphore lại. (chú ý rằng: thuật ngữ “pend” và “post” được sử dụng thường xuyên hơn các thuật ngữ “acquire” và “release”.

Hình 7.6: Tạo tín hiệu cho sự kiện thông qua semaphore

Ở các ví dụ trên, semaphore được khởi tạo bởi một giá trị khác 0 bởi vì tài nguyên là sẵn sàng để sử dụng. Ở đây, semaphore được khởi tạo là 0. Vì vậy khi tác vụ đầu tiên treo (pend) semaphore, nó ngay lập tức bị khóa lại - sự kiện chưa được sảy ra. Khi một ISR gửi (post) lại semaphore, tác vụ đó tiếp tục được đánh thức và tiếp tục được thực hiện .

Khi semaphore được sử dụng như một khóa tài nguyên, nhiều tác vụ có thể post hoặc pend nó. Tuy nhiên trong trường hợp tạo tín hiệu hoặc đồng bộ hóa, semaphore thường được sử dụng bởi chỉ một ISR và một 1 tác vụ.

Cơ chế tương tự như trên cũng có thể được sử dụng khi một tác vụ muốn tạo tín hiệu của một sự kiện tới một tác vụ khác.

Monitor

Monitor là một ngôn ngữ lập trình được xây dựng để điều khiển việc truy nhập vào vùng dữ liệu chia sẻ trong hoạt động của hệ thống. Mã chương trình đồng bộ được bổ sung vào trong bộ biên dịch và thực thi khi chạy chương trình.

  • Monitor là một modul đóng gói
  • Các cấu trúc dữ liệu được chia sẻ.
  • Các thủ tục hoạt động thao tác trên các cấu trúc dữ liệu chia sẻ.
  • Đồng bộ các luồng thực thi đồng thời mà có thể kích hoạt các thủ tục trong hoạt động hệ thống.
  • Monitor có thể bảo vệ dữ liệu khỏi sự truy nhập không có cấu trúc. Nó đảm bảo rằng các luồng truy nhập vào dữ liệu thông qua các thủ tục tương tác theo những cách hợp pháp và có kiểm soát.
  • Monitor đảm bảo loại trừ xung đột
  • Chỉ có một luồng có thể thực thi bất kỳ thủ tục nào tại mỗi một thời điểm (luồng trong monitor)
  • Nếu có một luồng đang thực thi bên trong một monitor nó sẽ khoá các luồng khác muốn vào, do đó monitor cũng phải có một hàng đợi.

 

Hình 7.7: Minh họa về Monitor

 

Nguồn: voer