A Chaos pipe, or simply pipe, is a device used by two processes to communicate. The communication flowing through pipes is splitted into chunks called messages and the handles are used by the kernel calls to refer to a pipe.
An important thing to note is that the kernel does not care about the content of the messages that flow through the pipe (unless the pipe is a special kernel pipe which is connected directly to the kernel), it only ensures that the messages reach their destination exactly once, in the proper order and in a good condition.
Each Chaos pipe has two ends and two unidirectional paths for data. Data that enters the pipe through one end always exit the pipe through the other end. The data cannot "turn around" in the pipe and leak out through the end through which it entered (therefore these pipes are much more like highway tunnels with have separate roads for both driving directions through the tunnel; the name "pipe" is used for them for historical reasons). The data in a pipe also cannot pass each other; they exit the pipe in exactly the same order they entered it. This concept is very similar to Unix sockets, but Chaos pipes, unlike Unix sockets, have no name.
Communication flows through the pipe in chunks called messages. The KCALL__SEND kernel call is used to "stuff" a message to one of the ends of the pipe. The messages then flow through the pipe and stack at the other end. The KCALL__RECV call is then used to pull the messages out of the end of the pipe (one at a time). The messages are pulled one at a time and they never "break" inside a pipe or "merge" one into another (message boundaries are preserved).
When the sending thread cannot stuff its message into the pipe (because the buffer in the pipe is full) it is blocked until the other process pulls enough messages out of the pipe to allow the stuffer to complete its stuffing. Therefore a pipe never overflows. Similarly when a thread tries to pull a message from an end of a pipe where none is waiting, it is blocked until the message arrives. These blocks are infinite (thread is blocked until the operation can be completed) and no direct means to set a timeout are provided (however when another thread closes a handle, all threads waiting on it are immediately unblocked with the keCLOSED error code so this can be used as a sort of mean to unblock a thread jammed on a handle).
The message size is limited and the limit can vary but it is at least 1.5 KiB (1536 bytes) and at most 64 KiB. This message size limit is constant when the system is running and no dynamical changes to it are allowed. Changing the message size limit requires kernel rebuild, reinstallation and reboot and when it is being lessened, some of the software will also need a rebuild and reinstall (the message size limit is available as a compile-time constant to system software).
The pipe buffers size is chosen so so that each pipe can hold at least one message of the maximal size in its buffer before the sending threads need to wait for more space. This means that when the pipe is empty and a thread tries to pass the message of the particular maximal size valid during its runtime, the operation will complete immediately (it will not be blocked waiting for more free space in the pipe buffers).
To refer to the two directions of a pipe in the KCALL__SEND and KCALL__RECV kernel calls, handles are used. A handle is just a nonnegative integer. ??? Question: Allow multiple handles to refer to the same direction of a pipe? ???
There exists a standard set of handles that refer to pipes connecting to filesystem server and the like so the processes can use the rest of the system without hunting for suitable handles first. These handles are the same during the system run but can be made volatile across some reboots to improve security (this is done by deploying the pseudosecret protocols security provision) so they are never used in the source code directly. Standard handle names are used to refer to these handles in the source code instead of the handles themselves. The protocol description header files contain declarations of constants that map these standard handle names to their respective handles.
Kernel pipes are special pipes used to access functionality placed into the kernel itself. From the process's perspective there is nothing special about them and even there is no way to determine whether a particular handle refers to a kernel pipe or not.