RFR: 8288140: Avoid redundant Hashtable.get call in Signal.handle [v2]

Andrey Turbanov aturbanov at openjdk.java.net
Tue Jun 14 19:26:54 UTC 2022


On Sat, 11 Jun 2022 08:46:18 GMT, Peter Levart <plevart at openjdk.org> wrote:

>> Hi,
>> I think the synchronized block was redundant already in original code. Since the entire handle method is `static synchronized` and it is the only method that modifies the `handlers` and `signals` maps.
>> But even with so much redundant synchronization, the Signal class is not without races. There are multiple possible races in `dispatch(int number)` method which is called from native code to dispatch a signal:
>> - race no. 1: dispatch method (not synchronized) performs 2 independent lookups into `signals` and `handlers` maps respectively, assuming their inter-referenced state is consistent. But `handle` method may be concurrently modifying them, so `dispatch` may see updated state of `signals` map while seeing old state of `handlers` map or vice versa.
>> - race  no. 2: besides `signals` and `handlers` there is a 3rd map in native code, updated with `handle0` native method. Native code dispatches signals according to that native map, forwarding them to either native handlers or to `dispatch` Java method. But `handle` method may be modifying that native map concurrently, so dispatch may be called as a consequence of updated native map while seeing old states of `signals` and `handlers` maps.
>> 
>> I'm sure I might have missed some additional races.
>> 
>> How to fix this? Is it even necessary? I think that this internal API is not used frequently so this was hardly an issue. But anyway, it would be a challenge. While the two java maps: `handlers` and `signals` could be replaced with a single map, there is the 3rd map in native code. We would not want to make `dispatch` method synchronized, so with careful ordering of modifications it is perhaps possible to account for races and make them harmless...
>> WDYT?
>
> Well, studying this further, I think some races are already accounted for and are harmless except one. The `handle` method 1st attempts to register handler in native code via call to `handle0` and then updates the `signals` map.. As soon as native code registers the handler, `dispatch` may be called and the lookup into `signals` map may return null which would cause NPE in the following lookup into `handlers` map. So there are two possibilities to fix this:
> - guard for null result from `singnals` lookup, or
> - swap the order of modifying the `signals` map and a call to `handle0` native method. You could even update the `signals` map with `.putIfAbsent()` to avoid multiple reassignment with otherwise equal instances. This may unnecessarily establish a mapping for a Signal the can later not be registered, so you can remove it from the `signals` map in that case to clean-up.
> The possible case when the lookup into `handlers` map returns null Handler is already taken into account, but there is a possible case where a NativeHandler is replaced with a java Handler implementation and `dispatch` is therefore already called, but `handlers` map lookup still returns a NativeHandler. In that case the NativeHandler::handle method will throw exception. To avoid that, NativeHandler::handle could be an empty method instead.

I tried to reproduce this possible problem via jcstress, but never caught this race.

package org.openjdk.jcstress.samples.jmm.basic;

import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.ZZZZZZZZ_Result;
import sun.misc.Signal;
import sun.misc.SignalHandler;

import java.io.IOException;

import static org.openjdk.jcstress.annotations.Expect.*;

@JCStressTest
@Outcome(id = "false, false, false, false, false, false, false, false", expect = ACCEPTABLE, desc = "All results are ok")
@Outcome(id = ".*", expect = ACCEPTABLE_INTERESTING, desc = "All results are ok")
@State
public class BasicJMM_11_Signal {

    /*
        How to run this test:
            $ /home/turbanoff/.jdks/liberica-18.0.1.1/bin/java -jar jcstress-samples/target/jcstress.jar -t BasicJMM_11_Signal -v
            $ /home/turbanoff/Projects/official_jdk/build/linux-x86_64-server-fastdebug/jdk/bin/java -jar jcstress-samples/target/jcstress.jar -t BasicJMM_11_Signal -v
            $ /home/turbanoff/Projects/jdk2/build/linux-x86_64-server-release/jdk/bin/java -jar jcstress-samples/target/jcstress.jar -t BasicJMM_11_Signal -v
     */

    private static final Signal[] signals = new Signal[]{
            new Signal("URG"),
//            new Signal("USR2"),
//            new Signal("ALRM"),
//            new Signal("USR1"),
            new Signal("CHLD"),
            new Signal("XFSZ"),
            new Signal("CONT"),
//            new Signal("POLL"),
            new Signal("WINCH"),
//            new Signal("IO"),
            new Signal("PIPE"),
//            new Signal("HUP"),
//            new Signal("POLL"),
//            new Signal("PROF"),
//            new Signal("INT"),
//            new Signal("STKFLT"),
            new Signal("TSTP"),
//            new Signal("SYS"),
//            new Signal("TERM"),
//            new Signal("TRAP"),
//            new Signal("ABRT"),
            new Signal("TTOU"),
            new Signal("TTIN"),
//            new Signal("BUS"),
//            new Signal("VTALRM"),
//            new Signal("XCPU"),
//            new Signal("PWR")
    };

    private static final String[][] commands = new String[signals.length][];
    private static final long pid = ProcessHandle.current().pid();


    static {
        for (int i = 0; i < commands.length; i++) {
            commands[i] = new String[]{ "kill", "-" + signals[i].getName(), Long.toString(pid) };
        }
    }

    @Actor
    public void thread1() {
        for (String[] command : commands) {
            try {
                new ProcessBuilder(command)
                        .directory(null)
                        .start();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    @Actor
    public void thread2(ZZZZZZZZ_Result r) {
        for (int i = 0; i < signals.length; i++) {
            Signal signal = signals[i];
            Signal.handle(signal, new MySignalHandler(i, r));
        }
    }

    private static class MySignalHandler implements SignalHandler {
        private final int num;
        private final ZZZZZZZZ_Result r;

        public MySignalHandler(int num, ZZZZZZZZ_Result r) {
            this.num = num;
            this.r = r;
        }

        @Override
        public void handle(Signal sig) {
            switch (num) {
                case 0:
                    r.r1 = true;
                    break;
                case 1:
                    r.r2 = true;
                    break;
                case 2:
                    r.r3 = true;
                    break;
                case 3:
                    r.r4 = true;
                case 4:
                    r.r5 = true;
                    break;
                case 5:
                    r.r6 = true;
                    break;
                case 6:
                    r.r7 = true;
                    break;
                case 7:
                    r.r8 = true;
                    break;
            }
        }
    }
}

-------------

PR: https://git.openjdk.org/jdk/pull/9100


More information about the core-libs-dev mailing list