요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
Coding style
4.Coding.rst:18-72커널의 표준 style은 Documentation/process/coding-style.rst에 정의된다. 오래된 in-tree code 일부가 규칙을 따르지 않는다고 해서 새 code도 자유롭게 작성할 수 있다는 뜻은 아니다. Style이 맞지 않으면 많은 reviewer가 내용 검토 전에 reformat을 요구한다.
큰 code base에서는 어느 위치든 빠르게 읽을 수 있는 일관성이 필요하다. 회사의 의무 style과 충돌하더라도 mainline에 넣으려면 kernel style을 따라야 한다. Upstream 기여는 formatting을 포함해 code에 대한 일부 통제권을 community와 나누는 일이다.
반대로 기존 code를 style만 고치기 위한 patch도 피한다. 이런 reformat patch는 실제 변경을 방해하는 noise로 받아들여진다. 다른 목적의 code를 수정하면서 주변 style을 함께 바로잡는 것은 자연스럽지만 style 변경 자체가 목적이면 안 된다.
Style 문서는 절대 어길 수 없는 법은 아니다. 예를 들어 80 column에 맞추려고 나눈 line이 훨씬 읽기 어렵다면 더 읽기 좋은 표현을 택할 수 있다.
clang-format은 code 일부의 formatting, 전체 file의 실수 탐색, include 정렬, variable·macro 정렬과 text reflow를 도울 수 있다. EditorConfig를 지원하는 editor는 indentation과 line ending 같은 기본값을 자동 적용할 수 있다.
필요한 만큼만 abstraction하기
4.Coding.rst:74-106커널 규모에서 abstraction은 필수지만 지나치거나 너무 이른 abstraction은 premature optimization만큼 해롭다. 실제 요구 수준까지만 추상화해야 한다.
모든 caller가 항상 0을 넘기는 function argument를 미래 flexibility를 위해 남기면 사용되지 않은 path가 눈치채지 못한 채 깨지거나, 실제 미래 요구가 처음 예상과 다를 수 있다. Kernel developer가 unused argument를 제거하는 이유이며 처음부터 넣지 않는 편이 낫다.
여러 OS에서 driver 본체를 공유하려고 hardware access를 숨기는 layer는 Linux code를 흐리고 성능 비용을 만들 수 있어 특히 환영받지 못한다.
반면 다른 subsystem에서 상당량의 code를 복사하려 한다면 library로 분리하거나 더 높은 layer에 공통 기능을 두어야 하는지 검토해야 한다. 같은 code를 커널 곳곳에 복제할 가치는 없다.
#ifdef와 C preprocessor
4.Coding.rst:108-133Preprocessor는 C가 아니며 과도하게 사용하면 사람이 읽기 어렵고 compiler의 type·correctness 검사도 약해진다. Source file 곳곳에 #ifdef block을 흩뿌리는 대신 가능하면 header에 조건을 모은다.
기능이 없을 때 빈 function이 되도록 conditional code를 감싸면 compiler가 call을 제거하고 본체 흐름은 깨끗하게 유지된다.
Macro는 side effect가 있는 expression을 여러 번 평가할 수 있고 type safety가 없다. 가능한 경우 inline function을 사용하면 생성 code는 비슷하면서 argument 단일 평가와 compiler type checking을 얻는다.
Inline function의 실제 비용
4.Coding.rst:135-158Function call을 없애는 효율만 보고 inline을 남발하면 각 call site에 code가 복제되어 kernel image가 커지고 instruction cache pressure가 늘어 오히려 느려질 수 있다. Inline function은 작고 드물게 사용해야 한다.
현대 hardware에서는 공간이 곧 시간일 수 있어 고전적인 단순 time/space tradeoff가 그대로 적용되지 않는다. 최신 compiler는 inline keyword와 별개로 실제 inline 여부를 적극 판단하므로 keyword 남발은 불필요할 수도 있다.
Concurrency와 locking
4.Coding.rst:160-191Devicescape wireless stack은 GPL로 공개된 뒤 mainline까지 약 1년이 걸렸다. Corporate 내부에서 single-processor만 고려해 개발한 흔적이 있었고, 현재 mac80211이 되기 전에 multiprocessor용 locking scheme을 뒤늦게 추가해야 했기 때문이다.
현대 kernel에서는 single-processor system도 responsiveness 향상을 위한 concurrency가 존재한다. 여러 thread가 동시에 접근할 수 있는 data structure와 hardware register는 올바른 lock으로 보호해야 한다. 구현 뒤 locking을 덧붙이는 일은 처음부터 설계하는 것보다 훨씬 어렵다.
Kernel developer는 spinlock, mutex, RCU, atomic 등 available primitive의 context와 의미를 이해하고 맞는 도구를 골라야 한다. Concurrency를 고려하지 않은 code는 mainline merge가 어렵다.
Regression과 user-space ABI
4.Coding.rst:193-226큰 개선을 위해 기존 사용자 일부를 깨뜨리는 변경도 regression이다. Mainline은 regression을 강하게 거부하며 빨리 고치지 못하면 원인 patch를 revert하는 것이 원칙이다.
새 system 10개를 지원하면서 기존 system 하나를 깨뜨리는 것이 이득이라는 계산은 받아들여지지 않는다. 새 문제를 만들어 bug를 고치면 실제 진전이 있었는지 알 수 없기 때문이다.
특히 user-space ABI regression은 심각하다. User space에 공개한 interface는 원칙적으로 영구 지원해야 하므로 처음 설계할 때부터 충분히 생각하고 명확히 문서화하며 넓게 review받아야 한다.
Code checking과 runtime debug 도구
4.Coding.rst:228-332완전히 error-free인 code가 어렵더라도 mainline 전에 최대한 많은 문제를 자동으로 찾아야 한다. Computer가 미리 잡은 문제는 사용자 system에서 발생하지 않는다.
make KCFLAGS=-W
make C=1
make coccicheck
- Compiler warning: review에 보낼 code는 warning이 없어야 한다. 원인을 이해하지 않고 warning만 숨기는 수정은 피한다.
- FRAME_WARN: 지정 크기보다 큰 stack frame을 경고한다.
- DEBUG_OBJECTS: kernel object lifetime과 잘못된 operation 순서를 추적한다.
- DEBUG_SLAB: memory allocation과 사용 오류를 찾는다.
- DEBUG_SPINLOCK, DEBUG_ATOMIC_SLEEP, DEBUG_MUTEXES: 흔한 locking·context 오류를 찾는다.
- lockdep: lock 획득·해제, lock ordering, interrupt context를 추적해 드문 deadlock 가능성을 미리 찾는다.
- Fault injection: memory allocation 등을 의도적으로 실패시켜 평소 실행되지 않는 error recovery path를 test한다.
- sparse: user/kernel address 혼동, endian type 혼합, bit flag와 integer 혼동 같은 static issue를 찾는다.
- Coccinelle: semantic patch로 coding problem을 찾고 수정안을 제안하며 광범위한 API conversion에도 유용하다.
- Cross compilation: 실제 board가 없어도 다른 architecture용 compile로 portability bug를 찾는다.
복잡한 locking이 있는 code는 제출 전에 lockdep을 켜고 실행해야 한다. Debug option 일부는 성능 비용이 크므로 항상 enable하지는 않지만 각 option의 검출 범위를 익혀 필요한 test에 사용해야 한다.
Patch와 API 문서화
4.Coding.rst:334-390충분한 문서는 merge를 쉽게 하고 다른 developer와 사용자에게 도움을 준다. 많은 경우 문서 추가는 사실상 의무다.
- Changelog: 해결하는 문제, solution 형태, 참여자, 성능 영향과 patch를 적용해야 하는 이유를 적는다.
- 새 user-space interface: sysfs와 /proc을 포함해 Documentation/ABI/README 형식으로 설명한다.
- 새 boot parameter: Documentation/admin-guide/kernel-parameters.rst에 추가한다.
- 새 Kconfig option: 기능과 사용자가 언제 선택해야 하는지 설명하는 help text를 넣는다.
- 외부에서 쓰는 internal API: subsystem의 kernel-doc comment를 유지하고 필요한 function에 추가한다.
새 code는 스스로 읽을 수 있어야 하며 comment는 미묘한 이유를 설명해야 한다. Verbose하게 code를 다시 말하는 comment는 필요 없다.
Memory barrier에는 왜 필요한지, data structure에는 locking rule, 주요 structure에는 전체 설계, 떨어진 code 사이의 숨은 dependency에는 관계를 적어야 한다. 겉보기에 불필요해 code janitor가 잘못 cleanup할 수 있는 부분에는 현재 형태의 이유를 남긴다.
Internal API 변경
4.Coding.rst:392-426User-space binary interface와 달리 kernel internal API는 필요하면 바꿀 수 있다. API를 우회하고 있거나 필요한 기능이 없어 쓰지 못한다면 API 자체를 개선할 신호일 수 있다.
Internal API 변경 patch는 무엇을 왜 바꾸는지 충분히 설명하고 더 큰 feature patch 안에 숨기지 말고 별도 patch로 분리한다.
API를 바꾼 개발자는 그 변경으로 깨지는 모든 in-tree code를 함께 고쳐야 한다. 널리 쓰는 function이면 수백~수천 곳의 변경과 다른 개발자의 작업 conflict가 생길 수 있으므로 근거가 충분해야 한다. Coccinelle이 광범위한 변경을 도울 수 있다.
Incompatible change는 가능하면 갱신되지 않은 caller가 compiler error로 드러나게 설계한다. In-tree use를 빠짐없이 찾고 out-of-tree 개발자도 변경을 알아차리게 할 수 있다. Out-of-tree 지원 의무는 없지만 불필요하게 더 어렵게 만들 이유도 없다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
.. _development_coding:
Getting the code right
======================
While there is much to be said for a solid and community-oriented design
process, the proof of any kernel development project is in the resulting
code. It is the code which will be examined by other developers and merged
(or not) into the mainline tree. So it is the quality of this code which
will determine the ultimate success of the project.
This section will examine the coding process. We'll start with a look at a
number of ways in which kernel developers can go wrong. Then the focus
will shift toward doing things right and the tools which can help in that
quest.
Pitfalls
---------
Coding style
************
The kernel has long had a standard coding style, described in
:ref:`Documentation/process/coding-style.rst <codingstyle>`. For much of
that time, the policies described in that file were taken as being, at most,
advisory. As a result, there is a substantial amount of code in the kernel
which does not meet the coding style guidelines. The presence of that code
leads to two independent hazards for kernel developers.
The first of these is to believe that the kernel coding standards do not
matter and are not enforced. The truth of the matter is that adding new
code to the kernel is very difficult if that code is not coded according to
the standard; many developers will request that the code be reformatted
before they will even review it. A code base as large as the kernel
requires some uniformity of code to make it possible for developers to
quickly understand any part of it. So there is no longer room for
strangely-formatted code.
Occasionally, the kernel's coding style will run into conflict with an
employer's mandated style. In such cases, the kernel's style will have to
win before the code can be merged. Putting code into the kernel means
giving up a degree of control in a number of ways - including control over
how the code is formatted.
The other trap is to assume that code which is already in the kernel is
urgently in need of coding style fixes. Developers may start to generate
reformatting patches as a way of gaining familiarity with the process, or
as a way of getting their name into the kernel changelogs - or both. But
pure coding style fixes are seen as noise by the development community;
they tend to get a chilly reception. So this type of patch is best
avoided. It is natural to fix the style of a piece of code while working
on it for other reasons, but coding style changes should not be made for
their own sake.
The coding style document also should not be read as an absolute law which
can never be transgressed. If there is a good reason to go against the
style (a line which becomes far less readable if split to fit within the
80-column limit, for example), just do it.
Note that you can also use the ``clang-format`` tool to help you with
these rules, to quickly re-format parts of your code automatically,
and to review full files in order to spot coding style mistakes,
typos and possible improvements. It is also handy for sorting ``#includes``,
for aligning variables/macros, for reflowing text and other similar tasks.
See the file :ref:`Documentation/dev-tools/clang-format.rst <clangformat>`
for more details.
Some basic editor settings, such as indentation and line endings, will be
set automatically if you are using an editor that is compatible with
EditorConfig. See the official EditorConfig website for more information:
https://editorconfig.org/
Abstraction layers
******************
Computer Science professors teach students to make extensive use of
abstraction layers in the name of flexibility and information hiding.
Certainly the kernel makes extensive use of abstraction; no project
involving several million lines of code could do otherwise and survive.
But experience has shown that excessive or premature abstraction can be
just as harmful as premature optimization. Abstraction should be used to
the level required and no further.
At a simple level, consider a function which has an argument which is
always passed as zero by all callers. One could retain that argument just
in case somebody eventually needs to use the extra flexibility that it
provides. By that time, though, chances are good that the code which
implements this extra argument has been broken in some subtle way which was
never noticed - because it has never been used. Or, when the need for
extra flexibility arises, it does not do so in a way which matches the
programmer's early expectation. Kernel developers will routinely submit
patches to remove unused arguments; they should, in general, not be added
in the first place.
Abstraction layers which hide access to hardware - often to allow the bulk
of a driver to be used with multiple operating systems - are especially
frowned upon. Such layers obscure the code and may impose a performance
penalty; they do not belong in the Linux kernel.
On the other hand, if you find yourself copying significant amounts of code
from another kernel subsystem, it is time to ask whether it would, in fact,
make sense to pull out some of that code into a separate library or to
implement that functionality at a higher level. There is no value in
replicating the same code throughout the kernel.
#ifdef and preprocessor use in general
**************************************
The C preprocessor seems to present a powerful temptation to some C
programmers, who see it as a way to efficiently encode a great deal of
flexibility into a source file. But the preprocessor is not C, and heavy
use of it results in code which is much harder for others to read and
harder for the compiler to check for correctness. Heavy preprocessor use
is almost always a sign of code which needs some cleanup work.
Conditional compilation with #ifdef is, indeed, a powerful feature, and it
is used within the kernel. But there is little desire to see code which is
sprinkled liberally with #ifdef blocks. As a general rule, #ifdef use
should be confined to header files whenever possible.
Conditionally-compiled code can be confined to functions which, if the code
is not to be present, simply become empty. The compiler will then quietly
optimize out the call to the empty function. The result is far cleaner
code which is easier to follow.
C preprocessor macros present a number of hazards, including possible
multiple evaluation of expressions with side effects and no type safety.
If you are tempted to define a macro, consider creating an inline function
instead. The code which results will be the same, but inline functions are
easier to read, do not evaluate their arguments multiple times, and allow
the compiler to perform type checking on the arguments and return value.
Inline functions
****************
Inline functions present a hazard of their own, though. Programmers can
become enamored of the perceived efficiency inherent in avoiding a function
call and fill a source file with inline functions. Those functions,
however, can actually reduce performance. Since their code is replicated
at each call site, they end up bloating the size of the compiled kernel.
That, in turn, creates pressure on the processor's memory caches, which can
slow execution dramatically. Inline functions, as a rule, should be quite
small and relatively rare. The cost of a function call, after all, is not
that high; the creation of large numbers of inline functions is a classic
example of premature optimization.
In general, kernel programmers ignore cache effects at their peril. The
classic time/space tradeoff taught in beginning data structures classes
often does not apply to contemporary hardware. Space *is* time, in that a
larger program will run slower than one which is more compact.
More recent compilers take an increasingly active role in deciding whether
a given function should actually be inlined or not. So the liberal
placement of "inline" keywords may not just be excessive; it could also be
irrelevant.
Locking
*******
In May, 2006, the "Devicescape" networking stack was, with great
fanfare, released under the GPL and made available for inclusion in the
mainline kernel. This donation was welcome news; support for wireless
networking in Linux was considered substandard at best, and the Devicescape
stack offered the promise of fixing that situation. Yet, this code did not
actually make it into the mainline until June, 2007 (2.6.22). What
happened?
This code showed a number of signs of having been developed behind
corporate doors. But one large problem in particular was that it was not
designed to work on multiprocessor systems. Before this networking stack
(now called mac80211) could be merged, a locking scheme needed to be
retrofitted onto it.
Once upon a time, Linux kernel code could be developed without thinking
about the concurrency issues presented by multiprocessor systems. Now,
however, this document is being written on a dual-core laptop. Even on
single-processor systems, work being done to improve responsiveness will
raise the level of concurrency within the kernel. The days when kernel
code could be written without thinking about locking are long past.
Any resource (data structures, hardware registers, etc.) which could be
accessed concurrently by more than one thread must be protected by a lock.
New code should be written with this requirement in mind; retrofitting
locking after the fact is a rather more difficult task. Kernel developers
should take the time to understand the available locking primitives well
enough to pick the right tool for the job. Code which shows a lack of
attention to concurrency will have a difficult path into the mainline.
Regressions
***********
One final hazard worth mentioning is this: it can be tempting to make a
change (which may bring big improvements) which causes something to break
for existing users. This kind of change is called a "regression," and
regressions have become most unwelcome in the mainline kernel. With few
exceptions, changes which cause regressions will be backed out if the
regression cannot be fixed in a timely manner. Far better to avoid the
regression in the first place.
It is often argued that a regression can be justified if it causes things
to work for more people than it creates problems for. Why not make a
change if it brings new functionality to ten systems for each one it
breaks? The best answer to this question was expressed by Linus in July,
2007:
::
So we don't fix bugs by introducing new problems. That way lies
madness, and nobody ever knows if you actually make any real
progress at all. Is it two steps forwards, one step back, or one
step forward and two steps back?
(https://lwn.net/Articles/243460/).
An especially unwelcome type of regression is any sort of change to the
user-space ABI. Once an interface has been exported to user space, it must
be supported indefinitely. This fact makes the creation of user-space
interfaces particularly challenging: since they cannot be changed in
incompatible ways, they must be done right the first time. For this
reason, a great deal of thought, clear documentation, and wide review for
user-space interfaces is always required.
Code checking tools
-------------------
For now, at least, the writing of error-free code remains an ideal that few
of us can reach. What we can hope to do, though, is to catch and fix as
many of those errors as possible before our code goes into the mainline
kernel. To that end, the kernel developers have put together an impressive
array of tools which can catch a wide variety of obscure problems in an
automated way. Any problem caught by the computer is a problem which will
not afflict a user later on, so it stands to reason that the automated
tools should be used whenever possible.
The first step is simply to heed the warnings produced by the compiler.
Contemporary versions of gcc can detect (and warn about) a large number of
potential errors. Quite often, these warnings point to real problems.
Code submitted for review should, as a rule, not produce any compiler
warnings. When silencing warnings, take care to understand the real cause
and try to avoid "fixes" which make the warning go away without addressing
its cause.
Note that not all compiler warnings are enabled by default. Build the
kernel with "make KCFLAGS=-W" to get the full set.
The kernel provides several configuration options which turn on debugging
features; most of these are found in the "kernel hacking" submenu. Several
of these options should be turned on for any kernel used for development or
testing purposes. In particular, you should turn on:
- FRAME_WARN to get warnings for stack frames larger than a given amount.
The output generated can be verbose, but one need not worry about
warnings from other parts of the kernel.
- DEBUG_OBJECTS will add code to track the lifetime of various objects
created by the kernel and warn when things are done out of order. If
you are adding a subsystem which creates (and exports) complex objects
of its own, consider adding support for the object debugging
infrastructure.
- DEBUG_SLAB can find a variety of memory allocation and use errors; it
should be used on most development kernels.
- DEBUG_SPINLOCK, DEBUG_ATOMIC_SLEEP, and DEBUG_MUTEXES will find a
number of common locking errors.
There are quite a few other debugging options, some of which will be
discussed below. Some of them have a significant performance impact and
should not be used all of the time. But some time spent learning the
available options will likely be paid back many times over in short order.
One of the heavier debugging tools is the locking checker, or "lockdep."
This tool will track the acquisition and release of every lock (spinlock or
mutex) in the system, the order in which locks are acquired relative to
each other, the current interrupt environment, and more. It can then
ensure that locks are always acquired in the same order, that the same
interrupt assumptions apply in all situations, and so on. In other words,
lockdep can find a number of scenarios in which the system could, on rare
occasion, deadlock. This kind of problem can be painful (for both
developers and users) in a deployed system; lockdep allows them to be found
in an automated manner ahead of time. Code with any sort of non-trivial
locking should be run with lockdep enabled before being submitted for
inclusion.
As a diligent kernel programmer, you will, beyond doubt, check the return
status of any operation (such as a memory allocation) which can fail. The
fact of the matter, though, is that the resulting failure recovery paths
are, probably, completely untested. Untested code tends to be broken code;
you could be much more confident of your code if all those error-handling
paths had been exercised a few times.
The kernel provides a fault injection framework which can do exactly that,
especially where memory allocations are involved. With fault injection
enabled, a configurable percentage of memory allocations will be made to
fail; these failures can be restricted to a specific range of code.
Running with fault injection enabled allows the programmer to see how the
code responds when things go badly. See
Documentation/fault-injection/fault-injection.rst for more information on
how to use this facility.
Other kinds of errors can be found with the "sparse" static analysis tool.
With sparse, the programmer can be warned about confusion between
user-space and kernel-space addresses, mixture of big-endian and
small-endian quantities, the passing of integer values where a set of bit
flags is expected, and so on. Sparse must be installed separately (it can
be found at https://sparse.wiki.kernel.org/index.php/Main_Page if your
distributor does not package it); it can then be run on the code by adding
"C=1" to your make command.
The "Coccinelle" tool (http://coccinelle.lip6.fr/) is able to find a wide
variety of potential coding problems; it can also propose fixes for those
problems. Quite a few "semantic patches" for the kernel have been packaged
under the scripts/coccinelle directory; running "make coccicheck" will run
through those semantic patches and report on any problems found. See
:ref:`Documentation/dev-tools/coccinelle.rst <devtools_coccinelle>`
for more information.
Other kinds of portability errors are best found by compiling your code for
other architectures. If you do not happen to have an S/390 system or a
Blackfin development board handy, you can still perform the compilation
step. A large set of cross compilers for x86 systems can be found at
https://www.kernel.org/pub/tools/crosstool/
Some time spent installing and using these compilers will help avoid
embarrassment later.
Documentation
-------------
Documentation has often been more the exception than the rule with kernel
development. Even so, adequate documentation will help to ease the merging
of new code into the kernel, make life easier for other developers, and
will be helpful for your users. In many cases, the addition of
documentation has become essentially mandatory.
The first piece of documentation for any patch is its associated
changelog. Log entries should describe the problem being solved, the form
of the solution, the people who worked on the patch, any relevant
effects on performance, and anything else that might be needed to
understand the patch. Be sure that the changelog says *why* the patch is
worth applying; a surprising number of developers fail to provide that
information.
Any code which adds a new user-space interface - including new sysfs or
/proc files - should include documentation of that interface which enables
user-space developers to know what they are working with. See
Documentation/ABI/README for a description of how this documentation should
be formatted and what information needs to be provided.
The file :ref:`Documentation/admin-guide/kernel-parameters.rst
<kernelparameters>` describes all of the kernel's boot-time parameters.
Any patch which adds new parameters should add the appropriate entries to
this file.
Any new configuration options must be accompanied by help text which
clearly explains the options and when the user might want to select them.
Internal API information for many subsystems is documented by way of
specially-formatted comments; these comments can be extracted and formatted
in a number of ways by the "kernel-doc" script. If you are working within
a subsystem which has kerneldoc comments, you should maintain them and add
them, as appropriate, for externally-available functions. Even in areas
which have not been so documented, there is no harm in adding kerneldoc
comments for the future; indeed, this can be a useful activity for
beginning kernel developers. The format of these comments, along with some
information on how to create kerneldoc templates can be found at
:ref:`Documentation/doc-guide/ <doc_guide>`.
Anybody who reads through a significant amount of existing kernel code will
note that, often, comments are most notable by their absence. Once again,
the expectations for new code are higher than they were in the past;
merging uncommented code will be harder. That said, there is little desire
for verbosely-commented code. The code should, itself, be readable, with
comments explaining the more subtle aspects.
Certain things should always be commented. Uses of memory barriers should
be accompanied by a line explaining why the barrier is necessary. The
locking rules for data structures generally need to be explained somewhere.
Major data structures need comprehensive documentation in general.
Non-obvious dependencies between separate bits of code should be pointed
out. Anything which might tempt a code janitor to make an incorrect
"cleanup" needs a comment saying why it is done the way it is. And so on.
Internal API changes
--------------------
The binary interface provided by the kernel to user space cannot be broken
except under the most severe circumstances. The kernel's internal
programming interfaces, instead, are highly fluid and can be changed when
the need arises. If you find yourself having to work around a kernel API,
or simply not using a specific functionality because it does not meet your
needs, that may be a sign that the API needs to change. As a kernel
developer, you are empowered to make such changes.
There are, of course, some catches. API changes can be made, but they need
to be well justified. So any patch making an internal API change should be
accompanied by a description of what the change is and why it is
necessary. This kind of change should also be broken out into a separate
patch, rather than buried within a larger patch.
The other catch is that a developer who changes an internal API is
generally charged with the task of fixing any code within the kernel tree
which is broken by the change. For a widely-used function, this duty can
lead to literally hundreds or thousands of changes - many of which are
likely to conflict with work being done by other developers. Needless to
say, this can be a large job, so it is best to be sure that the
justification is solid. Note that the Coccinelle tool can help with
wide-ranging API changes.
When making an incompatible API change, one should, whenever possible,
ensure that code which has not been updated is caught by the compiler.
This will help you to be sure that you have found all in-tree uses of that
interface. It will also alert developers of out-of-tree code that there is
a change that they need to respond to. Supporting out-of-tree code is not
something that kernel developers need to be worried about, but we also do
not have to make life harder for out-of-tree developers than it needs to
be.
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
올바른 코드를 만드는 일
1-15견고하고 community 중심적인 설계 절차가 중요하지만, kernel 개발 project의 성패를 최종적으로 증명하는 것은 그 결과로 나온 code다. 다른 개발자는 설계 의도만이 아니라 실제 code를 검토하고, 그 code를 mainline tree에 merge할지 결정한다. 따라서 project가 궁극적으로 성공하는지는 code의 품질에 달려 있다.
이 문서는 coding 과정에서 kernel 개발자가 흔히 빠지는 함정을 먼저 살펴본다. 이어서 올바른 구현 방향과, 문제를 mainline에 보내기 전에 찾아내는 데 도움이 되는 도구를 설명한다.
Coding style
18-72Kernel에는 오래전부터 Documentation/process/coding-style.rst에 정리된 표준 coding style이 있다. 과거에는 이 정책을 강제 규칙이 아니라 권고 정도로 취급한 시기가 길었고, 그 결과 현재 tree에도 지침을 따르지 않는 code가 상당량 남아 있다. 기존 code의 이런 모습은 개발자에게 서로 독립적인 두 가지 함정을 만든다.
첫 번째 함정은 kernel coding 표준이 중요하지 않으며 실제 review에서도 강제되지 않는다고 믿는 것이다. 현실은 반대다. 새 code가 표준에 맞게 작성되지 않았다면 kernel에 추가하기가 매우 어렵다. 많은 개발자는 내용 검토를 시작하기도 전에 먼저 형식을 고치라고 요청한다. Kernel처럼 거대한 code base에서 어느 영역이든 빠르게 이해하려면 일정한 형식이 필요하므로, 특이한 개인식 formatting을 허용할 여지가 없다.
회사에서 의무화한 style과 kernel style이 충돌할 때도 있다. 그 code를 kernel에 merge하려면 kernel style이 우선해야 한다. Code를 kernel에 넣는다는 것은 여러 측면에서 통제권 일부를 넘기는 일이며, code formatting을 결정하는 권한도 여기에 포함된다.
두 번째 함정은 이미 kernel 안에 있는 code가 coding style 수정부터 시급히 필요하다고 생각하는 것이다. 개발 절차를 익히거나 changelog에 이름을 남기기 위해 reformatting patch를 만들 수 있지만, community는 기능 변화가 없는 순수 style 수정은 대개 noise로 본다. 다른 이유로 code를 수정하면서 그 주변 style을 함께 바로잡는 것은 자연스럽지만, style 변경만을 목적으로 한 patch는 피하는 편이 좋다.
Coding style 문서를 절대로 어길 수 없는 법률처럼 읽어서도 안 된다. 예를 들어 80-column 제한에 맞추려고 줄을 나누었더니 훨씬 읽기 어려워진다면, 명확한 이유를 가지고 제한을 벗어날 수 있다.
clang-format은 규칙에 맞춰 code 일부를 빠르게 자동 정렬하고, 파일 전체에서 style 오류·오타·개선 후보를 찾는 데 쓸 수 있다. #include 정렬, 변수와 macro 정렬, text reflow 같은 작업에도 유용하다. 자세한 내용은 Documentation/dev-tools/clang-format.rst를 참고한다.
EditorConfig를 지원하는 editor라면 indentation이나 line ending 같은 기본 설정이 자동으로 적용된다. 추가 정보는 https://editorconfig.org/ 에서 확인할 수 있다.
Abstraction layer
74-105Computer Science 교육에서는 flexibility와 information hiding을 위해 abstraction layer를 폭넓게 사용하라고 가르친다. 수백만 line의 code로 이루어진 kernel 역시 abstraction 없이 유지될 수 없다. 그러나 경험상 지나치거나 너무 이른 abstraction은 premature optimization만큼 해로울 수 있다. 필요한 수준까지만 추상화하고 그 이상은 만들지 않아야 한다.
예를 들어 어떤 함수의 인자를 모든 caller가 항상 0으로 넘긴다고 하자. 언젠가 flexibility가 필요할지 모른다는 이유로 그 인자를 남겨 둘 수 있다. 그러나 실제로 사용될 때까지 한 번도 실행되지 않은 구현은 눈에 띄지 않은 채 망가졌을 가능성이 크다. 또는 훗날 요구되는 flexibility의 형태가 처음 예상과 전혀 다를 수 있다. Kernel 개발자는 사용되지 않는 인자를 제거하는 patch를 흔히 제출하므로, 일반적으로 그런 인자는 처음부터 추가하지 않는 것이 맞다.
Hardware 접근을 감추는 abstraction layer, 특히 driver 대부분을 여러 operating system에서 공용으로 사용하려고 만든 계층은 좋지 않게 평가된다. 이런 계층은 code의 실제 동작을 가리고 performance penalty까지 만들 수 있으므로 Linux kernel에 들어갈 자리가 아니다.
반대로 다른 kernel subsystem에서 상당한 양의 code를 복사하고 있다면, 공통 부분을 별도 library로 분리하거나 더 높은 계층에서 기능을 제공하는 것이 맞는지 검토해야 한다. 동일한 code를 kernel 곳곳에 반복해서 복제하는 데에는 가치가 없다.
#ifdef와 C preprocessor
108-132C preprocessor는 하나의 source file에 많은 flexibility를 효율적으로 넣는 수단처럼 보이기 때문에 일부 C programmer에게 강한 유혹이 된다. 하지만 preprocessor는 C 언어 자체가 아니다. 과도하게 사용하면 다른 사람이 읽기 어려워지고 compiler가 correctness를 검사하기도 어려워진다. Preprocessor 사용량이 지나치다면 code 정리가 필요하다는 신호인 경우가 거의 대부분이다.
#ifdef에 의한 conditional compilation은 강력하고 kernel 내부에서도 사용된다. 그렇다고 source 곳곳을 #ifdef block으로 채워서는 안 된다. 가능하면 #ifdef는 header file 안으로 제한한다. 조건에 따라 빠져야 하는 구현은, 해당 기능이 없을 때 빈 함수가 되는 작은 function으로 감쌀 수 있다. Compiler는 빈 함수 call을 제거하므로 결과 code는 더 깔끔하고 흐름도 따라가기 쉽다.
/* 조건부 구현은 header의 stub으로 격리하는 형태가 바람직하다. */
#ifdef CONFIG_FEATURE
void feature_run(struct device *dev);
#else
static inline void feature_run(struct device *dev)
{
}
#endif
위 code는 원문의 설명을 구체화한 예시이며 Linux v6.18.37 원문에 직접 포함된 snippet은 아니다.
C preprocessor macro에는 side effect를 가진 expression을 여러 번 평가할 수 있다는 위험과 type safety가 없다는 문제가 있다. Macro를 만들고 싶다면 먼저 inline function으로 바꿀 수 있는지 검토한다. 생성되는 machine code는 같을 수 있지만 inline function은 읽기 쉽고 인자를 중복 평가하지 않으며 compiler가 인자와 반환값의 type을 검사할 수 있다.
Inline function과 cache 비용
135-157Inline function에도 자체적인 위험이 있다. Function call을 없애면 효율적일 것이라는 생각에 source file을 inline function으로 가득 채우기 쉽지만, 오히려 performance가 떨어질 수 있다. Function body가 모든 call site에 복제되면서 compiled kernel 크기가 불어나고, processor cache에 압력을 주어 실행 속도가 크게 느려질 수 있기 때문이다.
일반적으로 inline function은 아주 작고 드물게 사용해야 한다. Function call 자체의 비용은 그렇게 크지 않다. 다수의 inline function을 만드는 것은 premature optimization의 전형적인 사례다.
Kernel programmer가 cache effect를 무시하면 큰 대가를 치를 수 있다. 입문 data structure 수업에서 말하는 전통적인 time/space tradeoff는 현대 hardware에 그대로 적용되지 않는 경우가 많다. 더 큰 program은 cache 효율이 나빠져 더 느리게 실행되므로, 이 환경에서는 공간이 곧 시간이다.
최근 compiler는 특정 함수를 실제로 inline할지 스스로 결정하는 데 더 적극적이다. 따라서 inline keyword를 널리 붙이는 일은 과도할 뿐 아니라 아무 효과도 없을 수 있다.
처음부터 concurrency와 locking을 설계한다
160-1902006년 5월 Devicescape networking stack이 GPL로 공개되어 mainline kernel에 들어갈 수 있게 되었을 때 큰 기대를 받았다. 당시 Linux wireless networking 지원은 부족하다는 평가를 받았고 이 stack이 상황을 개선할 것으로 보였다. 그러나 실제 mainline merge는 2007년 6월, Linux 2.6.22에서야 이루어졌다.
이 code에는 회사 내부에서 비공개로 개발된 흔적이 여러 곳에 있었고, 특히 multiprocessor system에서 동작하도록 설계되지 않았다는 큰 문제가 있었다. 현재 mac80211이라고 부르는 이 networking stack은 merge 전에 locking 체계를 사후에 추가해야 했다.
예전에는 multiprocessor가 만드는 concurrency 문제를 고려하지 않고 Linux kernel code를 작성할 수 있었다. 하지만 이 문서가 처음 작성될 당시에도 dual-core laptop은 이미 일상적인 장치였다. Single-processor system에서도 responsiveness를 높이는 작업은 kernel 내부 concurrency 수준을 올린다. Locking을 생각하지 않고 kernel code를 작성할 수 있던 시대는 오래전에 끝났다.
둘 이상의 thread가 동시에 접근할 수 있는 모든 resource, 즉 data structure와 hardware register 등은 lock으로 보호해야 한다. 새 code는 처음부터 이 요구를 반영해 작성해야 한다. 구현이 끝난 뒤 locking을 덧붙이는 일은 훨씬 어렵다. Kernel 개발자는 제공되는 locking primitive를 충분히 이해하고 상황에 맞는 도구를 골라야 하며, concurrency를 제대로 고려하지 않은 code는 mainline 진입 과정에서 큰 어려움을 겪는다.
Regression과 user-space ABI
193-225기존 사용자에게 동작하던 무언가를 깨뜨리는 대신 큰 개선을 얻는 변경을 시도하고 싶을 수 있다. 이런 변경을 regression이라고 하며 mainline kernel에서는 매우 강하게 거부된다. 극히 일부 예외를 제외하면 regression을 제때 고치지 못할 경우 해당 변경은 revert된다. 가장 좋은 방법은 처음부터 regression을 만들지 않는 것이다.
한 system을 깨뜨리는 대신 열 system에 새 기능을 제공한다면 regression을 정당화할 수 있다는 주장도 있다. 그러나 2007년 7월 Linus는 이 생각을 다음과 같이 설명했다.
So we don't fix bugs by introducing new problems. That way lies
madness, and nobody ever knows if you actually make any real
progress at all. Is it two steps forwards, one step back, or one
step forward and two steps back?
즉, 새로운 문제를 도입하는 방식으로 bug를 고치지 않는다. 그렇게 하면 실제로 전진했는지조차 판단할 수 없다. 두 걸음 나아가고 한 걸음 물러난 것인지, 한 걸음 나아가고 두 걸음 물러난 것인지 알 수 없기 때문이다.
특히 허용되지 않는 regression은 user-space ABI를 바꾸는 일이다. Interface가 user space로 한번 export되면 사실상 영구히 지원해야 한다. 이 때문에 user-space interface 설계는 특히 어렵다. 호환되지 않는 방식으로 나중에 고칠 수 없으므로 처음부터 제대로 만들어야 하며, 충분한 검토와 명확한 문서, 넓은 범위의 review가 항상 필요하다.
Compiler warning과 runtime debugging 도구
228-304오류 없는 code 작성은 아직 소수만이 도달할 수 있는 이상에 가깝다. 현실적인 목표는 mainline에 들어가기 전에 가능한 한 많은 오류를 찾아 고치는 것이다. Kernel 개발자는 잘 드러나지 않는 다양한 문제를 자동으로 찾는 도구를 마련해 왔다. Computer가 미리 찾은 문제는 나중에 사용자가 겪지 않아도 되는 문제이므로 가능한 모든 자동 검사 도구를 사용해야 한다.
첫 단계는 compiler warning을 무시하지 않는 것이다. 현대 gcc는 많은 잠재 오류를 찾아 경고할 수 있고, 그 경고는 실제 문제를 가리키는 경우가 많다. Review에 제출하는 code는 원칙적으로 compiler warning을 만들지 않아야 한다. Warning을 없앨 때는 근본 원인을 이해해야 하며, 원인을 해결하지 않은 채 메시지만 사라지게 하는 수정은 피한다.
make KCFLAGS=-W
기본값으로 활성화되지 않은 compiler warning까지 보려면 위와 같이 build한다. Kernel에는 debugging 기능을 켜는 configuration option도 많으며 대부분 kernel hacking submenu에 있다. 개발 또는 test용 kernel에서는 다음 option을 켜는 것이 좋다.
- FRAME_WARN: 지정 크기보다 큰 stack frame을 경고한다. 출력이 많아질 수 있지만 kernel의 다른 영역에서 나온 경고까지 모두 직접 해결할 필요는 없다.
- DEBUG_OBJECTS: kernel이 만드는 여러 object의 lifetime을 추적하고 operation 순서가 잘못되면 경고한다. 독자적인 복잡한 object를 생성하고 외부에 제공하는 subsystem을 추가한다면 object debugging infrastructure 지원도 고려한다.
- DEBUG_SLAB: memory allocation과 사용 과정에서 발생하는 다양한 오류를 찾는다. 대부분의 development kernel에서 사용해야 한다.
- DEBUG_SPINLOCK, DEBUG_ATOMIC_SLEEP, DEBUG_MUTEXES: 흔한 locking 오류를 다수 찾아낸다.
그 밖에도 debugging option이 많이 있다. 일부는 performance overhead가 커서 항상 켜 둘 수 없지만, 어떤 option이 있는지 학습하는 데 들인 시간은 대개 짧은 기간 안에 여러 배로 돌아온다.
더 무거운 debugging 도구 중 하나가 locking checker인 lockdep이다. Lockdep은 system의 모든 spinlock과 mutex 획득·해제, lock 사이의 획득 순서, 현재 interrupt 환경 등을 추적한다. 이를 바탕으로 lock이 항상 같은 순서로 잡히는지, 모든 경로에서 interrupt에 대한 전제가 일치하는지 검사한다. 드물게 발생할 수 있는 deadlock 시나리오를 배포 전에 자동으로 찾을 수 있으므로, 단순하지 않은 locking을 포함한 code는 제출 전에 lockdep을 켜고 실행해야 한다.
신중한 kernel programmer라면 memory allocation처럼 실패할 수 있는 operation의 return status를 확인한다. 하지만 그 결과로 작성한 failure recovery path는 실제로 한 번도 실행해 보지 않았을 가능성이 높다. Test되지 않은 code는 망가진 code일 가능성이 크므로 error-handling path도 의도적으로 실행해 보아야 한다.
Kernel의 fault injection framework는 특히 memory allocation 실패를 주입해 이 검증을 수행한다. 활성화하면 설정한 비율의 allocation을 실패시킬 수 있고, 실패를 특정 code range로 제한할 수도 있다. Fault injection 상태로 실행하면 상황이 나빠졌을 때 code가 실제로 어떻게 반응하는지 볼 수 있다. 사용법은 Documentation/fault-injection/fault-injection.rst에 설명되어 있다.
Sparse, Coccinelle과 cross compilation
306-331Sparse static analysis 도구는 user-space address와 kernel-space address의 혼동, big-endian 값과 little-endian 값의 혼합, bit flag 집합이 필요한 자리에 integer를 전달하는 문제 등을 찾아 경고한다. Distribution package에 없다면 https://sparse.wiki.kernel.org/index.php/Main_Page 에서 별도로 설치한 뒤 make 명령에 C=1을 추가하여 실행한다.
make C=1
Coccinelle은 다양한 잠재 coding 문제를 찾고 수정안까지 제안할 수 있다. Kernel tree의 scripts/coccinelle directory에는 여러 semantic patch가 포함되어 있으며 make coccicheck를 실행하면 이 patch들을 순회하면서 발견한 문제를 보고한다. 자세한 내용은 Documentation/dev-tools/coccinelle.rst를 참고한다.
make coccicheck
또 다른 종류의 portability 오류는 다른 architecture용으로 compile할 때 가장 잘 드러난다. S/390 system이나 Blackfin development board를 실제로 보유하지 않아도 cross compiler로 compilation 단계는 수행할 수 있다. x86 system용 cross compiler 모음은 https://www.kernel.org/pub/tools/crosstool/ 에 있다. 이를 설치하고 사용하는 데 시간을 들이면 나중에 곤란한 상황을 피할 수 있다.
Patch와 interface 문서화
334-389Kernel 개발에서 documentation은 규칙이라기보다 예외였던 시기가 많았다. 그래도 충분한 문서는 새 code의 merge를 쉽게 하고 다른 개발자와 사용자의 작업을 돕는다. 이제는 문서 추가가 사실상 필수인 경우도 많다.
모든 patch의 첫 번째 documentation은 함께 제출하는 changelog다. Log entry에는 해결하려는 문제, 해결 방법의 형태, patch 작업에 참여한 사람, 관련 performance 영향, patch를 이해하는 데 필요한 기타 사항을 적어야 한다. 특히 그 patch를 적용할 가치가 왜 있는지 설명해야 한다. 놀랄 만큼 많은 개발자가 이 정보를 빠뜨린다.
새 sysfs 또는 /proc file을 포함하여 새로운 user-space interface를 추가하는 code에는 user-space 개발자가 interface 사용법과 의미를 알 수 있는 문서가 따라야 한다. 형식과 필수 정보는 Documentation/ABI/README에 설명되어 있다.
Documentation/admin-guide/kernel-parameters.rst는 kernel boot-time parameter 전체를 설명한다. 새 parameter를 추가하는 patch는 해당 문서에도 항목을 추가해야 한다. 새 configuration option에는 option의 의미와 사용자가 언제 선택해야 하는지를 명확히 설명하는 help text가 반드시 있어야 한다.
여러 subsystem의 internal API는 특별한 형식의 comment로 문서화되며 kernel-doc script가 이를 추출해 다양한 형식으로 만들 수 있다. Kerneldoc comment를 사용하는 subsystem에서 작업한다면 기존 comment를 함께 유지하고, 외부에 제공되는 function에는 필요에 따라 새 comment를 추가한다. 아직 이런 문서가 없는 영역에서도 미래를 위해 kerneldoc comment를 추가해 두는 것은 유용하며 초보 kernel 개발자가 시작하기 좋은 작업이기도 하다. Comment 형식과 kerneldoc template 생성법은 Documentation/doc-guide/에 있다.
기존 kernel code를 많이 읽다 보면 comment가 없는 경우가 흔하다는 것을 알 수 있다. 하지만 새 code에 대한 기대 수준은 과거보다 높아졌으므로 comment 없는 code는 merge하기 더 어렵다. 그렇다고 장황한 comment를 원하는 것은 아니다. Code 자체가 읽을 수 있어야 하고, comment는 미묘하거나 code만으로 드러나지 않는 부분을 설명해야 한다.
- Memory barrier를 사용한 곳에는 그 barrier가 왜 필요한지 설명하는 한 줄을 둔다.
- Data structure의 locking rule은 적절한 위치에 설명한다.
- 핵심 data structure에는 전체 역할과 불변 조건을 포함한 충분한 문서를 둔다.
- 서로 떨어진 code 조각 사이의 명확하지 않은 dependency를 밝혀 둔다.
- 겉보기에는 불필요해 보여 잘못된 cleanup을 유도할 수 있는 구현에는 왜 그 방식이어야 하는지 적는다.
Internal API 변경의 책임
392-425Kernel이 user space에 제공하는 binary interface는 극도로 심각한 상황이 아니면 깨뜨릴 수 없다. 반면 kernel internal programming interface는 매우 유동적이며 필요할 때 변경할 수 있다. Kernel API를 피해 우회 구현을 하고 있거나 필요한 기능이 없어 특정 API를 사용하지 못한다면, API 자체를 바꿔야 한다는 신호일 수 있다. Kernel 개발자는 그런 변경을 할 권한이 있다.
물론 조건이 있다. API 변경에는 충분한 근거가 필요하다. Internal API를 바꾸는 patch에는 무엇을 어떻게 바꾸며 왜 필요한지 설명을 붙여야 한다. 또한 더 큰 patch 안에 묻어 두지 말고 API 변경을 독립된 patch로 분리해야 한다.
Internal API를 바꾼 개발자는 일반적으로 그 변경으로 인해 kernel tree 안에서 깨지는 모든 code를 고칠 책임도 진다. 널리 사용되는 function이라면 수백 또는 수천 곳을 수정해야 할 수 있고, 그중 다수는 다른 개발자가 진행 중인 작업과 충돌할 가능성이 있다. 매우 큰 작업이 될 수 있으므로 변경 근거가 확실한지 먼저 확인해야 한다. 범위가 넓은 API 변경에는 Coccinelle이 도움을 줄 수 있다.
호환되지 않는 API 변경을 할 때는 가능하면 아직 갱신되지 않은 code가 compiler에 의해 검출되도록 설계한다. 그러면 tree 안의 모든 사용처를 찾았는지 확인할 수 있고, out-of-tree code 개발자도 대응해야 할 변경이 생겼음을 알 수 있다. Kernel 개발자가 out-of-tree code 지원을 책임질 필요는 없지만, 필요 이상으로 그들의 작업을 어렵게 만들 이유도 없다.
결국 평가받는 것은 code다
4.Coding.rst:3-16Community를 고려한 설계 절차가 중요해도 kernel project의 결과는 실제 code로 증명된다. 다른 개발자는 code를 review하고 mainline에 merge할지 결정하므로 최종 성공은 code 품질에 달려 있다.