Tránh đảo ngược mức độ ưu tiên

Bài viết này giải thích cách hệ thống âm thanh của Android cố gắng tránh đảo ngược mức độ ưu tiên và nêu bật những kỹ thuật mà bạn cũng có thể sử dụng.

Những kỹ thuật này có thể hữu ích cho các nhà phát triển ứng dụng âm thanh hiệu suất cao, OEM và nhà cung cấp SoC đang triển khai HAL âm thanh. Xin lưu ý rằng việc triển khai các kỹ thuật này không đảm bảo ngăn chặn được các trục trặc hoặc lỗi khác, đặc biệt là nếu được sử dụng bên ngoài ngữ cảnh âm thanh. Kết quả của bạn có thể khác và bạn nên tự đánh giá cũng như kiểm thử.

Thông tin khái quát

Máy chủ âm thanh Android AudioFlinger và việc triển khai ứng dụng AudioTrack/AudioRecord đang được tái cấu trúc để giảm độ trễ. Công việc này bắt đầu từ Android 4.1 và tiếp tục được cải thiện hơn nữa trong các phiên bản 4.2, 4.3, 4.4 và 5.0.

Để đạt được độ trễ thấp hơn này, hệ thống cần có nhiều thay đổi. Một thay đổi quan trọng là chỉ định tài nguyên CPU cho các luồng quan trọng về thời gian bằng chính sách lập lịch có thể dự đoán hơn. Lập lịch đáng tin cậy cho phép giảm số lượng và kích thước vùng đệm âm thanh trong khi vẫn tránh được tình trạng thiếu hụt và tràn bộ nhớ.

Đảo ngược mức độ ưu tiên

Đảo ngược mức độ ưu tiên là một chế độ lỗi kinh điển của hệ thống theo thời gian thực, trong đó một tác vụ có mức độ ưu tiên cao hơn bị chặn trong một khoảng thời gian không giới hạn khi chờ một tác vụ có mức độ ưu tiên thấp hơn giải phóng một tài nguyên (chẳng hạn như trạng thái dùng chung được bảo vệ) bằng một mutex.

Trong hệ thống âm thanh, đảo ngược mức độ ưu tiên thường biểu hiện dưới dạng trục trặc (tiếng lách cách, tiếng bốp, tiếng rớt), âm thanh lặp lại khi sử dụng bộ nhớ đệm vòng hoặc độ trễ khi phản hồi lệnh.

Một giải pháp thường dùng cho tình trạng đảo ngược mức độ ưu tiên là tăng kích thước vùng đệm âm thanh. Tuy nhiên, phương thức này làm tăng độ trễ và chỉ che giấu vấn đề thay vì giải quyết vấn đề. Bạn nên hiểu và ngăn chặn tình trạng đảo ngược mức độ ưu tiên, như minh hoạ bên dưới.

Trong quá trình triển khai âm thanh trên Android, tình trạng đảo ngược mức độ ưu tiên thường xảy ra nhất ở những nơi sau. Vì vậy, bạn nên tập trung vào những điều sau:

  • giữa luồng bộ trộn thông thường và luồng bộ trộn nhanh trong AudioFlinger
  • giữa luồng gọi lại ứng dụng cho AudioTrack nhanh và luồng bộ trộn nhanh (cả hai đều có mức độ ưu tiên cao, nhưng mức độ ưu tiên hơi khác nhau)
  • giữa luồng gọi lại ứng dụng cho AudioRecord nhanh và luồng chụp nhanh (tương tự như trước)
  • trong quá trình triển khai Lớp trừu tượng phần cứng (HAL) âm thanh, ví dụ: cho điện thoại hoặc tính năng khử tiếng vang
  • trong trình điều khiển âm thanh trong nhân
  • giữa luồng gọi lại AudioTrack hoặc AudioRecord và các luồng ứng dụng khác (việc này nằm ngoài tầm kiểm soát của chúng tôi)

Các giải pháp thường gặp

Các giải pháp thường gặp bao gồm:

  • tắt các gián đoạn
  • mutex kế thừa mức độ ưu tiên

Không thể tắt các gián đoạn trong không gian người dùng Linux và không hoạt động đối với Bộ xử lý đa năng đối xứng (SMP).

Tính năng kế thừa mức độ ưu tiên futexes (mutex không gian người dùng nhanh) không được dùng trong hệ thống âm thanh vì tương đối nặng và vì chúng dựa vào một ứng dụng đáng tin cậy.

Các kỹ thuật mà Android sử dụng

Các thử nghiệm bắt đầu bằng "try lock" và khoá có thời gian chờ. Đây là các biến thể chặn có giới hạn và không chặn của thao tác khoá mutex. Thử khoá và khoá có thời gian chờ hoạt động khá tốt nhưng dễ gặp phải một số chế độ lỗi không rõ ràng: máy chủ không đảm bảo có thể truy cập vào trạng thái dùng chung nếu máy khách đang bận và thời gian chờ tích luỹ có thể quá dài nếu có một chuỗi dài các khoá không liên quan đều hết thời gian chờ.

Chúng tôi cũng sử dụng các thao tác nguyên tử, chẳng hạn như:

  • tăng dần
  • bitwise "or"
  • bitwise "and"

