Normally, you imagine that at every address, there's some memory associated with it. In a primitive CPU, you might imagine a byte of RAM associated with each address generated by a program.
Think about what happens when you try to load or store a byte from memory.
If an I/O device can follow the same protocol, it can respond the same way, and as far as the CPU is concerned, it doesn't matter that the I/O device is doing the communication.
The only issue is to have memory respond to some of the (physical) addresses and the I/O device respond to other addresses. Thus, the set of addresses handed by memory and I/O devices should be disjoint.
That byte might tell you if the printer is printing, or is out of paper, or out of toner, or what kind of paper is being used, etc.
You may also want to direct the printer to print. You can send it what characters to print, or what font, etc. This information can be written to a "memory" address. However, instead of having memory at that address, you have an I/O device. The CPU acts as if its writing to memory, and the I/O device reads this information.
However, if you have a byte of at some memory address that is really the status of a printer, then the printer is updating that byte. If this corresponds to a variable in your C program, then that variable may be changing.
A problem occurs with caching. Suppose that value is placed in the cache. Now we have a copy, but the I/O device is updating what would be considered "memory", not the cache directly.
One way to have the compiler avoid caching is to use the volatile keyword. A variable that's volatile is expected to change outside the program's control. Thus, it should not be kept in cache, because the cache may not see the updates.
This pin would be output, say, 0, if the address on the address bus was a memory address, and output 1 if the address was an I/O address.
This output pin would be used in the chip enable of memory and I/O devices. When the value is 0, then memory could have its chip enable active. When the value is 1, the I/O device could have its chip enable active.
Basically, the mechanism for communicating with I/O devices and memory is the same, as far as the signals sent to the devices from the CPU.
The main difference is just in the instruction set, where some ISAs support I/O instructions while others just use memory-mapped I/O, and reserve a section of memory for I/O devices.
Much of it is invalid, and if an invalid physical address is generated, it may send an interrupt back to the CPU to indicate this has happened.
Thus, communicating to an I/O device can be the same as reading and writing to memory addresses devoted to the I/O device. The I/O device merely has to use the same protocol to communicate with the CPU as memory uses.
Some ISAs use special I/O instructions. However, the signals generated by the CPU for I/O instructions and for memory-mapped I/O are nearly the same. Usually, there's just one special I/O pin that lets you know whether its a memory address or an I/O address. Other than that, they behave nearly identically.