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 tình trạng đả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 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 bối cảnh âm thanh. Kết quả của bạn có thể khác nhau và bạn nên tự đánh giá và kiểm thử.
Nền
Máy chủ âm thanh AudioFlinger của Android và quá trình triển khai ứng dụng khách AudioTrack/AudioRecord đang được thiết kế lại để 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 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 dễ dự đoán hơn. Việc lập lịch đáng tin cậy cho phép giảm kích thước và số lượng bộ đệm âm thanh mà vẫn tránh được tình trạng thiếu và tràn bộ đệm.
Đảo ngược mức độ ưu tiên
Đảo ngược mức độ ưu tiên là một chế độ lỗi cổ điển của hệ thống 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 một tài nguyên, chẳng hạn như (trạng thái được chia sẻ được bảo vệ bởi) một mutex.
Trong hệ thống âm thanh, tình trạng đảo ngược mức độ ưu tiên thường biểu hiện dưới dạng trục trặc (tiếng tách, tiếng nổ, tiếng rớt), âm thanh lặp lại khi sử dụng bộ đệm vòng hoặc độ trễ trong việc phản hồi lệnh.
Một giải pháp phổ biến cho tình trạng đảo ngược mức độ ưu tiên là tăng kích thước bộ đệ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 có nhiều khả năng xảy ra ở những vị trí sau. Vì vậy, bạn nên tập trung vào những vị trí 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, chẳng hạn như cho điện thoại hoặc loại bỏ tiếng vọng
- trong trình điều khiển âm thanh trong kernel
- giữa luồng gọi lại AudioTrack hoặc AudioRecord và các luồng ứng dụng khác (ngoài tầm kiểm soát của chúng tôi)
Các giải pháp phổ biến
Các giải pháp điển hình bao gồm:
- tắt gián đoạn
- mutex kế thừa mức độ ưu tiên
Việc tắt gián đoạn là không khả thi trong không gian người dùng Linux và không hoạt động đối với Bộ xử lý đa đối xứng (SMP).
Futex kế thừa mức độ ưu tiên (mutex không gian người dùng nhanh) không được sử 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 khách đáng tin cậy.
Các kỹ thuật mà Android sử dụng
Các thử nghiệm bắt đầu bằng "thử khoá" và khoá có 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. Thử khoá và khoá có thời gian chờ hoạt động khá tốt nhưng dễ bị ảnh hưởng bởi 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 được chia sẻ nếu ứng dụng khách 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 thao tác 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. Trong thực tế, chúng tôi nhận thấy rằng số 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 để hoàn thiện 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ụ ở trên, đồng thời gần đây đã thêm các kỹ thuật sau:
- Sử dụng hàng đợi FIFO một trình đọc một trình ghi không chặn cho dữ liệu.
- 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 có thể được truy cập nguyên tử 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 nhiều từ phức tạp, 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 trình đọc một trình ghi không chặn được 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 lần đẩy liền kề thành một lần đẩy.
- Chú ý đến các rào cản bộ nhớ để đảm bảo tính chính xác của SMP.
- Tin tưởng, nhưng hãy xác minh. Khi chia sẻ trạng thái giữa các quy trình, đừng cho rằng trạng thái đó được định dạng tốt. Ví dụ: hãy kiểm tra để đảm bảo rằng các chỉ mục nằm trong giới hạn. Không cần xác minh này giữa các luồng trong cùng một quy trình, giữa các quy trình tin tưởng lẫn nhau (thường có cùng UID). Việc 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 đó việc hỏng dữ liệu là 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 trình đọc một trình ghi, chúng tôi nhận thấy các thuật toán này 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 một trình đọc/ghi 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ùng cho mục đích chung. 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 này 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ã xử lý ứng dụng mẫu OpenSL ES 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 thư viện nguồn mở không phải Android.
Chúng tôi đã xuất bản một ví dụ về quá trình triển khai FIFO không chặn được thiết kế riêng cho mã xử lý ứng dụng. Hãy 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ững gì chúng tôi biết, không có công cụ tự động nào để tìm tình trạng đảo ngược mức độ ưu tiên, đặc biệt là trước khi tình trạng này 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 thấy tình trạng đảo ngược mức độ ưu tiên nếu có thể truy cập vào toàn bộ mã nguồn. Tất nhiên, nếu có mã người dùng tuỳ ý (như ở đây đối với ứng dụng) hoặc là toàn bộ mã nguồn lớn (như đối với nhân hệ điều hành 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 vững toàn bộ hệ thống và các tương tác. Các công cụ như
systrace
và
ps -t -p
rất hữu ích để xem tình trạng đả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 cuối
Sau tất cả những 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 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 vấn đề hơn.