James Kraus
James Kraus

Reputation: 3478

Why do I get Sys_error("Bad file descriptor") when I call `close_out` on a output channel?

Reading this article on socket programming with ocaml, I ran into this example server code:

# let establish_server server_fun sockaddr =
   let domain = domain_of sockaddr in
   let sock = Unix.socket domain Unix.SOCK_STREAM 0 
   in Unix.bind sock sockaddr ;
      Unix.listen sock 3;
      while true do
        let (s, caller) = Unix.accept sock 
        in match Unix.fork() with
               0 -> if Unix.fork() <> 0 then exit 0 ; 
                    let inchan = Unix.in_channel_of_descr s 
                    and outchan = Unix.out_channel_of_descr s 
                    in server_fun inchan outchan ;
                       close_in inchan ;
                       close_out outchan ;
                       exit 0
             | id -> Unix.close s; ignore(Unix.waitpid [] id)
      done ;;
val establish_server :
  (in_channel -> out_channel -> 'a) -> Unix.sockaddr -> unit = <fun>

Tinkering with the code locally, I was surprised that I received Fatal error: exception Sys_error("Bad file descriptor") each time I connected to the socket. Here's my tinkering code:

let my_name = Unix.gethostname();;
let my_entry_byname = Unix.gethostbyname my_name ;;
let my_addr = my_entry_byname.h_addr_list.(0);;

let socket_desc = Unix.socket Unix.PF_INET Unix.SOCK_STREAM 0;;

let hello_server sockaddr =
  let domain = Unix.domain_of_sockaddr sockaddr in
  let socket_desc = Unix.socket domain Unix.SOCK_STREAM 0
  in Unix.bind socket_desc sockaddr;
     Unix.listen socket_desc 3;
     let addr_in =
       match Unix.getsockname socket_desc with
         Unix.ADDR_INET (a, _) -> a
       | _ -> failwith "not INET";
     in
     print_string (String.concat "" ["Listening on "; Unix.string_of_inet_addr addr_in]);
     flush stdout;
     while true do
       let (s, _caller) = Unix.accept socket_desc
       in match Unix.fork() with
            0 -> if Unix.fork() <> 0 then exit 0;
                 print_string "Got a connection!";
                 flush stdout;
                 let inchan = Unix.in_channel_of_descr s
                 and outchan = Unix.out_channel_of_descr s
                 in output_string outchan "Hello world!";
                    flush outchan;
                    close_in inchan;
                    close_out outchan;
                    exit 0;
            | id -> Unix.close s; ignore(Unix.waitpid [] id)
     done;;


let start_server () =
  let addr = Unix.ADDR_INET(my_addr, 12345)
  in hello_server addr;;

let () = start_server()

It seems the error is probably due to the close_out outchan call in the child process. I can't quite figure out why I'm getting the error though. What's wrong with calling close_out on that channel?

Fwiw, I'm connecting to the channel with telnet my.local.ip.addr 12345

Edit: Also: why do we call Unix.close s in the parent process and not in the child?

Upvotes: 0

Views: 931

Answers (1)

Jeffrey Scofield
Jeffrey Scofield

Reputation: 66823

You are closing the socket twice.

let (s, _caller) = Unix.accept socket_desc

Now you have Unix file descriptor s of your socket.

let inchan = Unix.in_channel_of_descr s
and outchan = Unix.out_channel_of_descr s

Now you have OCaml input and output channels with the socket as their underlying streams.

Unix.close s;

Now you have closed the read/write endpoints of the Unix socket.

close_out outchan;

Now you attempt to close the socket a second time. Since the underlying stream is already closed, this is an error.

The way to look at it (IMHO) is that after you do this:

let inchan = Unix.in_channel_of_descr s
and outchan = Unix.out_channel_of_descr s

You are signing a contract not to use the underlying Unix socket any longer. From this point on you should be dealing only with the OCaml channels.

If you remove Unix.close s, things should work (or fail at the next problem :-)

Update

I ran your given code from the tutorial and it also gets the bad file descriptor exception.

Possibly this is a flawed tutorial.

It appears that both close_in and close_out are going to close the socket completely (and so not leave it in a so-called half open state). So I would just call close_out.

It might be best to do your socket I/O entirely through the Unix interface. It seems a bit fragile to have two OCaml channels sharing the same file descriptor.

Update 2

You can use Unix.dup to get a second file descriptor to use for one of the two OCaml channels. The resulting code feels much less fragile to me:

match Unix.fork() with
| 0 ->
    (* Child process *)
    if Unix.fork() <> 0 then exit 0; (* Daemonize *)
    print_string "Got a connection!";
    flush stdout;
    let s' = Unix.dup s in
    let inchan = Unix.in_channel_of_descr s
    and outchan = Unix.out_channel_of_descr s' in
    output_string outchan "Hello world!";
    flush outchan;
    close_in inchan;
    close_out outchan;
    exit 0
| id ->
    (* Parent process *)
    Unix.close s;
    ignore(Unix.waitpid [] id)

I tested this code and it worked with no bad file descriptor exceptions.

Upvotes: 1

Related Questions