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 các kỹ thuật mà bạn cũng có thể sử dụng.
Các 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, nhà sản xuất thiết bị gốc (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 sẽ ngăn chặn được sự cố hoặc các lỗi khác, đặc biệt là nếu 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á và kiểm thử.
Thông tin khái quát
Chúng tôi đang tái cấu trúc máy chủ âm thanh AudioFlinger của Android và cách triển khai ứng dụng AudioTrack/AudioRecord để giảm độ trễ. Công việc này bắt đầu trong Android 4.1 và tiếp tục cải tiến trong 4.2, 4.3, 4.4 và 5.0.
Để đạt được độ trễ thấp hơn này, cần có nhiều thay đổi trong toàn bộ hệ thống. 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ên lịch dễ dự đoán hơn. Tính năng lập lịch biểu đáng tin cậy cho phép giảm kích thước và số lượng vùng đệm âm thanh trong khi vẫn tránh tình trạng thiếu và vượt quá.
Đảo ngược mức độ ưu tiên
Đảo ngược mức độ ưu tiên là một lỗi điển hình của các 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 để chờ một tác vụ có mức độ ưu tiên thấp hơn giải phóng tài nguyên, chẳng hạn như (trạng thái dùng chung được bảo vệ bằng) mutex.
Trong hệ thống âm thanh, hiện tượng đảo ngược mức độ ưu tiên thường biểu hiện dưới dạng lỗi (nhấp, bật lên, bị ngắt), âm thanh lặp lại khi sử dụng vùng đệm tròn hoặc chậm trễ trong việc phản hồi lệnh.
Một giải pháp phổ biến cho việc đả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 việc đảo ngược mức độ ưu tiên, như trong ví dụ dưới đây.
Trong quá trình triển khai âm thanh Android, việc đảo ngược mức độ ưu tiên có nhiều khả năng xảy ra nhất ở những vị trí này. Vì vậy, bạn nên tập trung vào những điểm sau:
- giữa luồng bộ trộn âm thanh thông thường và luồng bộ trộn âm thanh 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 khác nhau một chút)
- 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ụ: để thực hiện điện thoại hoặc loại bỏ tiếng vọng
- trong trình điều khiển âm thanh trong hạt nhân
- giữa luồng gọi lại AudioTrack hoặc AudioRecord và các luồng ứng dụng khác (đây là điều 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
Sau đây là các giải pháp điển hình:
- tắt các ngắt
- mutex kế thừa mức độ ưu tiên
Bạn không thể tắt các tín hiệu ngắt trong không gian người dùng Linux và không thể sử dụng cho các Bộ xử lý đa năng đối xứng (SMP).
Tính năng kế thừa ưu tiên futex (mutex không gian người dùng nhanh) không được dùng trong hệ thống âm thanh vì các tính năng này tương đối nặng và phụ thuộc 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á với thời gian chờ. Đây là các biến thể không chặn và chặn có giới hạn của thao tác khoá mutex. Hãy thử khoá và khoá có thời gian chờ hoạt động khá tốt nhưng dễ bị một số chế độ lỗi không rõ ràng: máy chủ không được đảm bảo có thể truy cập vào trạng thái dùng chung nếu ứng dụng đ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ử như:
- tăng dần
- "hoặc" bit
- 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à các phương thức này có thể yêu cầu thử lại không giới hạn. Trong thực tế, chúng tôi nhận thấy rằng việc thử lại không phải là vấn đề.
Lưu ý: Các thao tác nguyên tử và hoạt động tương tác của các thao tác này 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 để đảm bảo tính đầy đủ, nhưng bạn cũng nên đọc bài viết SMP Primer for Android (Giới thiệu về SMP cho 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ụ trên, đồng thời gần đây đã thêm các kỹ thuật sau:
- Sử dụng hàng đợi FIFO không chặn cho một trình đọc và một trình ghi dữ liệu.
- Cố gắng 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 có thể truy cập nguyên tử trong thao tác một bus mà không cần thử lại.
- Đối với trạng thái phức tạp gồm nhiều từ, hãy sử dụng hàng đợi trạng thái. Về cơ bản, hàng đợi trạng thái chỉ là hàng đợi FIFO một trình đọc, một trình ghi không chặn dùng cho trạng thái thay vì dữ liệu, ngoại trừ việc trình ghi thu gọn các thao tác đẩy liền kề thành một thao tác đẩy duy nhất.
- Hãy chú ý đến hàng rào 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 định dạng tốt. Ví dụ: kiểm tra để đảm bảo rằng các chỉ mục nằm trong giới hạn. Bạn 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 một UID). Bạn cũng không cần phải sử dụng tính năng này cho dữ liệu dùng chung, chẳng hạn như âm thanh PCM, trong đó việc hỏng dữ liệu không quan trọng.
Thuật toán không chặn
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 trình đọc một trình ghi, chúng tôi nhận thấy các hàng đợi này phức tạp và dễ gặp lỗi.
Kể từ Android 4.2, bạn có thể tìm thấy các lớp trình đọc/ghi đơn không chặn 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*
Các lớp này được thiết kế riêng cho AudioFlinger và không dành cho mục đích chung. Các thuật toán không chặn nổi tiếng là rất khó gỡ lỗi. Bạn có thể xem mã này dưới dạng một mô hình. Tuy nhiên, hãy lưu ý rằng có thể có lỗi và các lớp này không được đảm bảo là phù hợp với 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 Android.
Chúng tôi đã xuất bản một ví dụ về cách triển khai FIFO không chặn được thiết kế riêng cho mã ứng dụng. Xem các tệp này nằm trong thư mục nguồn nền tảng frameworks/av/audio_utils
:
Công cụ
Theo như chúng tôi biết, không có công cụ tự động nào để tìm lỗi đảo ngược mức độ ưu tiên, đặc biệt là trước khi lỗi này xảy ra. Một số công cụ phân tích mã tĩnh nghiên cứu có thể tìm thấy các phép đảo ngượ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ó liên quan đến 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à đọc mã rất cẩn thận 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ư systrace và ps -t -p
rất hữu ích để xem việc đảo ngược mức độ ưu tiên sau khi nó xảy ra, nhưng không cho bạn biết trước.
Lời kết
Sau tất cả những cuộc thảo luận này, đừng ngại sử dụng mutex. Mutex là một công cụ hữu ích cho việc sử dụng thông thường, khi được sử 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. Tuy nhiên, 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.