How to read/write to Linux pseudoterminals using separate processes but without forking?
Asked Answered
L

1

7

I'd like to write a program that emulates a device on a serial port. I'm trying to use pseudoterminals to accomplish this. I want one distinct process to control the master. This process acts as the serial device emulator. I want another process (kermit for example) to be able to communicate with the master using the slave terminal. Because of the distinct process requirement, I am not using any forks. Almost every pseudoterminal example on the Internet shows the use of fork() for master/slave.

I have it working in one direction. That is, I can have the slave process write data to the slave pseudoterminal, and the master will read it off the master pseudoterminal just fine.

The problem is in the other direction. I can't get the master to write data and the slave to read data.

I'll show both the nonworking bidirectional code and the working unidirectional code.

Nonworking bidirectional master:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char* argv[])
{
  // get the master fd
  int masterfd = open("/dev/ptmx", O_RDWR | O_NOCTTY);
  if(masterfd < 0)
  {
    perror("getpt");
    exit(1);
  }

  // grant access to the slave
  if(grantpt(masterfd) < 0)
  {
    perror("grantpt");
    exit(1);
  }

  // unlock the slave
  if(unlockpt(masterfd) < 0)
  {
    perror("unlockpt");
    exit(1);
  }

  // get the path to the slave
  char slavepath[64];
  if(ptsname_r(masterfd, slavepath, sizeof(slavepath)) < 0)
  {
    perror("ptsname_r");
    exit(1);
  }

  printf("Using %s\n", slavepath);

  char bufout = 'D';
  char bufin;

  int c;
  while(1)
  {
    printf("reading\n");
    c = read(masterfd, &bufin, 1);
    printf("read %i bytes: %c\n", c, bufin);
    if(c == -1) break;

    if(bufout == 'D') bufout = 'E';
    else if(bufout == 'E') bufout = 'D';
    printf("writing %c\n", bufout);
    c = write(masterfd, &bufout, 1);
    printf("wrote %i bytes\n", c);
    if(c == -1) break;

    sleep(1);
  }

  close(masterfd);

  return 0;
}

Nonworking bidrectional slave:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char* argv[])
{
  int fd = open("/dev/pts/15", O_RDWR | O_NOCTTY);
  if(fd < 0)
  {
    perror("open");
    exit(1);
  }

  char bufout = 'A';
  char bufin;

  int c;
  while(1)
  {
    if(bufout == 'A') bufout = 'B';
    else if(bufout == 'B') bufout = 'A';
    printf("writing %c\n", bufout);
    c = write(fd, &bufout, 1);
    if(c == -1) break;

    printf("reading\n");
    c = read(fd, &bufin, 1);
    printf("read %i bytes: %c\n", c, bufin);
    if(c == -1) break;

    sleep(1);
  }

  close(fd);
}

Output of nonworking master: (Notice the first character received came from the slave, then the rest are characters written by the master. In other words, the master is reading off the same characters it wrote to the master pts and its ignoring what the slave is writing, except for the first character.)

Using /dev/pts/15
reading
read 1 bytes: B     [<--- FROM THE SLAVE]
writing E
wrote 1 bytes
reading
read 1 bytes: E     [<--- REST FROM THE MASTER]
writing D
wrote 1 bytes
reading
read 1 bytes: D
writing E
wrote 1 bytes
reading
read 1 bytes: E
writing D
wrote 1 bytes
reading
read 1 bytes: D
^C

Output of nonworking slave: (Never receives what the master is writing.)

writing B
reading
^C

Working unidrectional master:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char* argv[])
{
  // get the master fd
  int masterfd = open("/dev/ptmx", O_RDWR | O_NOCTTY);
  if(masterfd < 0)
  {
    perror("getpt");
    exit(1);
  }

  // grant access to the slave
  if(grantpt(masterfd) < 0)
  {
    perror("grantpt");
    exit(1);
  }

  // unlock the slave
  if(unlockpt(masterfd) < 0)
  {
    perror("unlockpt");
    exit(1);
  }

  // get the path to the slave
  char slavepath[64];
  if(ptsname_r(masterfd, slavepath, sizeof(slavepath)) < 0)
  {
    perror("ptsname_r");
    exit(1);
  }

  printf("Using %s\n", slavepath);

  char bufout = 'D';
  char bufin;

  int c;
  while(1)
  {
    printf("reading\n");
    c = read(masterfd, &bufin, 1);
    printf("read %i bytes: %c\n", c, bufin);
    if(c == -1) break;

    sleep(1);
  }

  close(masterfd);

  return 0;
}

Working unidrectional slave:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char* argv[])
{
  int fd = open("/dev/pts/15", O_RDWR | O_NOCTTY);
  if(fd < 0)
  {
    perror("open");
    exit(1);
  }

  char bufout = 'A';
  char bufin;

  int c;
  while(1)
  {
    if(bufout == 'A') bufout = 'B';
    else if(bufout == 'B') bufout = 'A';
    printf("writing %c\n", bufout);
    c = write(fd, &bufout, 1);
    if(c == -1) break;

    sleep(1);
  }

  close(fd);
}

Output of working master: (Reads what the slave writes successfully.)

Using /dev/pts/15
reading
read 1 bytes: B
reading
read 1 bytes: A
reading
read 1 bytes: B
reading
read 1 bytes: A
reading
read 1 bytes: B
^C

Output of working slave:

writing B
writing A
writing B
writing A
writing B
^C
Lithographer answered 4/5, 2014 at 17:46 Comment(2)
Might GNU screen work? I'm not positive about this. When I tried something like this I ended up having a lot of xterms running and connected to the terminals with screen -x and entered commands with xlib. But there HAS to be a better way.Baluchistan
Do you know about multiplexing syscalls like poll(2)? Maybe they could be useful.Stoup
N
8

I noticed that your first example works if we use a terminal emulator like screenor picocom instead of your slave program.

I think that the difference comes from various line "cooking" settings. Here is the default state of the pts when it is created:

$ stty -F /dev/pts/2 
speed 38400 baud; line = 0;
-brkint -imaxbel

Now here what it becomes, after it has been opened with screen:

$ stty -F /dev/pts/2 
speed 9600 baud; line = 0;
kill = ^H; min = 100; time = 2;
-icrnl -imaxbel
-opost -onlcr
-isig -icanon -echo

Then I tried setting the pts to raw mode in the slave program before the read/write operations. Raw mode is a combination of line settings that disables as much cooking as possible.

struct termios ts;

if(tcgetattr(fd, &ts))
{
  perror("tcgetattr");
  exit(1);
}

cfmakeraw(&ts);
tcsetattr (fd, TCSANOW, &ts);

Looks like it makes it work.

Master:

$ ./pts_master 
Using /dev/pts/2
reading
read 1 bytes: B
writing E
wrote 1 bytes
reading
read 1 bytes: A
writing D
wrote 1 bytes
reading
read 1 bytes: B
writing E
wrote 1 bytes
...

Slave:

$ ./pts_slave 
writing B
reading
read 1 bytes: E
writing A
reading
read 1 bytes: D
writing B
...
Nestling answered 4/5, 2014 at 20:49 Comment(1)
Brilliant. I admit I have no idea what "line cooking" means, but that will give me some more reading to do just to formulate a higher understanding. Your solution works.Lithographer

© 2022 - 2024 — McMap. All rights reserved.