Two ways. The first is IO mapped IO, which is what the x86 supports with the IN and OUT instructions. All this really is is a MOV to a different "address" space (16 bits of addressing). The second is memory mapped IO, in which hardware is mapped to the addressing space of the CPU (Motorola 68K uses this format). A CPU that allows IO mapped IO can also do memory mapped IO (it's not precluded).
PCI did for discovery, and could use it for device access (but often also memory-mapped). More info from the osdev wiki, which btw is a great place if you want to know about low-level initialization, the boot process etc.