C, 233
=====
This of course uses much more code, but I think the spirit of the problem exonerates this bare-bones approach; for all we know, the internal rationalize() functions of modern languages have lots of internal loops.

Note that this doesn't work for an input of "0." because that is not a standard way to write a float, so when it re-writes the float to string, the result will never be a "0.".

The specs want a function that returns values instead of just printing to screen, hence the argument-passing.

Code (ungolfed):

    void r(char* x, int* a, int* b) {
	    int i = -1;
	    char z[32];
	    double v =atof(x);
	    while(1) {
		    i++;
		    double y = ((double)i)/((double)(*b));
		    double w;
		    sprintf(z, "%.*f", strlen(strchr(x,'.'))-1, y);
		    if(strcmp(x, z)==0) {
			    *a = i;
			    return;
		    }
		    w = atof(z);
		    if(w > v) {
			    (*b)++;
			    r(x, a, b);
			    return;
		    }
	    }
    }

Usage:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    int main(int argc, char* argv[]) {
	    int num;
	    int denom = 1; // start with a denominator of 1
	    r(argv[1], &num, &denom);
	    printf("%d/%d\n", num, denom);
	    return 0;
    }

Golfed code:

    typedef double D;
    void r(char*x,int*a,int*b){int i=-1;char z[32];D v=atof(x);while(1){i++;D y=((D)i)/((D)(*b));D w;sprintf(z,"%.*f",strlen(strchr(x,'.'))-1,y);if(!strcmp(x,z)){*a=i;return;}w=atof(z);if(w>v){(*b)++;r(x,a,b);return;}}}