(1) Naming schema is more complex than windows
I use udev rules to create symlinks to my microcontrollers, so they have stable names.
It only takes a single line in a plain text file, saved (with superuser privileges) to the correct location.
(2) Dealing with serial ports that appear and disappear.
In the case of a udev symlink, you still need to reopen the same device (symlink). This cannot really be avoided. Attempting any I/O to the disappeared device will result in a clear error, though.
(3) Software that auto-opens serial ports.
There are two details here.
One is to use an udev rule that tells services like ModemManager to skip the device.
The other is to use the standard
TTY ioctl,
TIOCEXCL, in the program using the serial port. This stops all but superuser from opening the port while the program has it open; they fail with EBUSY (Device or resource busy) error.
(It is just a single call,
ioctl(fd,TIOCEXCL); or
ioctl(fileno(stream),TIOCEXCL);, after the port has been opened. This will stop all except system services like ModemManager that do run with superuser privileges; those are controlled via the udev rule.)
"Cura" is particularly bad. I accidentally opened it one day (for some reason it had become the default file opener for .stl). My existing print job got trashed. I think Cura also decided to dance with my board debugging session one day. I've uninstalled it now so I can't make this mistake again 
You need to use a program that does the TIOCEXCL ioctl to communicate with your boards or printers.
The main downside of the way Linux routes serial and USB serial ports through the tty (
termios) layer is that it adds overhead, limiting the throughput somewhat. For example, on Teensy 4.0 and 4.1 (high-speed USB, 480 Mbit/s), reading USB serial data from Teensy is limited to 25-30 megabytes per second (200-230 Mbits/s), depending on the single-core CPU speed and USB hardware implementation.
I use C and Python to interface to my microcontroller projects, using POSIX termios (tty) interfaces (
man 3 termios for C, or
python termios built-in library). Python isn't suitable for massive amounts of I/O, say more than a couple of megabytes per second, because of the inherent complexity in its I/O implementation, but it is definitely sufficient for full-speed USB serial (12 Mbit/s).
By default, the termios layer does all sorts of transformations ("canonical mode") and can even generate signals (INT, QUIT, TSTP, TTOU; and INFO on BSDs but not in Linux), and these need to be turned off ("raw mode"). It's just a dozen lines of code or so, fortunately. The annoying bit is that to be "nice", one needs to store the original settings, and restore them before exiting; while not required per se at all, it is kinda-sorta expected.
One thing I'm adamant on in my own microcontroller implementations –– I use several different microcontrollers, all with native USB implementations, from 8-bit AVR (ATmega32u4) to i.MX RT1062 –– is that the communications are truly asynchronous, even for a query-response interface. That means that the connection is essentially two separate unidirectional "pipes" that work independently, not in lock-step. One can supply more than one query in a row, without waiting for the response in between. This is also why e.g. G-code supports line numbers (
Nlinenum). In my own query-response interfaces, I similarly allow an unique identifier for each query or command, repeated in the response; and explicitly allow (in my host program) responses to occur in a different order. This way, operations that take longer, say moving a servo, do not delay faster operations, say changing the state of a relay.
A particularly useful approach I've found is to use Qt5 or Gtk+ (via PySide2 or PyQt5, or Python GI) for a nice graphical UI, with a separate thread (or thread pair!) communicating with each microcontroller,
python Queues between the threads passing the commands and responses and data around, and an idle handler converting the messages in the response Queues to UI events.
This way, if you have say a button for a relay, clicking on the button does not immediately change its state, only sends the Queue message instructing the MCU to change the relay state. When the MCU has done so and responds, the response message is what changes the button state. (I personally like to use my own "button" derivative class, though, with an additional mid-way state (immediately when pushed), showing the user the button is in the process of changing, but not done changing yet. You could use a push-button and a separate indicator, for example, with the button popping back up when the response is received and the indicator updated.)
To make such an application portable, the low-level serial interface (basically dual pipes to the microcontroller) has to be abstracted out. I really dislike libusb (I don't trust it, it just ignores most errors internally) and other serial abstractions I've seen. The termios approach works for all but Windows, although how one discovers the suitable microcontrollers/serial devices does vary a bit between Linux/Android and BSDs. Annoying, but such is life.