Tất cả các hàm này đều trả về giá trị trước đó và bao gồm các rào cản SMP cần thiết. Nhược điểm là chúng có thể yêu cầu số lần thử lại không giới hạn. Trên thực tế, chúng tôi nhận thấy rằng các lần thử lại không phải là vấn đề.

Lưu ý: Các thao tác nguyên tử và tương tác của chúng với các rào cản bộ nhớ thường bị hiểu sai và sử dụng không đúng cách. Chúng tôi đưa các phương thức này vào đây để cung cấp thông tin đầy đủ, nhưng bạn cũng nên đọc bài viết SMP Primer for Android để biết thêm thông tin.

Chúng tôi vẫn có và sử dụng hầu hết các công cụ nêu trên, đồng thời gần đây đã bổ sung những kỹ thuật sau:

  • Sử dụng hàng đợi FIFO một người đọc một người ghi không chặn cho dữ liệu.
  • Hãy thử sao chép trạng thái thay vì chia sẻ trạng thái giữa các mô-đun có mức độ ưu tiên cao và thấp.
  • Khi cần chia sẻ trạng thái, hãy giới hạn trạng thái ở từ có kích thước tối đa mà có thể truy cập một cách riêng lẻ trong một thao tác trên một bus mà không cần thử lại.
  • Đối với trạng thái phức tạp có nhiều từ, hãy sử dụng hàng đợi trạng thái. Hàng đợi trạng thái về cơ bản chỉ là hàng đợi FIFO một người đọc một người ghi không chặn được dùng cho trạng thái thay vì dữ liệu, ngoại trừ việc người ghi sẽ thu gọn các thao tác đẩy liền kề thành một thao tác đẩy duy nhất.
  • Chú ý đến rào cản bộ nhớ để đảm bảo tính chính xác của SMP.
  • Tin tưởng nhưng phải xác minh. Khi chia sẻ trạng thái giữa các quy trình, đừng giả định rằng trạng thái đó có dạng thức phù hợp. Ví dụ: kiểm tra để đảm bảo các chỉ mục nằm trong phạm vi. Không cần xác minh giữa các luồng trong cùng một quy trình, giữa các quy trình tin cậy lẫn nhau (thường có cùng UID). Điều này cũng không cần thiết đối với dữ liệu được chia sẻ, chẳng hạn như âm thanh PCM, trong đó lỗi không đáng kể.

Thuật toán không chặn

Các thuật toán không chặn là chủ đề của nhiều nghiên cứu gần đây. Tuy nhiên, ngoại trừ hàng đợi FIFO một người đọc một người ghi, chúng tôi nhận thấy các hàng đợi này rất phức tạp và dễ xảy ra lỗi.

Kể từ Android 4.2, bạn có thể tìm thấy các lớp không chặn, một trình đọc/ghi của chúng tôi ở những vị trí sau:

  • frameworks/av/include/media/nbaio/
  • frameworks/av/media/libnbaio/
  • frameworks/av/services/audioflinger/StateQueue*

Những API này được thiết kế riêng cho AudioFlinger và không phải là API đa năng. Các thuật toán không chặn nổi tiếng là khó gỡ lỗi. Bạn có thể xem mã này như một mô hình. Tuy nhiên, hãy lưu ý rằng có thể có lỗi và các lớp không được đảm bảo phù hợp cho các mục đích khác.

Đối với nhà phát triển, một số mã ứng dụng OpenSL ES mẫu cần được cập nhật để sử dụng các thuật toán không chặn hoặc tham chiếu đến một thư viện nguồn mở không phải của Android.

Chúng tôi đã xuất bản một ví dụ về việc triển khai FIFO không chặn được thiết kế dành riêng cho mã ứng dụng. Xem các tệp này trong thư mục nguồn nền tảng frameworks/av/audio_utils:

Công cụ

Theo những gì chúng tôi biết, không có công cụ tự động nào để tìm ra sự đảo ngược mức độ ưu tiên, đặc biệt là trước khi điều đó xảy ra. Một số công cụ phân tích mã tĩnh nghiên cứu có khả năng tìm ra các trường hợp đảo ngược mức độ ưu tiên nếu có thể truy cập vào toàn bộ cơ sở mã. Tất nhiên, nếu có mã người dùng tuỳ ý (như ở đây đối với ứng dụng) hoặc là một cơ sở mã lớn (như đối với nhân Linux và trình điều khiển thiết bị), thì việc phân tích tĩnh có thể không thực tế. Điều quan trọng nhất là bạn phải đọc mã thật kỹ và nắm bắt được toàn bộ hệ thống cũng như các hoạt động tương tác. Các công cụ như systraceps -t -p rất hữu ích để xem đảo ngược mức độ ưu tiên sau khi xảy ra, nhưng không cho bạn biết trước.

Lời kết

Sau tất cả những nội dung thảo luận này, đừng ngại sử dụng mutex. Mutex là bạn của bạn khi sử dụng thông thường, khi được dùng và triển khai đúng cách trong các trường hợp sử dụng thông thường không quan trọng về thời gian. Nhưng giữa các tác vụ có mức độ ưu tiên cao và thấp, cũng như trong các hệ thống nhạy cảm về thời gian, mutex có nhiều khả năng gây ra sự cố hơn.