1 /* 2 * Copyright (c) 1990 Jan-Simon Pendry 3 * Copyright (c) 1990 Imperial College of Science, Technology & Medicine 4 * Copyright (c) 1990 The Regents of the University of California. 5 * All rights reserved. 6 * 7 * This code is derived from software contributed to Berkeley by 8 * Jan-Simon Pendry at Imperial College, London. 9 * 10 * %sccs.include.redist.c% 11 * 12 * @(#)nfs_ops.c 5.3 (Berkeley) 05/12/91 13 * 14 * $Id: nfs_ops.c,v 5.2.1.6 91/05/07 22:18:16 jsp Alpha $ 15 * 16 */ 17 18 #include "am.h" 19 20 #ifdef HAS_NFS 21 22 #define NFS 23 #define NFSCLIENT 24 #ifdef NFS_3 25 typedef nfs_fh fhandle_t; 26 #endif /* NFS_3 */ 27 #ifdef NFS_HDR 28 #include NFS_HDR 29 #endif /* NFS_HDR */ 30 #include <sys/mount.h> 31 #include "mount.h" 32 33 /* 34 * Network file system 35 */ 36 37 /* 38 * Convert from nfsstat to UN*X error code 39 */ 40 #define unx_error(e) ((int)(e)) 41 42 /* 43 * The NFS layer maintains a cache of file handles. 44 * This is *fundamental* to the implementation and 45 * also allows quick remounting when a filesystem 46 * is accessed soon after timing out. 47 * 48 * The NFS server layer knows to flush this cache 49 * when a server goes down so avoiding stale handles. 50 * 51 * Each cache entry keeps a hard reference to 52 * the corresponding server. This ensures that 53 * the server keepalive information is maintained. 54 * 55 * The copy of the sockaddr_in here is taken so 56 * that the port can be twiddled to talk to mountd 57 * instead of portmap or the NFS server as used 58 * elsewhere. 59 * The port# is flushed if a server goes down. 60 * The IP address is never flushed - we assume 61 * that the address of a mounted machine never 62 * changes. If it does, then you have other 63 * problems... 64 */ 65 typedef struct fh_cache fh_cache; 66 struct fh_cache { 67 qelem fh_q; /* List header */ 68 voidp fh_wchan; /* Wait channel */ 69 int fh_error; /* Valid data? */ 70 int fh_id; /* Unique id */ 71 int fh_cid; /* Callout id */ 72 struct fhstatus fh_handle; /* Handle on filesystem */ 73 struct sockaddr_in fh_sin; /* Address of mountd */ 74 fserver *fh_fs; /* Server holding filesystem */ 75 char *fh_path; /* Filesystem on host */ 76 }; 77 78 /* 79 * FH_TTL is the time a file handle will remain in the cache since 80 * last being used. If the file handle becomes invalid, then it 81 * will be flushed anyway. 82 */ 83 #define FH_TTL (5 * 60) /* five minutes */ 84 #define FH_TTL_ERROR (30) /* 30 seconds */ 85 86 static int fh_id = 0; 87 #define FHID_ALLOC() (++fh_id) 88 extern qelem fh_head; 89 qelem fh_head = { &fh_head, &fh_head }; 90 91 static int call_mountd P((fh_cache*, unsigned long, fwd_fun, voidp)); 92 93 AUTH *nfs_auth; 94 95 static fh_cache *find_nfs_fhandle_cache P((voidp idv, int done)); 96 static fh_cache *find_nfs_fhandle_cache(idv, done) 97 voidp idv; 98 int done; 99 { 100 fh_cache *fp, *fp2 = 0; 101 int id = (int) idv; 102 103 ITER(fp, fh_cache, &fh_head) { 104 if (fp->fh_id == id) { 105 fp2 = fp; 106 break; 107 } 108 } 109 110 #ifdef DEBUG 111 if (fp2) { 112 dlog("fh cache gives fp %#x, fs %s", fp2, fp2->fh_path); 113 } else { 114 dlog("fh cache search failed"); 115 } 116 #endif /* DEBUG */ 117 118 if (fp2 && !done) { 119 fp2->fh_error = ETIMEDOUT; 120 return 0; 121 } 122 123 return fp2; 124 } 125 126 /* 127 * Called when a filehandle appears 128 */ 129 static void got_nfs_fh P((voidp pkt, int len, struct sockaddr_in *sa, 130 struct sockaddr_in *ia, voidp idv, int done)); 131 static void got_nfs_fh(pkt, len, sa, ia, idv, done) 132 voidp pkt; 133 int len; 134 struct sockaddr_in *sa, *ia; 135 voidp idv; 136 int done; 137 { 138 fh_cache *fp = find_nfs_fhandle_cache(idv, done); 139 if (fp) { 140 fp->fh_error = pickup_rpc_reply(pkt, len, (voidp) &fp->fh_handle, xdr_fhstatus); 141 if (!fp->fh_error) { 142 #ifdef DEBUG 143 dlog("got filehandle for %s:%s", fp->fh_fs->fs_host, fp->fh_path); 144 #endif /* DEBUG */ 145 /* 146 * Wakeup anything sleeping on this filehandle 147 */ 148 if (fp->fh_wchan) { 149 #ifdef DEBUG 150 dlog("Calling wakeup on %#x", fp->fh_wchan); 151 #endif /* DEBUG */ 152 wakeup(fp->fh_wchan); 153 } 154 } 155 } 156 } 157 158 void flush_nfs_fhandle_cache P((fserver *fs)); 159 void flush_nfs_fhandle_cache(fs) 160 fserver *fs; 161 { 162 fh_cache *fp; 163 ITER(fp, fh_cache, &fh_head) { 164 if (fp->fh_fs == fs || fs == 0) { 165 fp->fh_sin.sin_port = (u_short) 0; 166 fp->fh_error = -1; 167 } 168 } 169 } 170 171 static void discard_fh P((fh_cache *fp)); 172 static void discard_fh(fp) 173 fh_cache *fp; 174 { 175 rem_que(&fp->fh_q); 176 #ifdef DEBUG 177 dlog("Discarding filehandle for %s:%s", fp->fh_fs->fs_host, fp->fh_path); 178 #endif /* DEBUG */ 179 free_srvr(fp->fh_fs); 180 free((voidp) fp->fh_path); 181 free((voidp) fp); 182 } 183 184 /* 185 * Determine the file handle for a node 186 */ 187 static int prime_nfs_fhandle_cache P((char *path, fserver *fs, struct fhstatus *fhbuf, voidp wchan)); 188 static int prime_nfs_fhandle_cache(path, fs, fhbuf, wchan) 189 char *path; 190 fserver *fs; 191 struct fhstatus *fhbuf; 192 voidp wchan; 193 { 194 fh_cache *fp, *fp_save = 0; 195 int error; 196 int reuse_id = FALSE; 197 198 #ifdef DEBUG 199 dlog("Searching cache for %s:%s", fs->fs_host, path); 200 #endif /* DEBUG */ 201 202 /* 203 * First search the cache 204 */ 205 ITER(fp, fh_cache, &fh_head) { 206 if (fs == fp->fh_fs && strcmp(path, fp->fh_path) == 0) { 207 switch (fp->fh_error) { 208 case 0: 209 error = fp->fh_error = unx_error(fp->fh_handle.fhs_status); 210 if (error == 0) { 211 if (fhbuf) 212 bcopy((voidp) &fp->fh_handle, (voidp) fhbuf, 213 sizeof(fp->fh_handle)); 214 if (fp->fh_cid) 215 untimeout(fp->fh_cid); 216 fp->fh_cid = timeout(FH_TTL, discard_fh, (voidp) fp); 217 } else if (error == EACCES) { 218 /* 219 * Now decode the file handle return code. 220 */ 221 plog(XLOG_INFO, "Filehandle denied for \"%s:%s\"", 222 fs->fs_host, path); 223 } else { 224 errno = error; /* XXX */ 225 plog(XLOG_INFO, "Filehandle error for \"%s:%s\": %m", 226 fs->fs_host, path); 227 } 228 229 /* 230 * The error was returned from the remote mount daemon. 231 * Policy: this error will be cached for now... 232 */ 233 return error; 234 235 case -1: 236 /* 237 * Still thinking about it, but we can re-use. 238 */ 239 fp_save = fp; 240 reuse_id = TRUE; 241 break; 242 243 default: 244 /* 245 * Return the error. 246 * Policy: make sure we recompute if required again 247 * in case this was caused by a network failure. 248 * This can thrash mountd's though... If you find 249 * your mountd going slowly then: 250 * 1. Add a fork() loop to main. 251 * 2. Remove the call to innetgr() and don't use 252 * netgroups, especially if you don't use YP. 253 */ 254 error = fp->fh_error; 255 fp->fh_error = -1; 256 return error; 257 } 258 break; 259 } 260 } 261 262 /* 263 * Not in cache 264 */ 265 if (fp_save) { 266 fp = fp_save; 267 /* 268 * Re-use existing slot 269 */ 270 untimeout(fp->fh_cid); 271 free_srvr(fp->fh_fs); 272 free(fp->fh_path); 273 } else { 274 fp = ALLOC(fh_cache); 275 bzero((voidp) fp, sizeof(*fp)); 276 ins_que(&fp->fh_q, &fh_head); 277 } 278 if (!reuse_id) 279 fp->fh_id = FHID_ALLOC(); 280 fp->fh_wchan = wchan; 281 fp->fh_error = -1; 282 fp->fh_cid = timeout(FH_TTL, discard_fh, (voidp) fp); 283 284 /* 285 * If the address has changed then don't try to re-use the 286 * port information 287 */ 288 if (fp->fh_sin.sin_addr.s_addr != fs->fs_ip->sin_addr.s_addr) { 289 fp->fh_sin = *fs->fs_ip; 290 fp->fh_sin.sin_port = 0; 291 } 292 fp->fh_fs = dup_srvr(fs); 293 fp->fh_path = strdup(path); 294 295 error = call_mountd(fp, MOUNTPROC_MNT, got_nfs_fh, wchan); 296 if (error) { 297 /* 298 * Local error - cache for a short period 299 * just to prevent thrashing. 300 */ 301 untimeout(fp->fh_cid); 302 fp->fh_cid = timeout(error < 0 ? 2 * ALLOWED_MOUNT_TIME : FH_TTL_ERROR, 303 discard_fh, (voidp) fp); 304 fp->fh_error = error; 305 } else { 306 error = fp->fh_error; 307 } 308 return error; 309 } 310 311 int make_nfs_auth P((void)) 312 { 313 #ifdef HAS_NFS_QUALIFIED_NAMES 314 /* 315 * From: Chris Metcalf <metcalf@masala.lcs.mit.edu> 316 * Use hostd, not just hostname. Note that uids 317 * and gids and the gidlist are type *int* and not the 318 * system uid_t and gid_t types. 319 */ 320 static int group_wheel = 0; 321 nfs_auth = authunix_create(hostd, 0, 0, 1, &group_wheel); 322 #else 323 nfs_auth = authunix_create_default(); 324 #endif 325 if (!nfs_auth) 326 return ENOBUFS; 327 return 0; 328 } 329 330 static int call_mountd P((fh_cache *fp, u_long proc, fwd_fun f, voidp wchan)); 331 static int call_mountd(fp, proc, f, wchan) 332 fh_cache *fp; 333 u_long proc; 334 fwd_fun f; 335 voidp wchan; 336 { 337 struct rpc_msg mnt_msg; 338 int len; 339 char iobuf[8192]; 340 int error; 341 342 if (!nfs_auth) { 343 error = make_nfs_auth(); 344 if (error) 345 return error; 346 } 347 348 if (fp->fh_sin.sin_port == 0) { 349 u_short port; 350 error = nfs_srvr_port(fp->fh_fs, &port, wchan); 351 if (error) 352 return error; 353 fp->fh_sin.sin_port = port; 354 } 355 356 rpc_msg_init(&mnt_msg, MOUNTPROG, MOUNTVERS, (unsigned long) 0); 357 len = make_rpc_packet(iobuf, sizeof(iobuf), proc, 358 &mnt_msg, (voidp) &fp->fh_path, xdr_nfspath, nfs_auth); 359 360 if (len > 0) { 361 error = fwd_packet(MK_RPC_XID(RPC_XID_MOUNTD, fp->fh_id), 362 (voidp) iobuf, len, &fp->fh_sin, &fp->fh_sin, (voidp) fp->fh_id, f); 363 } else { 364 error = -len; 365 } 366 return error; 367 } 368 369 /*-------------------------------------------------------------------------*/ 370 371 /* 372 * NFS needs the local filesystem, remote filesystem 373 * remote hostname. 374 * Local filesystem defaults to remote and vice-versa. 375 */ 376 static char *nfs_match(fo) 377 am_opts *fo; 378 { 379 char *xmtab; 380 if (fo->opt_fs && !fo->opt_rfs) 381 fo->opt_rfs = fo->opt_fs; 382 if (!fo->opt_rfs) { 383 plog(XLOG_USER, "nfs: no remote filesystem specified"); 384 return FALSE; 385 } 386 if (!fo->opt_rhost) { 387 plog(XLOG_USER, "nfs: no remote host specified"); 388 return FALSE; 389 } 390 /* 391 * Determine magic cookie to put in mtab 392 */ 393 xmtab = (char *) xmalloc(strlen(fo->opt_rhost) + strlen(fo->opt_rfs) + 2); 394 sprintf(xmtab, "%s:%s", fo->opt_rhost, fo->opt_rfs); 395 #ifdef DEBUG 396 dlog("NFS: mounting remote server \"%s\", remote fs \"%s\" on \"%s\"", 397 fo->opt_rhost, fo->opt_rfs, fo->opt_fs); 398 #endif /* DEBUG */ 399 400 return xmtab; 401 } 402 403 /* 404 * Initialise am structure for nfs 405 */ 406 static int nfs_init(mf) 407 mntfs *mf; 408 { 409 if (!mf->mf_private) { 410 int error; 411 struct fhstatus fhs; 412 413 char *colon = strchr(mf->mf_info, ':'); 414 if (colon == 0) 415 return ENOENT; 416 417 error = prime_nfs_fhandle_cache(colon+1, mf->mf_server, &fhs, (voidp) mf); 418 if (!error) { 419 mf->mf_private = (voidp) ALLOC(fhstatus); 420 mf->mf_prfree = (void (*)()) free; 421 bcopy((voidp) &fhs, mf->mf_private, sizeof(fhs)); 422 } 423 return error; 424 } 425 426 return 0; 427 } 428 429 int mount_nfs_fh(fhp, dir, fs_name, opts, mf) 430 struct fhstatus *fhp; 431 char *dir; 432 char *fs_name; 433 char *opts; 434 mntfs *mf; 435 { 436 struct nfs_args nfs_args; 437 struct mntent mnt; 438 int retry; 439 char *colon; 440 /*char *path;*/ 441 char host[MAXHOSTNAMELEN + MAXPATHLEN + 2]; 442 fserver *fs = mf->mf_server; 443 int flags; 444 #ifdef notdef 445 unsigned short port; 446 #endif /* notdef */ 447 448 MTYPE_TYPE type = MOUNT_TYPE_NFS; 449 450 bzero((voidp) &nfs_args, sizeof(nfs_args)); /* Paranoid */ 451 452 /* 453 * Extract host name to give to kernel 454 */ 455 if (!(colon = strchr(fs_name, ':'))) 456 return ENOENT; 457 #ifndef NFS_ARGS_NEEDS_PATH 458 *colon = '\0'; 459 #endif 460 strncpy(host, fs_name, sizeof(host)); 461 #ifndef NFS_ARGS_NEEDS_PATH 462 *colon = ':'; 463 #endif /* NFS_ARGS_NEEDS_PATH */ 464 /*path = colon + 1;*/ 465 466 bzero((voidp) &nfs_args, sizeof(nfs_args)); 467 468 mnt.mnt_dir = dir; 469 mnt.mnt_fsname = fs_name; 470 mnt.mnt_type = MTAB_TYPE_NFS; 471 mnt.mnt_opts = opts; 472 mnt.mnt_freq = 0; 473 mnt.mnt_passno = 0; 474 475 retry = hasmntval(&mnt, "retry"); 476 if (retry <= 0) 477 retry = 1; /* XXX */ 478 479 /*again:*/ 480 481 /* 482 * set mount args 483 */ 484 NFS_FH_DREF(nfs_args.fh, (NFS_FH_TYPE) fhp->fhstatus_u.fhs_fhandle); 485 486 #ifdef ULTRIX_HACK 487 nfs_args.optstr = mnt.mnt_opts; 488 #endif /* ULTRIX_HACK */ 489 490 nfs_args.hostname = host; 491 nfs_args.flags |= NFSMNT_HOSTNAME; 492 #ifdef HOSTNAMESZ 493 /* 494 * Most kernels have a name length restriction. 495 */ 496 if (strlen(host) >= HOSTNAMESZ) 497 strcpy(host + HOSTNAMESZ - 3, ".."); 498 #endif /* HOSTNAMESZ */ 499 500 if (nfs_args.rsize = hasmntval(&mnt, "rsize")) 501 nfs_args.flags |= NFSMNT_RSIZE; 502 503 if (nfs_args.wsize = hasmntval(&mnt, "wsize")) 504 nfs_args.flags |= NFSMNT_WSIZE; 505 506 if (nfs_args.timeo = hasmntval(&mnt, "timeo")) 507 nfs_args.flags |= NFSMNT_TIMEO; 508 509 if (nfs_args.retrans = hasmntval(&mnt, "retrans")) 510 nfs_args.flags |= NFSMNT_RETRANS; 511 512 #ifdef NFSMNT_BIODS 513 if (nfs_args.biods = hasmntval(&mnt, "biods")) 514 nfs_args.flags |= NFSMNT_BIODS; 515 516 #endif /* NFSMNT_BIODS */ 517 518 #ifdef notdef 519 /* 520 * This isn't supported by the ping algorithm yet. 521 * In any case, it is all done in nfs_init(). 522 */ 523 if (port = hasmntval(&mnt, "port")) 524 sin.sin_port = htons(port); 525 else 526 sin.sin_port = htons(NFS_PORT); /* XXX should use portmapper */ 527 #endif /* notdef */ 528 529 if (hasmntopt(&mnt, MNTOPT_SOFT) != NULL) 530 nfs_args.flags |= NFSMNT_SOFT; 531 532 #ifdef NFSMNT_SPONGY 533 if (hasmntopt(&mnt, "spongy") != NULL) { 534 nfs_args.flags |= NFSMNT_SPONGY; 535 if (nfs_args.flags & NFSMNT_SOFT) { 536 plog(XLOG_USER, "Mount opts soft and spongy are incompatible - soft ignored"); 537 nfs_args.flags &= ~NFSMNT_SOFT; 538 } 539 } 540 #endif /* MNTOPT_SPONGY */ 541 542 #ifdef MNTOPT_INTR 543 if (hasmntopt(&mnt, MNTOPT_INTR) != NULL) 544 nfs_args.flags |= NFSMNT_INT; 545 #endif /* MNTOPT_INTR */ 546 547 #ifdef MNTOPT_NODEVS 548 if (hasmntopt(&mnt, MNTOPT_NODEVS) != NULL) 549 nfs_args.flags |= NFSMNT_NODEVS; 550 #endif /* MNTOPT_NODEVS */ 551 552 #ifdef MNTOPT_COMPRESS 553 if (hasmntopt(&mnt, "compress") != NULL) 554 nfs_args.flags |= NFSMNT_COMPRESS; 555 #endif /* MNTOPT_COMPRESS */ 556 557 #ifdef MNTOPT_NOCONN 558 if (hasmntopt(&mnt, "noconn") != NULL) 559 nfs_args.flags |= NFSMNT_NOCONN; 560 #endif /* MNTOPT_NOCONN */ 561 562 #ifdef NFSMNT_PGTHRESH 563 if (nfs_args.pg_thresh = hasmntval(&mnt, "pgthresh")) 564 nfs_args.flags |= NFSMNT_PGTHRESH; 565 #endif /* NFSMNT_PGTHRESH */ 566 567 NFS_SA_DREF(nfs_args, fs->fs_ip); 568 569 flags = compute_mount_flags(&mnt); 570 571 #ifdef NFSMNT_NOCTO 572 if (hasmntopt(&mnt, "nocto") != NULL) 573 nfs_args.flags |= NFSMNT_NOCTO; 574 #endif /* NFSMNT_NOCTO */ 575 576 #ifdef HAS_TCP_NFS 577 if (hasmntopt(&mnt, "tcp") != NULL) 578 nfs_args.sotype = SOCK_STREAM; 579 #endif /* HAS_TCP_NFS */ 580 581 582 #ifdef ULTRIX_HACK 583 /* 584 * Ultrix passes the flags argument as part of the 585 * mount data structure, rather than using the 586 * flags argument to the system call. This is 587 * confusing... 588 */ 589 if (!(nfs_args.flags & NFSMNT_PGTHRESH)) { 590 nfs_args.pg_thresh = 64; /* 64k - XXX */ 591 nfs_args.flags |= NFSMNT_PGTHRESH; 592 } 593 nfs_args.gfs_flags = flags; 594 flags &= M_RDONLY; 595 if (flags & M_RDONLY) 596 nfs_args.flags |= NFSMNT_RONLY; 597 #endif /* ULTRIX_HACK */ 598 599 return mount_fs(&mnt, flags, (caddr_t) &nfs_args, retry, type); 600 } 601 602 static int mount_nfs(dir, fs_name, opts, mf) 603 char *dir; 604 char *fs_name; 605 char *opts; 606 mntfs *mf; 607 { 608 #ifdef notdef 609 int error; 610 struct fhstatus fhs; 611 char *colon; 612 613 if (!(colon = strchr(fs_name, ':'))) 614 return ENOENT; 615 616 #ifdef DEBUG 617 dlog("locating fhandle for %s", fs_name); 618 #endif /* DEBUG */ 619 error = prime_nfs_fhandle_cache(colon+1, mf->mf_server, &fhs, (voidp) 0); 620 621 if (error) 622 return error; 623 624 return mount_nfs_fh(&fhs, dir, fs_name, opts, mf); 625 #endif 626 if (!mf->mf_private) { 627 plog(XLOG_ERROR, "Missing filehandle for %s", fs_name); 628 return EINVAL; 629 } 630 631 return mount_nfs_fh((struct fhstatus *) mf->mf_private, dir, fs_name, opts, mf); 632 } 633 634 static int nfs_fmount(mf) 635 mntfs *mf; 636 { 637 int error; 638 639 error = mount_nfs(mf->mf_mount, mf->mf_info, mf->mf_mopts, mf); 640 641 #ifdef DEBUG 642 if (error) { 643 errno = error; 644 dlog("mount_nfs: %m"); 645 } 646 #endif /* DEBUG */ 647 return error; 648 } 649 650 static int nfs_fumount(mf) 651 mntfs *mf; 652 { 653 int error = UMOUNT_FS(mf->mf_mount); 654 if (error) 655 return error; 656 657 return 0; 658 } 659 660 static void nfs_umounted(mp) 661 am_node *mp; 662 { 663 #ifdef INFORM_MOUNTD 664 /* 665 * Don't bother to inform remote mountd 666 * that we are finished. Until a full 667 * track of filehandles is maintained 668 * the mountd unmount callback cannot 669 * be done correctly anyway... 670 */ 671 672 mntfs *mf = mp->am_mnt; 673 fserver *fs; 674 char *colon, *path; 675 676 if (mf->mf_error || mf->mf_refc > 1) 677 return; 678 679 fs = mf->mf_server; 680 681 /* 682 * Call the mount daemon on the server to 683 * announce that we are not using the fs any more. 684 * 685 * This is *wrong*. The mountd should be called 686 * when the fhandle is flushed from the cache, and 687 * a reference held to the cached entry while the 688 * fs is mounted... 689 */ 690 colon = path = strchr(mf->mf_info, ':'); 691 if (fs && colon) { 692 fh_cache f; 693 #ifdef DEBUG 694 dlog("calling mountd for %s", mf->mf_info); 695 #endif /* DEBUG */ 696 *path++ = '\0'; 697 f.fh_path = path; 698 f.fh_sin = *fs->fs_ip; 699 f.fh_sin.sin_port = (u_short) 0; 700 f.fh_fs = fs; 701 f.fh_id = 0; 702 f.fh_error = 0; 703 (void) prime_nfs_fhandle_cache(colon+1, mf->mf_server, (struct fhstatus *) 0, (voidp) mf); 704 (void) call_mountd(&f, MOUNTPROC_UMNT, (fwd_fun) 0, (voidp) 0); 705 *colon = ':'; 706 } 707 #endif /* INFORM_MOUNTD */ 708 } 709 710 /* 711 * Network file system 712 */ 713 am_ops nfs_ops = { 714 "nfs", 715 nfs_match, 716 nfs_init, 717 auto_fmount, 718 nfs_fmount, 719 auto_fumount, 720 nfs_fumount, 721 efs_lookuppn, 722 efs_readdir, 723 0, /* nfs_readlink */ 724 0, /* nfs_mounted */ 725 nfs_umounted, 726 find_nfs_srvr, 727 FS_MKMNT|FS_BACKGROUND|FS_AMQINFO 728 }; 729 730 #endif /* HAS_NFS */ 731