On the other hand, signals were just a poor design. They are much nicer now on Linux with signalfd(2), which gives you a file descriptor to read them on, a much nicer interface than all the old syscalls for signals.
Well, I'm not sure signalfd(2) was feasible back when signals were added. Was it?
Today it's usual to structure your main() as some kind of select()-like loop; even if you don't need one for your main workflow, at the worst you can spawn an I/O thread and put it there. But back then, you didn't have threads. Lots of programs didn't read your input but still wanted to catch signals, e.g. to exit cleanly on kill -15. Many others read input, but without an event loop - they would just try to readchar() periodically when they had nothing else to do.
An occasional readchar could be replaced with an occasional read to a signalfd. The problem is blocking calls, you need to not block for too long in case a signal arrives, which is more messy it is true.
Blocking calls are not the problem - you simply block on select({stdin, signalfd}) instead of blocking on read(stdin). The problem rather is slow IO calls that nonetheless never block - the canonical example being a read() from a disk file. In this case (eg. /bin/tar) you would have to poll your signalfd after every read() - but this breaks the file abstraction for programs like /bin/cat that don't care whether the file descriptor they're reading from is the blockable type or not, so they'd have to do both.
This is now starting to look considerably inelegant, and we haven't even talked about implementing the equivalent of synchronous signals provoked by a program's own action (SIGILL, SIGFPE, SIGSEGV, SIGBUS...).