DataReader is a forward-only iterator over a set of results. It's usually the most efficient way to deal with records when you don't need random access (in other words you can't go backwards). It is "scalable" to any number of records, at least in terms of memory pressure, since it only loads one record at a time. One typical way to get a DataReader is by using the ExecuteReader method of a DbCommand.
DataSet represents a set of DataTable objects. More often than not, it will just contain one table, but if you do a query with multiple SELECT statements, the DataSet will contain a table for each. Because this is an in-memory representation, you have to be careful about how much data you pull into a DataSet. You can "Fill" a DataSet using the Fill method of a DataAdapter.
DataAdapter is a kind of "pipe" that funnels data from a DB engine into a DataSet. That's why you'll have one DataAdapter implementation for each DB provider type. This dataadapter object is used to read the data from database and bind that data to dataset.One DataSet, many providers.Dataadapter is a disconnected oriented architecture.
*DataTable represents a single table in the database. It has rows and columns. There is no much difference between dataset and datatable, dataset is simply the collection of datatables.